{ "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": { "ExecuteTime": { "end_time": "2024-08-07T08:27:48.707160Z", "start_time": "2024-08-07T08:27:48.609910Z" }, "tags": [ "remove-cell" ] }, "outputs": [], "source": [ "#|hide\n", "#|default_exp data" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Data level\n", "\n", "Reading and advance in data processing levels to be stored in netcdf database format.\n", "\n", "```{note}\n", "raw > l1a > l1b > l1c\n", "```" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "ExecuteTime": { "end_time": "2024-08-07T08:28:05.914391Z", "start_time": "2024-08-07T08:27:48.751261Z" } }, "outputs": [], "source": [ "#|export\n", "import os\n", "import numpy as np\n", "import pandas as pd\n", "import xarray as xr\n", "import logging\n", "from toolz import assoc_in, merge_with\n", "#import pkg_resources as pkg_res\n", "import importlib.resources\n", "import warnings\n", "\n", "from trosat import sunpos as sp\n", "from trosat.cfconv import read_cfjson\n", "\n", "import pyrnet\n", "pyrnet_version = pyrnet.__version__\n", "import pyrnet.pyrnet\n", "import pyrnet.utils\n", "import pyrnet.logger\n", "import pyrnet.reports\n", "import pyrnet.qcrad\n", "\n", "# logging setup\n", "logging.basicConfig(\n", " filename='pyrnet.log',\n", " encoding='utf-8',\n", " level=logging.DEBUG,\n", " format='%(asctime)s %(name)s %(levelname)s:%(message)s'\n", ")\n", "logger = logging.getLogger(__name__)" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "ExecuteTime": { "end_time": "2024-08-07T08:28:08.393227Z", "start_time": "2024-08-07T08:28:05.922915Z" } }, "outputs": [], "source": [ "import matplotlib.pyplot as plt" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## netCDF dataset operations\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Filename\n", "Filename from config format string." ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "ExecuteTime": { "end_time": "2024-08-07T08:28:08.448215Z", "start_time": "2024-08-07T08:28:08.406783Z" } }, "outputs": [], "source": [ "#|export\n", "def get_fname(ds, freq, period=None, timevar='time', sfx='nc', kind=None, station=None, config=None):\n", " config = get_config(config)\n", " if period is None:\n", " period = ds[timevar].values[-1] - ds[timevar].values[0]\n", " period = pd.to_timedelta(period).floor(\"s\").isoformat()\n", " if kind is None:\n", " if ds.station.size==1:\n", " kind = 's'\n", " station = ds.station.values[0]\n", " else:\n", " kind = 'n'\n", " station = ds.station.size\n", " format_dict = dict(\n", " dt = pd.to_datetime(ds[timevar].values[0]),\n", " period = period,\n", " campaign = config[\"campaign\"],\n", " kind = kind,\n", " station = int(station),\n", " level = ds.processing_level,\n", " freq = freq,\n", " collection = int(config[\"collection\"]),\n", " sfx = sfx\n", " )\n", " return config[\"output\"].format(**format_dict) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Update netcdf coverage metadata\n", "Add and update ACDD covarge metadata to dataset" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "ExecuteTime": { "end_time": "2024-08-07T08:28:08.557399Z", "start_time": "2024-08-07T08:28:08.472550Z" } }, "outputs": [], "source": [ "#|export\n", "def update_coverage_meta(ds, timevar='time'):\n", " \"\"\"Update global attributes related to geospatial and time coverage\n", " \"\"\"\n", " duration = ds[timevar].values[-1] - ds[timevar].values[0]\n", " resolution = np.mean(np.diff(ds[timevar].values))\n", " now = pd.to_datetime(np.datetime64(\"now\"))\n", " gattrs = {\n", " 'date_created': now.isoformat(),\n", " 'geospatial_lat_min': np.nanmin(ds.lat.values),\n", " 'geospatial_lat_max': np.nanmax(ds.lat.values),\n", " 'geospatial_lat_units': 'degN',\n", " 'geospatial_lon_min': np.nanmin(ds.lon.values),\n", " 'geospatial_lon_max': np.nanmax(ds.lon.values),\n", " 'geospatial_lon_units': 'degE',\n", " 'time_coverage_start': pd.to_datetime(ds[timevar].values[0]).isoformat(),\n", " 'time_coverage_end': pd.to_datetime(ds[timevar].values[-1]).isoformat(),\n", " 'time_coverage_duration': pd.to_timedelta(duration).isoformat(),\n", " 'time_coverage_resolution': pd.to_timedelta(resolution).isoformat(),\n", " }\n", " ds.attrs.update(gattrs)\n", " return ds\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Stretch resolution\n", "Update encoding *scale_factor* and *add_offset* to use the full dtype resolution." ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "ExecuteTime": { "end_time": "2024-08-07T08:28:08.610985Z", "start_time": "2024-08-07T08:28:08.566647Z" } }, "outputs": [], "source": [ "#|export\n", "def stretch_resolution(ds: xr.Dataset) -> xr.Dataset:\n", " \"\"\" Stretch variable resolution to full integer size,\n", " to not lose resolution after averaging ADC count data.\"\"\"\n", " for var in ds:\n", " if \"scale_factor\" not in ds[var].encoding:\n", " continue\n", " if \"valid_range\" not in ds[var].attrs:\n", " continue\n", " dtype = ds[var].encoding['dtype']\n", " valid_range = ds[var].valid_range\n", " int_limit = np.iinfo(dtype).max\n", " scale_factor = ds[var].encoding['scale_factor']\n", " scale_factor_mod = int((int_limit-1)/valid_range[1])\n", " ds[var].encoding.update({\n", " \"scale_factor\": scale_factor / scale_factor_mod,\n", " \"_FillValue\": int_limit,\n", " })\n", " ds[var].attrs.update({\n", " \"valid_range\": valid_range * scale_factor_mod\n", " })\n", " return ds" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Write to netcdf\n", "Ensure complete metadata and merge if needed before writing to netcdf." ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "ExecuteTime": { "end_time": "2024-08-07T08:28:08.673319Z", "start_time": "2024-08-07T08:28:08.641672Z" } }, "outputs": [], "source": [ "#|export\n", "def to_netcdf(ds, fname, timevar=\"time\"):\n", " \"\"\"xarray to netcdf, but merge if exist\n", " \"\"\"\n", " # save to netCDF4\n", " ds = update_coverage_meta(ds, timevar=timevar)\n", " ds.to_netcdf(fname,\n", " encoding={timevar:{'dtype':'float64'}}) # for OpenDAP 2 compatibility\n", "#|export\n", "def to_netcdf_l1b(ds, fname, freq='1s', timevar=\"time\"):\n", " \"\"\"xarray to netcdf, but merge if exist\n", " \"\"\"\n", " # merge if necessary\n", " if isinstance(ds, xr.Dataset):\n", " dslist = [ds]\n", " else:\n", " dslist = ds\n", " \n", " if os.path.exists(fname):\n", " ds1 = xr.load_dataset(fname)\n", " dslist.append(ds1)\n", " \n", " ds = merge_l1b(dslist, freq=freq, timevar=timevar)\n", " # save to netCDF4\n", " ds = update_coverage_meta(ds, timevar=timevar)\n", " \n", " if os.path.exists(fname): \n", " os.remove(fname)\n", " ds.to_netcdf(fname,\n", " encoding={timevar:{'dtype':'float64'}}) # for OpenDAP 2 compatibility" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Resample\n", "Specialized (fast) resample methods, as xarray resample is sometimes very slow." ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "ExecuteTime": { "end_time": "2024-08-07T08:28:08.714140Z", "start_time": "2024-08-07T08:28:08.680291Z" } }, "outputs": [], "source": [ "#|export\n", "def resample(ds, freq, methods='mean', kwargs={}):\n", " \"\"\" Resample xarray dataset using pandas for speed.\n", " https://github.com/pydata/xarray/issues/4498#issuecomment-706688398\n", " \"\"\"\n", " if isinstance(methods,str):\n", " methods = [methods]\n", "\n", " dsr = ds.to_dataframe().resample(freq)\n", " dsouts = []\n", " for method in methods:\n", " # what we want (quickly), but in Pandas form\n", " with warnings.catch_warnings():\n", " warnings.simplefilter(action='ignore', category=FutureWarning)\n", " df_h = dsr.apply(method)\n", " # rebuild xarray dataset with attributes\n", " vals = []\n", " for c in df_h.columns:\n", " vals.append(\n", " xr.DataArray(data=df_h[c],\n", " dims=['time'],\n", " coords={'time': df_h.index},\n", " attrs=ds[c].attrs)\n", " )\n", " dsouts.append(xr.Dataset(dict(zip(df_h.columns, vals)), attrs=ds.attrs))\n", "\n", " if len(dsouts) == 1:\n", " dsouts = dsouts[0]\n", " return dsouts" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Config \n", "Parse default config" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "ExecuteTime": { "end_time": "2024-08-07T08:28:08.766325Z", "start_time": "2024-08-07T08:28:08.727184Z" } }, "outputs": [], "source": [ "#|export\n", "def get_config(config: dict|None = None) -> dict:\n", " \"\"\"Read default config and merge with input config\n", " \"\"\"\n", "\n", " fn_config = os.path.join(importlib.resources.files(\"pyrnet\"), \"share/pyrnet_config.json\")\n", " default_config = pyrnet.utils.read_json(fn_config)\n", " if config is None:\n", " config = default_config\n", " config = {**default_config, **config}\n", "\n", " # add default files\n", " cfiles = {\n", " \"file_cfmeta\": \"share/pyrnet_cfmeta.json\",\n", " \"file_calibration\": \"share/pyrnet_calibration.json\",\n", " \"file_mapping\": \"share/pyrnet_station_map.json\",\n", " \"file_gti_angles\": \"share/pyrnet_gti_angles.json\",\n", " \"file_site\": \"share/pyrnet_sites.json\",\n", " }\n", " for fn in cfiles:\n", " if config[fn] is None:\n", " config[fn] = os.path.join(importlib.resources.files(\"pyrnet\"), cfiles[fn])\n", " return config\n", "\n", "def get_sensor_config(sconfig: dict|None = None) -> dict:\n", " \"\"\" Read the sensor configuration from the default json file and merge if needed. \n", " \"\"\"\n", " fn_config = os.path.join(importlib.resources.files(\"pyrnet\"), \"share/pyrnet_sensor_config.json\")\n", " default_config = pyrnet.utils.read_json(fn_config)\n", " if sconfig is None:\n", " sconfig = default_config\n", " sconfig = {**default_config, **sconfig}\n", " return sconfig\n", "\n", "def get_cfmeta(config: dict|None = None) -> dict:\n", " \"\"\"Read global and variable attributes and encoding from cfmeta.json\n", " \"\"\"\n", " config= get_config(config)\n", " # parse the json file\n", " cfdict = read_cfjson(config[\"file_cfmeta\"])\n", " # get global attributes:\n", " gattrs = cfdict['attributes']\n", " # apply config\n", " gattrs = {k:v.format_map(config) for k,v in gattrs.items()}\n", " # get variable attributes\n", " d = pyrnet.utils.get_var_attrs(cfdict)\n", " # split encoding attributes\n", " vattrs, vencode = pyrnet.utils.get_attrs_enc(d)\n", " return gattrs, vattrs, vencode" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Encoding\n", "The encoding attributes are defined within the file PyrNet/share/pyrnet_cfmeta.json per default.\n", "\n", "The netcdf packing of data is realized via the two attributes *scale_factor* and *add_offset*:\n", " ```unpacked_variable = scale_factor * packed_variable + add_offset```" ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "ExecuteTime": { "end_time": "2024-08-07T08:28:08.798727Z", "start_time": "2024-08-07T08:28:08.776513Z" } }, "outputs": [], "source": [ "#|export\n", "def calc_encoding(sconfig:dict, ADCV=3.3, ADCbits=10) -> dict:\n", " ADCfac = ADCV / (2**ADCbits-1) # Last bit is reserved \n", " sencoding = {}\n", " for k, v in sconfig.items():\n", " sencoding.update(\n", " {k: dict(\n", " units=v['units'],\n", " scale_factor=v['C']*ADCfac/v['gain'],\n", " add_offset=v['offset'],\n", " valid_range= [0, min(((2**ADCbits-1), int(v['Vmax']*v['gain']/ADCfac)))]\n", " )}\n", " )\n", " return sencoding" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "ExecuteTime": { "end_time": "2024-08-07T08:28:08.904118Z", "start_time": "2024-08-07T08:28:08.831580Z" } }, "outputs": [ { "data": { "text/plain": [ "{'ta': {'units': 'K',\n", " 'scale_factor': 0.12903225806451613,\n", " 'add_offset': 253.15,\n", " 'valid_range': [0, 775]},\n", " 'rh': {'units': '1',\n", " 'scale_factor': 0.0012903225806451613,\n", " 'add_offset': 0.0,\n", " 'valid_range': [0, 775]},\n", " 'battery_voltage': {'units': 'V',\n", " 'scale_factor': 0.0064516129032258064,\n", " 'add_offset': 0.0,\n", " 'valid_range': [0, 992]},\n", " 'ghi': {'units': 'V',\n", " 'scale_factor': 1.075268817204301e-05,\n", " 'add_offset': 0.0,\n", " 'valid_range': [0, 1023]},\n", " 'gti': {'units': 'V',\n", " 'scale_factor': 1.075268817204301e-05,\n", " 'add_offset': 0.0,\n", " 'valid_range': [0, 1023]}}" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "sconfig = get_sensor_config()\n", "sencoding = calc_encoding(sconfig)\n", "sencoding" ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "ExecuteTime": { "end_time": "2024-08-07T08:28:08.982774Z", "start_time": "2024-08-07T08:28:08.910075Z" }, "tags": [ "hide-input", "hide-output" ] }, "outputs": [ { "data": { "text/plain": [ "{'attributes': {'Conventions': 'CF-1.10, ACDD-1.3',\n", " 'title': 'TROPOS pyranometer network (PyrNet) observational data set',\n", " 'history': '',\n", " 'institution': 'Leibniz Institute for Tropospheric Research (TROPOS)',\n", " 'source': 'TROPOS pyranometer network (PyrNet)',\n", " 'references': 'https://doi.org/10.5194/amt-9-1153-2016',\n", " 'Department': 'Remote Sensing of Atmospheric Processes',\n", " 'Department_team': 'Clouds, Aerosol and Radiation',\n", " 'Address': 'Permoser Str. 15, 04318 Leipzig, Germany',\n", " 'Contact_person': 'Andreas Macke and the clouds, aerosol and radiation team of the remote sensing department, mailto:andreas.macke@tropos.de',\n", " 'Contributor_name': '{contributor_name}',\n", " 'Contributor_role': '{contributor_role}',\n", " 'Authors_software': 'Hartwig Deneke, Jonas Witthuhn, mailto:deneke@tropos.de',\n", " 'Creator_name': '{creator_name}',\n", " 'Project': '{project}',\n", " 'Standard_name_vocabulary': 'CF Standard Name Table v81',\n", " 'License': 'CC-BY-SA 3.0'},\n", " 'variables': {'ta': {'type': 'u2',\n", " 'attributes': {'units': 'K',\n", " 'long_name': 'air temperature',\n", " 'standard_name': 'air_temperature',\n", " 'scale_factor': 0.01,\n", " 'add_offset': 253.15,\n", " '_FillValue': np.uint16(65535),\n", " 'valid_range': [np.uint16(0), np.uint16(10000)],\n", " 'zlib': True}},\n", " 'rh': {'type': 'u2',\n", " 'attributes': {'units': '1',\n", " 'long_name': 'air relative humidity',\n", " 'standard_name': 'relative_humidity',\n", " 'scale_factor': 0.0001,\n", " 'add_offset': 0.0,\n", " 'valid_range': [np.uint16(0), np.uint16(10000)],\n", " '_FillValue': np.uint16(65535),\n", " 'zlib': True}},\n", " 'battery_voltage': {'type': 'u2',\n", " 'attributes': {'units': 'V',\n", " 'standard_name': 'battery_voltage',\n", " 'scale_factor': 0.001,\n", " 'add_offset': 0.0,\n", " 'valid_range': [np.uint16(0), np.uint16(6400)],\n", " '_FillValue': np.uint16(65535),\n", " 'zlib': True}},\n", " 'ghi': {'type': 'u2',\n", " 'attributes': {'units': 'W m-2',\n", " 'long_name': 'downwelling shortwave flux',\n", " 'standard_name': 'downwelling_shortwave_flux_in_air',\n", " 'scale_factor': 0.025,\n", " 'add_offset': 0.0,\n", " 'valid_range': [np.uint16(0), np.uint16(60000)],\n", " '_FillValue': np.uint16(65535),\n", " 'zlib': True}},\n", " 'gti': {'type': 'u2',\n", " 'attributes': {'units': 'V',\n", " 'long_name': 'downwelling shortwave flux measured on secondary platform, might be tilted',\n", " 'standard_name': 'downwelling_shortwave_flux_in_air',\n", " 'scale_factor': 0.025,\n", " 'add_offset': 0.0,\n", " 'valid_range': [np.uint16(0), np.uint16(60000)],\n", " '_FillValue': np.uint16(65535),\n", " 'zlib': True}},\n", " 'station': {'type': 'u1',\n", " 'attributes': {'units': '-',\n", " 'long_name': 'PyrNet unit box number',\n", " '_FillValue': np.uint8(255),\n", " 'zlib': True}},\n", " 'szen': {'type': 'u2',\n", " 'attributes': {'standard_name': 'solar_zenith_angle',\n", " 'units': 'degree',\n", " 'scale_factor': 0.005,\n", " 'add_offset': 0.0,\n", " 'valid_range': [np.uint16(0), np.uint16(36000)],\n", " '_FillValue': np.uint16(65535),\n", " 'zlib': True}},\n", " 'sazi': {'type': 'u2',\n", " 'attributes': {'standard_name': 'solar_azimuth_angle',\n", " 'units': 'degree',\n", " 'scale_factor': 0.01,\n", " 'add_offset': 0.0,\n", " 'valid_range': [np.uint16(0), np.uint16(36000)],\n", " '_FillValue': np.uint16(65535),\n", " 'zlib': True}},\n", " 'esd': {'type': 'f8',\n", " 'attributes': {'long_name': 'Earth-sun distance',\n", " 'note': 'Calculated based on Spencer (1971), as mean of its values over all time steps.',\n", " 'units': 'ua'}},\n", " 'maintenance_flag': {'type': 'u1',\n", " 'attributes': {'standard_name': 'quality_flag',\n", " 'long_name': 'Maintenance quality control flags',\n", " 'note': 'Soiling describes subjectively the coverage of the pyranometer dome with dirt. The level flag is problematic if the bubble of the spirit level touches the reference ring, and bad if it is outside.',\n", " 'valid_range': [np.uint8(0), np.uint8(11)],\n", " 'flag_masks': [np.uint8(3),\n", " np.uint8(3),\n", " np.uint8(3),\n", " np.uint8(12),\n", " np.uint8(12)],\n", " 'flag_values': [np.uint8(1),\n", " np.uint8(2),\n", " np.uint8(3),\n", " np.uint8(4),\n", " np.uint8(8)],\n", " 'flag_meanings': 'soiling_light soiling_moderate soiling_heavy level_problematic level_bad',\n", " '_FillValue': np.uint8(255),\n", " 'zlib': True}},\n", " 'qc_flag': {'type': 'u1',\n", " 'attributes': {'standard_name': 'quality_flag',\n", " 'long_name': 'Automatic quality checks.',\n", " 'valid_range': [np.uint8(0), np.uint8(127)],\n", " 'flag_masks': [np.uint8(1),\n", " np.uint8(2),\n", " np.uint8(4),\n", " np.uint8(8),\n", " np.uint8(16),\n", " np.uint8(32),\n", " np.uint8(64)],\n", " 'flag_values': [np.uint8(1),\n", " np.uint8(2),\n", " np.uint8(4),\n", " np.uint8(8),\n", " np.uint8(16),\n", " np.uint8(32),\n", " np.uint8(64)],\n", " 'flag_meanings': 'below_physical_limit above_physical_limit below_rare_limit above_rare_limit comparison_to_low comparison_to_high quality_control_failed',\n", " '_FillValue': np.uint8(255),\n", " 'zlib': True}},\n", " 'lat': {'type': 'f8',\n", " 'attributes': {'standard_name': 'latitude',\n", " 'units': 'degree_north',\n", " 'valid_range': [np.float64(-90.0), np.float64(90.0)],\n", " '_FillValue': np.float64(-9999.0),\n", " 'zlib': True}},\n", " 'lon': {'type': 'f8',\n", " 'attributes': {'standard_name': 'longitude',\n", " 'units': 'degree_east',\n", " 'valid_range': [np.float64(-180.0), np.float64(180.0)],\n", " '_FillValue': np.float64(-99999.0),\n", " 'zlib': True}},\n", " 'iadc': {'type': 'u4',\n", " 'attributes': {'standard_name': 'index',\n", " 'comment': 'index to map gps to adc records',\n", " 'units': '-',\n", " '_FillValue': np.uint32(4294967295),\n", " 'zlib': True}},\n", " 'adctime': {'type': 'u4',\n", " 'attributes': {'standard_name': 'time',\n", " '_FillValue': np.uint32(4294967295),\n", " 'zlib': True}},\n", " 'time': {'type': 'f8',\n", " 'attributes': {'standard_name': 'time', 'zlib': True}}}}" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "#|dropcode\n", "#|dropout\n", "fn_cfjson = os.path.join(importlib.resources.files(\"pyrnet\"), \"share/pyrnet_cfmeta.c01.json\")\n", "read_cfjson(fn_cfjson)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Irradiance values\n", "\n", "Irradiance values are stored as Voltage for later calibration. Assigning 1500Wm-2 as maximum measurable irradiance from the irradiance sensor. The maximum counts (*valid_max*) measured by the logger can be calculated by:\n", " * maximum counts = $\\mathrm{gain}*1500*\\mathrm{C_{max}}*1023/3.3$\n", "\n", "Most calibration factors are in the area of 7.5 uV/Wm-2, assuming $\\mathrm{C_{max}}=8$uV/Wm-2 for this estimate seems sufficient. All radiation sensors are amplified with a fixed gain of $\\mathrm{gain}=300$.\n", "\n", "Later (level 1c+) the scale factor will be instrument specific by adding the calibration from V to W m-2\n", "\n", "### Temperature and humidity\n", "\n", "\n", "Calibration coefficients for Temperature and Humidity DKRF-4000 series (discontinued) https://www.driesen-kern.de/downloads/produktlinie_feuchte.pdf are:\n", " * Temperature (T) range :-20-80 degC from 0-5V\n", " * relative Humidity (rH) range: 0-100% from 0-5V\n", "\n", "As the logger ADC range is 0-3.3V with a 10bit resolution, the sensors are measured through a voltage splitting circuit. Therefore, the ADC counts have to be doubled.\n", " * Voltage U [V] = $2* 3.3/1023$ [counts]\n", " * T = $-20 + 100*(U/5)$ [degC] = $253.15 + 100*(U/5)$ [K]\n", " * rH = $(U/5)$ [%] = $(U/5)$ [%]\n", "\n", "### Ancillary data\n", "\n", "* solar zenith angle (*szen*)\n", " * valid range unpacked: (0,180) (deg)\n", " * packing in u2 integer (unsigned 16bit)\n", " * fill value = $2^{16} - 1$\n", " * scale_factor = $180 /(2^{16}-2)$\n", "* solar azimuth angle (*sazi*)\n", " * valid range unpacked: (0,360)\n", " * packing in u2 integer (unsigned 16bit)\n", " * fill_value = $2^{16} -1$\n", " * scale_factor= $360 / (2^{16}-2)$\n", "* earth sun distance (*esd*)\n", " * valid range unpacked: (0.98,1.02)\n", " * packing in u2 integer (unsigned 16bit)\n", " * fill_value = $2^{16} -1$\n", " * scale_factor= $(1.02-0.98)/(2^{16}-2)$\n", " * add_offset = 0.98\n" ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "ExecuteTime": { "end_time": "2024-08-07T08:28:09.034368Z", "start_time": "2024-08-07T08:28:08.988692Z" }, "tags": [ "hide-input", "hide-output" ] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "ta scale_factor: 0.12903225806451613\n", "ta add_offset: 253.15\n", "ta valid_range: [0, 775]\n", "\n", "rh scale_factor: 0.0012903225806451613\n", "rh add_offset: 0\n", "rh valid_range: [0, 775]\n", "\n", "battery scale_factor: 0.0064516129032258064\n", "battery add_offset: 0\n", "battery valid_range: [0, 992]\n", "\n", "radflux scale_factor: 1.075268817204301e-05\n", "radflux add_offset: 0\n", "radflux valid_range: [0, 1023]\n", "\n", "scale_factor szen (deg): 0.005\n", "add_offset szen: 0.0 \n", "valid_range szen: [0, 36000]\n", "\n", "scale_factor sazi (deg): 0.01\n", "add_offset sazi: 0.0 \n", "valid_range sazi: [0, 36000]\n", "\n", "scale_factor esd (AU): 1.0000000000000008e-06\n", "add_offset esd: 0.98 \n", "valid_range esd: [0, 40000]\n", "\n", "scale_factor lat (degree_north): 1e-06\n", "add_offset lat (degree_north): -90.\n", "valid_range lat: [0, 180_000_000]\n", "\n", "scale_factor lon (degree_east): 1e-06\n", "add_offset lon (degree_east): -180.\n", "valid_range lon: [0, 360_000_000]\n", "\n" ] } ], "source": [ "#|dropcode\n", "#|dropout\n", "sensor_config = {\n", " \"ta\": {\n", " \"C\": 100./5., # K/V\n", " \"offset\": 253.15, # K at 0V\n", " \"gain\": 0.5, # measured over voltage splitter\n", " \"Vmax\": 5, # V output at sensor\n", " },\n", " \"rh\": {\n", " \"C\": 1./5., # 1/V\n", " \"offset\": 0, # 0 at 0V\n", " \"gain\": 0.5, # measured over voltage splitter\n", " \"Vmax\": 5, # V output at sensor\n", " },\n", " \"battery\": {\n", " \"C\": 1., # V(batt)/V\n", " \"offset\": 0, # V at 0V\n", " \"gain\": 0.5, # measured over voltage splitter\n", " \"Vmax\": 6.4, # V output at sensor\n", " },\n", " \"radflux\": {\n", " \"C\": 1., # Vsensor/Vmeasured\n", " \"offset\": 0, # V at 0V\n", " \"gain\": 300, # amplified\n", " \"Vmax\": 1500*8*1e-6, # V output at sensor\n", " },\n", "}\n", "ADCfac = 3.3/1023. # V/counts\n", "for var in sensor_config:\n", " sconf = sensor_config[var]\n", " print(f\"{var} scale_factor: {sconf['C']*ADCfac/sconf['gain']}\")\n", " print(f\"{var} add_offset: {sconf['offset']}\")\n", " print(f\"{var} valid_range: [0, {min((1023, int(sconf['Vmax']*sconf['gain']/ADCfac)))}]\")\n", " print()\n", "\n", "# szen encoding\n", "print(f\"scale_factor szen (deg): {180./(36_000)}\")\n", "print(f\"add_offset szen: 0.0 \")\n", "print(f\"valid_range szen: [0, {36_000}]\")\n", "print()\n", "# sazi encoding\n", "print(f\"scale_factor sazi (deg): {360./(36_000)}\")\n", "print(f\"add_offset sazi: 0.0 \")\n", "print(f\"valid_range sazi: [0, {36_000}]\")\n", "print()\n", "# esd encoding\n", "print(f\"scale_factor esd (AU): {(1.02-0.98)/(40_000)}\")\n", "print(f\"add_offset esd: 0.98 \")\n", "print(f\"valid_range esd: [0, {40_000}]\")\n", "print()\n", "# lat encoding\n", "print(f\"scale_factor lat (degree_north): {(90.-(-90.))/180_000_000}\")\n", "print(f\"add_offset lat (degree_north): -90.\")\n", "print(f\"valid_range lat: [0, 180_000_000]\")\n", "print()\n", "# lon encoding\n", "print(f\"scale_factor lon (degree_east): {(180.-(-180.))/360_000_000}\")\n", "print(f\"add_offset lon (degree_east): -180.\")\n", "print(f\"valid_range lon: [0, 360_000_000]\")\n", "print()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Add encoding to netcdf" ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "ExecuteTime": { "end_time": "2024-08-07T08:28:09.130435Z", "start_time": "2024-08-07T08:28:09.063553Z" } }, "outputs": [], "source": [ "#|export\n", "def add_encoding(ds, vencode=None):\n", " \"\"\"\n", " Set valid_range attribute and encoding to every variable of the dataset.\n", "\n", " Parameters\n", " ----------\n", " ds: xr.Dataset\n", " Dataset of any processing level. The processing level will be\n", " determined by the global attribute 'processing_level'.\n", " vencode: dict or None\n", " Dictionary of encoding attributes by variable name, will be merged with pyrnet default cfmeta. The default is None.\n", "\n", " Returns\n", " -------\n", " xr.Dataset\n", " The input dataset but with encoding and valid_range attribute.\n", " \"\"\"\n", " # prepare netcdf encoding\n", " _, vattrs_default, vencode_default = get_cfmeta()\n", "\n", " # Add valid range temporary to encoding dict.\n", " # As valid_range is not implemented in xarray encoding,\n", " # it has to be stored as a variable attribute later.\n", " for k in vencode_default:\n", " if \"valid_range\" in vencode_default[k]:\n", " continue\n", " if \"valid_range\" not in vattrs_default[k]:\n", " continue\n", " vencode_default = assoc_in(vencode_default,\n", " [k,'valid_range'],\n", " vattrs_default[k]['valid_range'])\n", " \n", " # merge input and default with preference on input\n", " if vencode is None:\n", " vencode = vencode_default\n", " else:\n", " a = vencode_default.copy()\n", " b = vencode.copy()\n", " vencode = {}\n", " for k in set(a)-set(b):\n", " vencode.update({k:a[k]})\n", " for k in set(a)&set(b):\n", " vencode.update({k: {**a[k],**b[k]}})\n", " for k in set(b)-set(a):\n", " vencode.update({k:b[k]})\n", "\n", " # add encoding to Dataset\n", " for k, v in vencode.items():\n", " for ki in [key for key in ds if key.startswith(k)]:\n", " ds[ki].encoding.update(v)\n", " if \"valid_range\" not in vencode[k]:\n", " continue\n", " # add valid_range to variable attributes\n", " for ki in [key for key in ds if key.startswith(k)]:\n", " ds[ki].attrs.update({\n", " 'valid_range': vencode[k]['valid_range']\n", " })\n", " \n", " # add encoding to coords\n", " if ds.processing_level=='l1a':\n", " ds[\"gpstime\"].encoding.update({\n", " **vencode[\"time\"],\n", " \"units\": f\"seconds since {np.datetime_as_string(ds.gpstime.data[0], unit='D')}T00:00Z\",\n", " })\n", " ds[\"maintenancetime\"].encoding.update({\n", " **vencode[\"time\"],\n", " \"units\": f\"seconds since {np.datetime_as_string(ds.maintenancetime.data[0], unit='D')}T00:00Z\",\n", " })\n", " ds[\"adctime\"].encoding.update({\n", " **vencode[\"adctime\"],\n", " \"units\": \"milliseconds\"\n", " })\n", " ds[\"station\"].encoding.update({\n", " **vencode[\"station\"]\n", " })\n", " elif ds.processing_level == 'l1b':\n", " ds[\"time\"].encoding.update({\n", " **vencode[\"time\"],\n", " \"units\": f\"seconds since {np.datetime_as_string(ds.time.data[0], unit='D')}T00:00Z\",\n", " })\n", " ds[\"maintenancetime\"].encoding.update({\n", " **vencode[\"time\"],\n", " \"units\": f\"seconds since {np.datetime_as_string(ds.maintenancetime.data[0], unit='D')}T00:00Z\",\n", " })\n", " else:\n", " raise ValueError(\"Dataset has no 'processing_level' attribute.\")\n", " return ds" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## l1a\n", "Full resolution unprocessed, uncalibrated data. One data file corresponds to one maintenance period of one PyrNet station.\n", "\n", "Level *l1a* will be processed from the logger raw data with the following workflow:\n", "\n", "1. Parse raw logger file\n", " * ```pyrnet.logger.read_records```\n", "1. Get maintenance logbook quality flags\n", " * ```pyrnet.reports```\n", "1. Get metadata and encoding\n", " * ```pyrnet_cfmeta_l1b.json```\n", "1. Make xarray Dataset\n", "1. Add variable and global attributes and encoding" ] }, { "cell_type": "code", "execution_count": 15, "metadata": { "ExecuteTime": { "end_time": "2024-08-07T08:28:09.255479Z", "start_time": "2024-08-07T08:28:09.141085Z" } }, "outputs": [], "source": [ "config = get_config()\n", "gattrs, vattrs, vencode = get_cfmeta(config)\n", "\n", "# update encoding with sensor config for l1a\n", "sconfig = get_sensor_config()\n", "sencoding = calc_encoding(sconfig, ADCV=3.3, ADCbits=10)" ] }, { "cell_type": "code", "execution_count": 16, "metadata": { "ExecuteTime": { "end_time": "2024-08-07T08:28:09.306453Z", "start_time": "2024-08-07T08:28:09.263953Z" } }, "outputs": [ { "data": { "text/plain": [ "{'ta': {'scale_factor': 0.12903225806451613,\n", " 'add_offset': 253.15,\n", " '_FillValue': np.uint16(65535),\n", " 'zlib': True,\n", " 'dtype': 'u2',\n", " 'gzip': True,\n", " 'complevel': 6,\n", " 'units': 'K'},\n", " 'rh': {'scale_factor': 0.0012903225806451613,\n", " 'add_offset': 0.0,\n", " '_FillValue': np.uint16(65535),\n", " 'zlib': True,\n", " 'dtype': 'u2',\n", " 'gzip': True,\n", " 'complevel': 6,\n", " 'units': '1'},\n", " 'battery_voltage': {'scale_factor': 0.0064516129032258064,\n", " 'add_offset': 0.0,\n", " '_FillValue': np.uint16(65535),\n", " 'zlib': True,\n", " 'dtype': 'u2',\n", " 'gzip': True,\n", " 'complevel': 6,\n", " 'units': 'V'},\n", " 'ghi': {'scale_factor': 1.075268817204301e-05,\n", " 'add_offset': 0.0,\n", " '_FillValue': np.uint16(65535),\n", " 'zlib': True,\n", " 'dtype': 'u2',\n", " 'gzip': True,\n", " 'complevel': 6,\n", " 'units': 'V'},\n", " 'gti': {'scale_factor': 1.075268817204301e-05,\n", " 'add_offset': 0.0,\n", " '_FillValue': np.uint16(65535),\n", " 'zlib': True,\n", " 'dtype': 'u2',\n", " 'gzip': True,\n", " 'complevel': 6,\n", " 'units': 'V'},\n", " 'station': {'_FillValue': np.uint8(255),\n", " 'zlib': True,\n", " 'dtype': 'u1',\n", " 'gzip': True,\n", " 'complevel': 6},\n", " 'szen': {'scale_factor': 0.005,\n", " 'add_offset': 0.0,\n", " '_FillValue': np.uint16(65535),\n", " 'zlib': True,\n", " 'dtype': 'u2',\n", " 'gzip': True,\n", " 'complevel': 6},\n", " 'sazi': {'scale_factor': 0.01,\n", " 'add_offset': 0.0,\n", " '_FillValue': np.uint16(65535),\n", " 'zlib': True,\n", " 'dtype': 'u2',\n", " 'gzip': True,\n", " 'complevel': 6},\n", " 'esd': {'dtype': 'f8', 'gzip': True, 'complevel': 6},\n", " 'maintenance_flag': {'_FillValue': np.uint8(255),\n", " 'zlib': True,\n", " 'dtype': 'u1',\n", " 'gzip': True,\n", " 'complevel': 6},\n", " 'qc_flag': {'_FillValue': np.uint8(255),\n", " 'zlib': True,\n", " 'dtype': 'u1',\n", " 'gzip': True,\n", " 'complevel': 6},\n", " 'lat': {'_FillValue': np.float64(-9999.0),\n", " 'zlib': True,\n", " 'dtype': 'f8',\n", " 'gzip': True,\n", " 'complevel': 6},\n", " 'lon': {'_FillValue': np.float64(-99999.0),\n", " 'zlib': True,\n", " 'dtype': 'f8',\n", " 'gzip': True,\n", " 'complevel': 6},\n", " 'iadc': {'_FillValue': np.uint32(4294967295),\n", " 'zlib': True,\n", " 'dtype': 'u4',\n", " 'gzip': True,\n", " 'complevel': 6},\n", " 'adctime': {'_FillValue': np.uint32(4294967295),\n", " 'zlib': True,\n", " 'dtype': 'u4',\n", " 'gzip': True,\n", " 'complevel': 6},\n", " 'time': {'zlib': True, 'dtype': 'f8', 'gzip': True, 'complevel': 6}}" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# from toolz import update_in\n", "for var,enc in sencoding.items():\n", " for k,v in enc.items():\n", " if k==\"valid_range\":\n", " vattrs = assoc_in(vattrs, [var,k], v)\n", " else:\n", " vencode = assoc_in(vencode, [var,k], v)\n", "vencode" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## to l1a_function" ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "ExecuteTime": { "end_time": "2024-08-07T08:28:09.431862Z", "start_time": "2024-08-07T08:28:09.315087Z" } }, "outputs": [], "source": [ "#|export\n", "def to_l1a(\n", " fname : str,\n", " *,\n", " station: int,\n", " report: dict|pd.DataFrame|None,\n", " date_of_measure : np.datetime64 = np.datetime64(\"now\"),\n", " config: dict|None = None,\n", " sconfig: dict|None = None,\n", " global_attrs: dict|None = None\n", ") -> xr.Dataset|None:\n", " \"\"\"\n", " Read logger raw file and parse it to xarray Dataset. Thereby, attributes and names are defined via cfmeta.json file and sun position values are calculated and added.\n", "\n", " Parameters\n", " ----------\n", " fname: str\n", " Path and filename of the raw logger file.\n", " station: int\n", " PyrNet station box number.\n", " report: dict\n", " Parsed maintenance report, see reports.ipynb\n", " bins: int\n", " Number of desired bins per day. The default is 86400, which result in\n", " mean values of 1 second steps per day. Maximum resolution is 86400000.\n", " date_of_measure: float, datetime or datetime64\n", " A rough date of measure to account for GPS week rollover. If measured in 2019, day resolution is recommended, before 2019 annual resolution, 2020 onwards not required. If float, interpreted as Julian day from 2000-01-01T12:00. the default is np.datetime64(\"now\").\n", " config: dict\n", " Stores processing specific configuration.\n", " * cfjson -> path to cfmeta.json, the default is \"../share/pyrnet_cfmeta.json\"\n", " * stripminutes -> number of minutes to be stripped from the data at start and end,\n", " the default is 5.\n", " sconfig: dict\n", " Config for ADC and amplifier for each sensor. The default is \"../share/pyrnet_sensor_config.json\"\n", " global_attrs: dict\n", " Additional global attributes for the Dataset. (Overrides cfmeta.json attributes)\n", " Returns\n", " -------\n", " xarray.Dataset\n", " Raw Logger data for one measurement periode.\n", " \"\"\"\n", " ADCV = 3.3\n", " ADCbits = 10\n", " \n", " # load and merge default config\n", " config = get_config(config)\n", " gattrs, vattrs, vencode = get_cfmeta(config)\n", " \n", " # update encoding with sensor config for l1a\n", " sconfig = get_sensor_config(sconfig)\n", " sencoding = calc_encoding(sconfig, ADCV=ADCV, ADCbits=ADCbits)\n", " # update encoding with sensor config for l1a\n", " for var, enc in sencoding.items():\n", " for k, v in enc.items():\n", " if k in [\"units\"]:\n", " vattrs = assoc_in(vattrs, [var, k], v)\n", " else:\n", " vencode = assoc_in(vencode, [var, k], v)\n", " \n", " # update additional global attributes\n", " if global_attrs is not None:\n", " gattrs.update(global_attrs)\n", "\n", " date_of_measure = pyrnet.utils.to_datetime64(date_of_measure)\n", "\n", " # 1. Parse raw file\n", " rec_adc, rec_gprmc = pyrnet.logger.read_records(fname=fname, date_of_measure=date_of_measure)\n", "\n", " if type(rec_adc)==bool or len(rec_gprmc.time)<3:\n", " logger.debug(\"Failed to load the data from the file, because of not enough stable GPS data, or file is empty.\")\n", " return None\n", "\n", " # Get ADC time\n", " adctime = pyrnet.logger.get_adc_time(rec_adc)\n", "\n", " # ADC to Volts\n", " # Drop time and internal battery sensor output (columns 0 and 1)\n", " adc_volts = ADCV * rec_adc[:,2:] / float(2**ADCbits - 1)\n", "\n", " # 2. Get Logbook maintenance quality flags\n", " key = f\"{station:03d}\"\n", " if report is None:\n", " logger.warning(\"No report available!\")\n", " report = {}\n", " if isinstance(report, pd.DataFrame):\n", " logger.info(f\"Parsing report at date {rec_gprmc.time[-1]}\")\n", " report = pyrnet.reports.parse_report(\n", " report,\n", " date_of_maintenance=rec_gprmc.time[-1],\n", " stations=station\n", " )\n", "\n", " if key not in report:\n", " logger.warning(f\"No report for station {station} available.\")\n", " warnings.warn(f\"No report for station {station} available.\")\n", " qc_main = pyrnet.reports.get_qcflag(4,3)\n", " qc_extra = pyrnet.reports.get_qcflag(4,3)\n", " vattrs = assoc_in(vattrs, [\"maintenance_flag_ghi\",\"note_general\"], \"No maintenance report!\")\n", " vattrs = assoc_in(vattrs, [\"maintenance_flag_gti\",\"note_general\"], \"No maintenance report!\")\n", " vattrs = assoc_in(vattrs, [\"maintenance_flag_ghi\",\"note_clean\"], \"\")\n", " vattrs = assoc_in(vattrs, [\"maintenance_flag_gti\",\"note_clean\"], \"\")\n", " vattrs = assoc_in(vattrs, [\"maintenance_flag_ghi\",\"note_level\"], \"\")\n", " vattrs = assoc_in(vattrs, [\"maintenance_flag_gti\",\"note_level\"], \"\")\n", " maintenancetime = np.array([rec_gprmc.time.astype('datetime64[ns]')[-1]])\n", " else:\n", " qc_main = pyrnet.reports.get_qcflag(\n", " qc_clean=report[key]['clean'],\n", " qc_level=report[key]['align']\n", " )\n", " qc_extra = pyrnet.reports.get_qcflag(\n", " qc_clean=report[key]['clean2'],\n", " qc_level=report[key]['align2']\n", " )\n", " maintenancetime = np.array([pyrnet.utils.to_datetime64(report[key][\"maintenancetime\"])]) \n", " # add qc notes\n", " vattrs = assoc_in(vattrs, [\"maintenance_flag_ghi\",\"note_general\"], report[key][\"note_general\"])\n", " vattrs = assoc_in(vattrs, [\"maintenance_flag_gti\",\"note_general\"], report[key][\"note_general\"])\n", " vattrs = assoc_in(vattrs, [\"maintenance_flag_ghi\",\"note_clean\"], report[key][\"note_clean\"])\n", " vattrs = assoc_in(vattrs, [\"maintenance_flag_gti\",\"note_clean\"], report[key][\"note_clean2\"])\n", " vattrs = assoc_in(vattrs, [\"maintenance_flag_ghi\",\"note_level\"], report[key][\"note_align\"])\n", " vattrs = assoc_in(vattrs, [\"maintenance_flag_gti\",\"note_level\"], report[key][\"note_align2\"])\n", " qc_main = np.ubyte(qc_main)\n", " qc_extra = np.ubyte(qc_extra)\n", " \n", " vattrs = assoc_in(vattrs, [\"ghi\",\"ancillary_variables\"], \"maintenance_flag_ghi\")\n", " vattrs = assoc_in(vattrs, [\"gti\",\"ancillary_variables\"], \"maintenance_flag_gti\")\n", "\n", " # 3. Add global meta data\n", " now = pd.to_datetime(np.datetime64(\"now\"))\n", " gattrs.update({\n", " 'processing_level': 'l1a',\n", " 'product_version': pyrnet_version,\n", " 'history': f'{now.isoformat()}: Generated level l1a by pyrnet version {pyrnet_version}; ',\n", " })\n", " # add site information\n", " if config['sites'] is not None:\n", " sites = pyrnet.utils.read_json(config['file_site'])[config['sites']]\n", " if key in sites:\n", " gattrs.update({ \"site\" : sites[key]})\n", "\n", " # add gti angles\n", " # default horizontal\n", " vattrs = assoc_in(vattrs, [\"gti\",\"hangle\"], 0.)\n", " vattrs = assoc_in(vattrs, [\"gti\",\"vangle\"], 0.)\n", " # update with angles from mapping file\n", " if config['gti_angles'] is not None:\n", " gti_angles = pyrnet.utils.read_json(config['file_gti_angles'])[config['gti_angles']]\n", " if key in gti_angles:\n", " hangle = np.nan if gti_angles[key][0] is None else gti_angles[key][0]\n", " vangle = np.nan if gti_angles[key][1] is None else gti_angles[key][1]\n", " vattrs = assoc_in(vattrs, [\"gti\",\"hangle\"], hangle)\n", " vattrs = assoc_in(vattrs, [\"gti\",\"vangle\"], vangle)\n", "\n", " if adc_volts.shape[1]<5: # gti data is not available\n", " adc_volts = np.concatenate((adc_volts,-1*np.ones(adc_volts.shape[0])[:,None]),axis=1)\n", "\n", " \n", "\n", " # 8. Make xarray Dataset\n", " values = {}\n", " for k, v in sconfig.items():\n", " offset = sconfig[k][\"offset\"]\n", " gain = sconfig[k][\"gain\"]\n", " C = sconfig[k][\"C\"]\n", " iadc = sconfig[k][\"iadc\"]\n", " volts = adc_volts[:,iadc][:,None]\n", " values.update(\n", " {k: offset + C*volts/gain}\n", " )\n", " \n", " # Remove rh and temperature error if input voltage to low\n", " # Allow only sensor values of voltage V < (V(battery)-2),\n", " # as input voltage needs to be at least 2V larger then sensor output\n", " for k in [\"rh\",\"ta\"]:\n", " iadc = sconfig[k][\"iadc\"]\n", " volts_sensor = (values[k][:,0]-sconfig[k][\"offset\"])/sconfig[k][\"C\"] # no gain, as we need voltage at sensor\n", " mask = volts_sensor > (values[\"battery_voltage\"][:,0] - 2.) \n", " values[k][mask,0] = np.nan \n", " \n", " ds = xr.Dataset(\n", " data_vars={\n", " \"ghi\": ((\"adctime\",\"station\"), values[\"ghi\"]), # [V]\n", " \"gti\": ((\"adctime\",\"station\"), values[\"gti\"]), # [V]\n", " \"ta\": ((\"adctime\",\"station\"), values[\"ta\"]), # [K]\n", " \"rh\": ((\"adctime\",\"station\"), values[\"rh\"]), # [-]\n", " \"battery_voltage\": ((\"adctime\",\"station\"), values[\"battery_voltage\"]), # [V]\n", " \"lat\": ((\"gpstime\",\"station\"), rec_gprmc.lat[:,None]), # [degN]\n", " \"lon\": ((\"gpstime\",\"station\"), rec_gprmc.lon[:,None]), # [degE]\n", " \"maintenance_flag_ghi\": ((\"maintenancetime\",\"station\"), [[qc_main]]),\n", " \"maintenance_flag_gti\": ((\"maintenancetime\",\"station\"), [[qc_extra]]),\n", " \"iadc\": ((\"gpstime\", \"station\"), rec_gprmc.iadc[:,None])\n", " },\n", " coords={\n", " \"adctime\": (\"adctime\", adctime.astype('timedelta64[ns]')),\n", " \"gpstime\": (\"gpstime\", rec_gprmc.time.astype('datetime64[ns]')),\n", " \"maintenancetime\": (\"maintenancetime\", maintenancetime),\n", " \"station\": (\"station\", [np.ubyte(station)]),\n", " },\n", " attrs=gattrs\n", " )\n", "\n", " # drop ocurance of douplicate gps values\n", " ds = ds.drop_duplicates(\"gpstime\")\n", "\n", " # add global coverage attributes\n", " ds = update_coverage_meta(ds, timevar=\"gpstime\")\n", "\n", " # add attributes to Dataset\n", " for k,v in vattrs.items():\n", " for key in [key for key in ds if key.startswith(k)]:\n", " ds[key].attrs.update(v)\n", "\n", " # add encoding to Dataset\n", " ds = add_encoding(ds, vencode)\n", "\n", " return ds" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Test to_l1a function" ] }, { "cell_type": "code", "execution_count": 18, "metadata": { "ExecuteTime": { "end_time": "2024-08-07T08:28:09.983931Z", "start_time": "2024-08-07T08:28:09.439294Z" }, "tags": [ "hide-output" ] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Processing fname:\n", "2022-08-30_P0DT0H0M4S_pyrnet__s001l1af10Hz.c01.nc\n" ] }, { "data": { "text/html": [ "
<xarray.Dataset> Size: 4kB\n",
"Dimensions: (adctime: 77, station: 1, gpstime: 5,\n",
" maintenancetime: 1)\n",
"Coordinates:\n",
" * adctime (adctime) timedelta64[ns] 616B 00:00:00 ... 00:00:0...\n",
" * gpstime (gpstime) datetime64[ns] 40B 2022-08-30T11:21:04.06...\n",
" * maintenancetime (maintenancetime) datetime64[ms] 8B 2023-05-08T16:0...\n",
" * station (station) uint8 1B 1\n",
"Data variables:\n",
" ghi (adctime, station) float64 616B 0.002086 ... 0.002086\n",
" gti (adctime, station) float64 616B 0.001935 ... 0.001935\n",
" ta (adctime, station) float64 616B 294.7 294.7 ... 295.0\n",
" rh (adctime, station) float64 616B 0.6245 ... 0.6297\n",
" battery_voltage (adctime, station) float64 616B 6.452 6.439 ... 6.452\n",
" lat (gpstime, station) float64 40B 51.39 51.39 ... 51.39\n",
" lon (gpstime, station) float64 40B 11.89 11.89 ... 11.89\n",
" maintenance_flag_ghi (maintenancetime, station) uint8 1B 9\n",
" maintenance_flag_gti (maintenancetime, station) uint8 1B 7\n",
" iadc (gpstime, station) uint32 20B 26 35 54 64 74\n",
"Attributes: (12/31)\n",
" Conventions: CF-1.10, ACDD-1.3\n",
" title: TROPOS pyranometer network (PyrNet) observatio...\n",
" history: 2025-10-16T11:47:26: Generated level l1a by p...\n",
" institution: Leibniz Institute for Tropospheric Research (T...\n",
" source: TROPOS pyranometer network (PyrNet)\n",
" references: https://doi.org/10.5194/amt-9-1153-2016\n",
" ... ...\n",
" geospatial_lon_max: 11.885256666666667\n",
" geospatial_lon_units: degE\n",
" time_coverage_start: 2022-08-30T11:21:04.065000\n",
" time_coverage_end: 2022-08-30T11:21:09\n",
" time_coverage_duration: P0DT0H0M4.935S\n",
" time_coverage_resolution: P0DT0H0M1.23375S<xarray.Dataset> Size: 4kB\n",
"Dimensions: (time: 77, station: 1, maintenancetime: 1)\n",
"Coordinates:\n",
" * maintenancetime (maintenancetime) datetime64[ns] 8B 2023-05-08T16:09:06\n",
" * station (station) float32 4B 1.0\n",
" * time (time) datetime64[ns] 616B 2022-08-30T11:21:01.443000 .....\n",
"Data variables:\n",
" ghi (time, station) float64 616B 0.002086 0.002086 ... 0.002086\n",
" gti (time, station) float64 616B 0.001935 0.001935 ... 0.001935\n",
" ta (time, station) float64 616B 294.7 294.7 ... 294.8 295.0\n",
" rh (time, station) float64 616B 0.6245 0.6245 ... 0.6297\n",
" battery_voltage (time, station) float64 616B 6.452 6.439 ... 6.465 6.452\n",
"Attributes: (12/31)\n",
" Conventions: CF-1.10, ACDD-1.3\n",
" title: TROPOS pyranometer network (PyrNet) observatio...\n",
" history: 2025-10-16T11:47:26: Generated level l1a by p...\n",
" institution: Leibniz Institute for Tropospheric Research (T...\n",
" source: TROPOS pyranometer network (PyrNet)\n",
" references: https://doi.org/10.5194/amt-9-1153-2016\n",
" ... ...\n",
" geospatial_lon_max: 11.885256666666667\n",
" geospatial_lon_units: degE\n",
" time_coverage_start: 2022-08-30T11:21:04.065000\n",
" time_coverage_end: 2022-08-30T11:21:09\n",
" time_coverage_duration: P0DT0H0M4.935S\n",
" time_coverage_resolution: P0DT0H0M1.23375S<xarray.Dataset> Size: 2kB\n",
"Dimensions: (time: 37, station: 1, maintenancetime: 1)\n",
"Coordinates:\n",
" * maintenancetime (maintenancetime) datetime64[ns] 8B 2023-05-08T16:09:06\n",
" * station (station) float32 4B 1.0\n",
" * time (time) datetime64[ns] 296B 2022-08-30T11:21:03.491000 .....\n",
"Data variables:\n",
" ghi (time, station) float64 296B 0.002086 0.002086 ... 0.002086\n",
" gti (time, station) float64 296B 0.001935 0.001935 ... 0.001935\n",
" ta (time, station) float64 296B 294.7 294.7 ... 294.8 294.8\n",
" rh (time, station) float64 296B 0.6245 0.6258 ... 0.6297\n",
" battery_voltage (time, station) float64 296B 6.452 6.452 ... 6.452 6.465\n",
"Attributes: (12/31)\n",
" Conventions: CF-1.10, ACDD-1.3\n",
" title: TROPOS pyranometer network (PyrNet) observatio...\n",
" history: 2025-10-16T11:47:26: Generated level l1a by p...\n",
" institution: Leibniz Institute for Tropospheric Research (T...\n",
" source: TROPOS pyranometer network (PyrNet)\n",
" references: https://doi.org/10.5194/amt-9-1153-2016\n",
" ... ...\n",
" geospatial_lon_max: 11.885256666666667\n",
" geospatial_lon_units: degE\n",
" time_coverage_start: 2022-08-30T11:21:04.065000\n",
" time_coverage_end: 2022-08-30T11:21:09\n",
" time_coverage_duration: P0DT0H0M4.935S\n",
" time_coverage_resolution: P0DT0H0M1.23375S<xarray.Dataset> Size: 492B\n",
"Dimensions: (station: 1, time: 5, maintenancetime: 1)\n",
"Coordinates:\n",
" * station (station) float32 4B 1.0\n",
" * time (time) datetime64[ns] 40B 2022-08-30T11:21:03 ... 2022-0...\n",
" * maintenancetime (maintenancetime) datetime64[ns] 8B 2023-05-08T16:09:06\n",
"Data variables:\n",
" ghi (time, station) float64 40B 0.002086 0.002085 ... 0.002086\n",
" gti (time, station) float64 40B 0.001935 0.001935 ... 0.001935\n",
" ta (time, station) float64 40B 294.8 294.8 294.8 294.8 294.8\n",
" rh (time, station) float64 40B 0.6263 0.6266 ... 0.6271 0.629\n",
" battery_voltage (time, station) float64 40B 6.445 6.446 6.45 6.448 6.458\n",
" ghi_min (time, station) float64 40B 0.002086 0.002075 ... 0.002086\n",
" gti_min (time, station) float64 40B 0.001935 0.001925 ... 0.001935\n",
" ghi_max (time, station) float64 40B 0.002086 0.002086 ... 0.002086\n",
" gti_max (time, station) float64 40B 0.001935 0.001946 ... 0.001935\n",
" ghi_std (time, station) float64 40B 0.0 3.4e-06 0.0 0.0 0.0\n",
" gti_std (time, station) float64 40B 0.0 5.069e-06 6.104e-06 0.0 0.0\n",
"Attributes: (12/31)\n",
" Conventions: CF-1.10, ACDD-1.3\n",
" title: TROPOS pyranometer network (PyrNet) observatio...\n",
" history: 2025-10-16T11:47:26: Generated level l1a by p...\n",
" institution: Leibniz Institute for Tropospheric Research (T...\n",
" source: TROPOS pyranometer network (PyrNet)\n",
" references: https://doi.org/10.5194/amt-9-1153-2016\n",
" ... ...\n",
" geospatial_lon_max: 11.885256666666667\n",
" geospatial_lon_units: degE\n",
" time_coverage_start: 2022-08-30T11:21:04.065000\n",
" time_coverage_end: 2022-08-30T11:21:09\n",
" time_coverage_duration: P0DT0H0M4.935S\n",
" time_coverage_resolution: P0DT0H0M1.23375S<xarray.Dataset> Size: 580B\n",
"Dimensions: (station: 1, time: 5, maintenancetime: 1)\n",
"Coordinates:\n",
" * station (station) float32 4B 1.0\n",
" * time (time) datetime64[ns] 40B 2022-08-30T11:21:03 ... 2...\n",
" * maintenancetime (maintenancetime) datetime64[ns] 8B 2023-05-08T16:0...\n",
"Data variables: (12/15)\n",
" ghi (time, station) float64 40B 0.002086 ... 0.002086\n",
" gti (time, station) float64 40B 0.001935 ... 0.001935\n",
" ta (time, station) float64 40B 294.8 294.8 ... 294.8\n",
" rh (time, station) float64 40B 0.6263 0.6266 ... 0.629\n",
" battery_voltage (time, station) float64 40B 6.445 6.446 ... 6.458\n",
" ghi_min (time, station) float64 40B 0.002086 ... 0.002086\n",
" ... ...\n",
" ghi_std (time, station) float64 40B 0.0 3.4e-06 0.0 0.0 0.0\n",
" gti_std (time, station) float64 40B 0.0 5.069e-06 ... 0.0 0.0\n",
" lat (time, station) float64 40B nan nan 51.39 51.39 51.39\n",
" lon (time, station) float64 40B nan nan 11.89 11.89 11.89\n",
" maintenance_flag_ghi (maintenancetime, station) float32 4B 9.0\n",
" maintenance_flag_gti (maintenancetime, station) float32 4B 7.0\n",
"Attributes: (12/31)\n",
" Conventions: CF-1.10, ACDD-1.3\n",
" title: TROPOS pyranometer network (PyrNet) observatio...\n",
" history: 2025-10-16T11:47:26: Generated level l1a by p...\n",
" institution: Leibniz Institute for Tropospheric Research (T...\n",
" source: TROPOS pyranometer network (PyrNet)\n",
" references: https://doi.org/10.5194/amt-9-1153-2016\n",
" ... ...\n",
" geospatial_lon_max: 11.885256666666667\n",
" geospatial_lon_units: degE\n",
" time_coverage_start: 2022-08-30T11:21:04.065000\n",
" time_coverage_end: 2022-08-30T11:21:09\n",
" time_coverage_duration: P0DT0H0M4.935S\n",
" time_coverage_resolution: P0DT0H0M1.23375S<xarray.Dataset> Size: 668B\n",
"Dimensions: (station: 1, time: 5, maintenancetime: 1)\n",
"Coordinates:\n",
" * station (station) float32 4B 1.0\n",
" * time (time) datetime64[ns] 40B 2022-08-30T11:21:03 ... 2...\n",
" * maintenancetime (maintenancetime) datetime64[ns] 8B 2023-05-08T16:0...\n",
"Data variables: (12/18)\n",
" ghi (time, station) float64 40B 0.002086 ... 0.002086\n",
" gti (time, station) float64 40B 0.001935 ... 0.001935\n",
" ta (time, station) float64 40B 294.8 294.8 ... 294.8\n",
" rh (time, station) float64 40B 0.6263 0.6266 ... 0.629\n",
" battery_voltage (time, station) float64 40B 6.445 6.446 ... 6.458\n",
" ghi_min (time, station) float64 40B 0.002086 ... 0.002086\n",
" ... ...\n",
" lon (time, station) float64 40B nan nan 11.89 11.89 11.89\n",
" maintenance_flag_ghi (maintenancetime, station) float32 4B 9.0\n",
" maintenance_flag_gti (maintenancetime, station) float32 4B 7.0\n",
" szen (time, station) float64 40B nan nan 42.51 42.51 42.51\n",
" sazi (time, station) float64 40B nan nan 182.9 182.9 182.9\n",
" esd (station) float64 8B 1.01\n",
"Attributes: (12/31)\n",
" Conventions: CF-1.10, ACDD-1.3\n",
" title: TROPOS pyranometer network (PyrNet) observatio...\n",
" history: 2025-10-16T11:47:26: Generated level l1a by p...\n",
" institution: Leibniz Institute for Tropospheric Research (T...\n",
" source: TROPOS pyranometer network (PyrNet)\n",
" references: https://doi.org/10.5194/amt-9-1153-2016\n",
" ... ...\n",
" geospatial_lon_max: 11.885256666666667\n",
" geospatial_lon_units: degE\n",
" time_coverage_start: 2022-08-30T11:21:04.065000\n",
" time_coverage_end: 2022-08-30T11:21:09\n",
" time_coverage_duration: P0DT0H0M4.935S\n",
" time_coverage_resolution: P0DT0H0M1.23375S<xarray.Dataset> Size: 668B\n",
"Dimensions: (station: 1, time: 5, maintenancetime: 1)\n",
"Coordinates:\n",
" * station (station) float32 4B 1.0\n",
" * time (time) datetime64[ns] 40B 2022-08-30T11:21:03 ... 2...\n",
" * maintenancetime (maintenancetime) datetime64[ns] 8B 2023-05-08T16:0...\n",
"Data variables: (12/18)\n",
" ghi (time, station) float64 40B 269.9 269.7 ... 280.9\n",
" gti (time, station) float64 40B 277.3 277.3 ... 288.6\n",
" ta (time, station) float64 40B 294.8 294.8 ... 294.8\n",
" rh (time, station) float64 40B 0.6263 0.6266 ... 0.629\n",
" battery_voltage (time, station) float64 40B 6.445 6.446 ... 6.458\n",
" ghi_min (time, station) float64 40B 269.9 268.5 ... 280.9\n",
" ... ...\n",
" lon (time, station) float64 40B nan nan 11.89 11.89 11.89\n",
" maintenance_flag_ghi (maintenancetime, station) float32 4B 9.0\n",
" maintenance_flag_gti (maintenancetime, station) float32 4B 7.0\n",
" szen (time, station) float64 40B nan nan 42.51 42.51 42.51\n",
" sazi (time, station) float64 40B nan nan 182.9 182.9 182.9\n",
" esd (station) float64 8B 1.01\n",
"Attributes: (12/31)\n",
" Conventions: CF-1.10, ACDD-1.3\n",
" title: TROPOS pyranometer network (PyrNet) observatio...\n",
" history: 2025-10-16T11:47:26: Generated level l1a by p...\n",
" institution: Leibniz Institute for Tropospheric Research (T...\n",
" source: TROPOS pyranometer network (PyrNet)\n",
" references: https://doi.org/10.5194/amt-9-1153-2016\n",
" ... ...\n",
" geospatial_lon_max: 11.885256666666667\n",
" geospatial_lon_units: degE\n",
" time_coverage_start: 2022-08-30T11:21:04.065000\n",
" time_coverage_end: 2022-08-30T11:21:09\n",
" time_coverage_duration: P0DT0H0M4.935S\n",
" time_coverage_resolution: P0DT0H0M1.23375S<xarray.Dataset> Size: 678B\n",
"Dimensions: (station: 1, time: 5, maintenancetime: 1)\n",
"Coordinates:\n",
" * station (station) float32 4B 1.0\n",
" * time (time) datetime64[ns] 40B 2022-08-30T11:21:03 ... 2...\n",
" * maintenancetime (maintenancetime) datetime64[ns] 8B 2023-05-08T16:0...\n",
"Data variables: (12/20)\n",
" ghi (time, station) float64 40B 269.9 269.7 ... 280.9\n",
" gti (time, station) float64 40B 277.3 277.3 ... 288.6\n",
" ta (time, station) float64 40B 294.8 294.8 ... 294.8\n",
" rh (time, station) float64 40B 0.6263 0.6266 ... 0.629\n",
" battery_voltage (time, station) float64 40B 6.445 6.446 ... 6.458\n",
" ghi_min (time, station) float64 40B 269.9 268.5 ... 280.9\n",
" ... ...\n",
" maintenance_flag_gti (maintenancetime, station) float32 4B 7.0\n",
" szen (time, station) float64 40B nan nan 42.51 42.51 42.51\n",
" sazi (time, station) float64 40B nan nan 182.9 182.9 182.9\n",
" esd (station) float64 8B 1.01\n",
" qc_flag_ghi (time, station) uint8 5B 0 0 0 0 0\n",
" qc_flag_gti (time, station) uint8 5B 0 0 0 0 0\n",
"Attributes: (12/31)\n",
" Conventions: CF-1.10, ACDD-1.3\n",
" title: TROPOS pyranometer network (PyrNet) observatio...\n",
" history: 2025-10-16T11:47:26: Generated level l1a by p...\n",
" institution: Leibniz Institute for Tropospheric Research (T...\n",
" source: TROPOS pyranometer network (PyrNet)\n",
" references: https://doi.org/10.5194/amt-9-1153-2016\n",
" ... ...\n",
" geospatial_lon_max: 11.885256666666667\n",
" geospatial_lon_units: degE\n",
" time_coverage_start: 2022-08-30T11:21:04.065000\n",
" time_coverage_end: 2022-08-30T11:21:09\n",
" time_coverage_duration: P0DT0H0M4.935S\n",
" time_coverage_resolution: P0DT0H0M1.23375S<xarray.Dataset> Size: 678B\n",
"Dimensions: (station: 1, time: 5, maintenancetime: 1)\n",
"Coordinates:\n",
" * station (station) float32 4B 1.0\n",
" * time (time) datetime64[ns] 40B 2022-08-30T11:21:03 ... 2...\n",
" * maintenancetime (maintenancetime) datetime64[ns] 8B 2023-05-08T16:0...\n",
"Data variables: (12/20)\n",
" ghi (time, station) float64 40B 269.9 269.7 ... 280.9\n",
" gti (time, station) float64 40B 277.3 277.3 ... 288.6\n",
" ta (time, station) float64 40B 294.8 294.8 ... 294.8\n",
" rh (time, station) float64 40B 0.6263 0.6266 ... 0.629\n",
" battery_voltage (time, station) float64 40B 6.445 6.446 ... 6.458\n",
" ghi_min (time, station) float64 40B 269.9 268.5 ... 280.9\n",
" ... ...\n",
" maintenance_flag_gti (maintenancetime, station) float32 4B 7.0\n",
" szen (time, station) float64 40B nan nan 42.51 42.51 42.51\n",
" sazi (time, station) float64 40B nan nan 182.9 182.9 182.9\n",
" esd (station) float64 8B 1.01\n",
" qc_flag_ghi (time, station) uint8 5B 0 0 0 0 0\n",
" qc_flag_gti (time, station) uint8 5B 0 0 0 0 0\n",
"Attributes: (12/31)\n",
" Conventions: CF-1.10, ACDD-1.3\n",
" title: TROPOS pyranometer network (PyrNet) observatio...\n",
" history: 2025-10-16T11:47:26: Generated level l1a by p...\n",
" institution: Leibniz Institute for Tropospheric Research (T...\n",
" source: TROPOS pyranometer network (PyrNet)\n",
" references: https://doi.org/10.5194/amt-9-1153-2016\n",
" ... ...\n",
" geospatial_lon_max: 11.885256558333333\n",
" geospatial_lon_units: degE\n",
" time_coverage_start: 2022-08-30T11:21:03\n",
" time_coverage_end: 2022-08-30T11:21:07\n",
" time_coverage_duration: P0DT0H0M4S\n",
" time_coverage_resolution: P0DT0H0M1S<xarray.Dataset> Size: 1kB\n",
"Dimensions: (station: 1, time: 9, maintenancetime: 1)\n",
"Coordinates:\n",
" * station (station) float32 4B 1.0\n",
" * time (time) datetime64[ns] 72B 2022-08-30T11:21:01 ... 2...\n",
" * maintenancetime (maintenancetime) datetime64[ns] 8B 2023-05-08T16:0...\n",
"Data variables: (12/20)\n",
" ghi (time, station) float64 72B 280.9 280.9 ... 280.9\n",
" gti (time, station) float64 72B 289.0 289.0 ... 289.4\n",
" ta (time, station) float64 72B 294.8 294.7 ... 294.8\n",
" rh (time, station) float64 72B 0.6253 0.6248 ... 0.629\n",
" battery_voltage (time, station) float64 72B 6.443 6.445 ... 6.465\n",
" gti_min (time, station) float64 72B 288.6 288.6 ... 288.6\n",
" ... ...\n",
" maintenance_flag_gti (maintenancetime, station) float32 4B 7.0\n",
" szen (time, station) float64 72B 42.51 42.51 ... 42.51\n",
" sazi (time, station) float64 72B 182.9 182.9 ... 182.9\n",
" esd (station) float64 8B 1.01\n",
" qc_flag_ghi (time, station) uint8 9B 0 0 0 0 0 0 0 0 0\n",
" qc_flag_gti (time, station) uint8 9B 0 0 0 0 0 0 0 0 0\n",
"Attributes: (12/31)\n",
" Conventions: CF-1.10, ACDD-1.3\n",
" title: TROPOS pyranometer network (PyrNet) observatio...\n",
" history: 2025-10-16T11:47:26: Generated level l1a by p...\n",
" institution: Leibniz Institute for Tropospheric Research (T...\n",
" source: TROPOS pyranometer network (PyrNet)\n",
" references: https://doi.org/10.5194/amt-9-1153-2016\n",
" ... ...\n",
" geospatial_lon_max: 11.885252\n",
" geospatial_lon_units: degE\n",
" time_coverage_start: 2022-08-30T11:21:01\n",
" time_coverage_end: 2022-08-30T11:21:09\n",
" time_coverage_duration: P0DT0H0M8S\n",
" time_coverage_resolution: P0DT0H0M1S<xarray.Dataset> Size: 1kB\n",
"Dimensions: (station: 1, time: 9, maintenancetime: 1)\n",
"Coordinates:\n",
" * station (station) float32 4B 1.0\n",
" * time (time) datetime64[ns] 72B 2022-08-30T11:21:01 ... 2...\n",
" * maintenancetime (maintenancetime) datetime64[ns] 8B 2023-05-08T16:0...\n",
"Data variables: (12/20)\n",
" ghi (time, station) float64 72B 280.9 280.9 ... 280.9\n",
" gti (time, station) float64 72B 289.0 289.0 ... 289.0\n",
" ta (time, station) float64 72B 294.8 294.7 ... 294.8\n",
" rh (time, station) float64 72B 0.6253 0.6248 ... 0.6253\n",
" battery_voltage (time, station) float64 72B 6.443 6.445 ... 6.443\n",
" gti_min (time, station) float64 72B 288.6 288.6 ... 288.6\n",
" ... ...\n",
" qc_flag_gti (time, station) uint8 9B 0 0 0 0 0 0 0 0 0\n",
" lat (station) float64 8B 51.39\n",
" lon (station) float64 8B 11.89\n",
" esd (station) float64 8B 1.01\n",
" maintenance_flag_ghi (maintenancetime, station) float64 8B 9.0\n",
" maintenance_flag_gti (maintenancetime, station) float64 8B 7.0\n",
"Attributes: (12/32)\n",
" Conventions: CF-1.10, ACDD-1.3\n",
" title: TROPOS pyranometer network (PyrNet) observatio...\n",
" history: 2025-10-16T11:47:26: Merged level l1b by pyrne...\n",
" institution: Leibniz Institute for Tropospheric Research (T...\n",
" source: TROPOS pyranometer network (PyrNet)\n",
" references: https://doi.org/10.5194/amt-9-1153-2016\n",
" ... ...\n",
" geospatial_lon_units: degE\n",
" time_coverage_start: 2022-08-30T11:21:01\n",
" time_coverage_end: 2022-08-30T11:21:09\n",
" time_coverage_duration: P0DT0H0M8S\n",
" time_coverage_resolution: P0DT0H0M1S\n",
" site: [np.str_('testsite1'), np.str_('testsite2')]