From 5f054b330b215a2597a221afccbcfad82ab17d19 Mon Sep 17 00:00:00 2001 From: Baptiste Vandecrux <35140661+BaptisteVandecrux@users.noreply.github.com> Date: Fri, 5 Jul 2024 15:07:29 +0200 Subject: [PATCH] Feature/smoothing and extrapolating gps coordinates (#268) * implemented gps postprocessing on top of the #262 This update: - clears up the SHF LHF calculation - reads dates of station relocations (when station coordinates are discontinuous) from the `aws-l0/metadata/station_configurations` - for each interval between station relocations, a linear function is fitted to the GPS observations of latitude longitude and altitude and is used to interpolate and extrapolate the gps observations - these new smoothed and gap-free coordinates are the variables `lat, lon, alt` - for bedrock stations (like KAN_B) static coordinates are used to build `lat, lon, alt` - eventually `lat_avg`, `lon_avg` `alt_avg` are calculated from `lat, lon, alt` and added as attributes to the netcdf files. Several minor fixes were also brought like: - better encoding info removal when reading netcdf - printing to files variables full of NaNs at `L2` and `L3/stations` but not printing them in the `L3/sites files`. - recalculate dirWindSpd if needed for historical data - due to xarray version, new columns need to be added manually before a concatenation of different datasets in join_l3 --- src/pypromice/process/L2toL3.py | 318 ++++++++++-------- src/pypromice/process/aws.py | 2 +- src/pypromice/process/get_l2tol3.py | 19 +- src/pypromice/process/join_l2.py | 2 +- src/pypromice/process/join_l3.py | 56 +-- src/pypromice/process/resample.py | 12 +- src/pypromice/process/write.py | 111 ++++-- src/pypromice/qc/github_data_issues.py | 11 +- src/pypromice/qc/persistence.py | 4 +- .../resources/variable_aliases_GC-Net.csv | 6 +- src/pypromice/resources/variables.csv | 180 +++++----- 11 files changed, 431 insertions(+), 290 deletions(-) diff --git a/src/pypromice/process/L2toL3.py b/src/pypromice/process/L2toL3.py index 2e05ff04..d78161fd 100755 --- a/src/pypromice/process/L2toL3.py +++ b/src/pypromice/process/L2toL3.py @@ -2,104 +2,157 @@ """ AWS Level 2 (L2) to Level 3 (L3) data processing """ +import pandas as pd import numpy as np import xarray as xr +import toml, os +from sklearn.linear_model import LinearRegression -def toL3(L2, T_0=273.15, z_0=0.001, R_d=287.05, eps=0.622, es_0=6.1071, - es_100=1013.246): +import logging +logger = logging.getLogger(__name__) + +def toL3(L2, station_config={}, T_0=273.15): '''Process one Level 2 (L2) product to Level 3 (L3) meaning calculating all derived variables: - - Sensible fluxes + - Turbulent fluxes + - smoothed and inter/extrapolated GPS coordinates Parameters ---------- L2 : xarray:Dataset L2 AWS data + station_config : Dict + Dictionary containing the information necessary for the processing of + L3 variables (relocation dates for coordinates processing, or thermistor + string maintenance date for the thermistors depth) T_0 : int - Steam point temperature. Default is 273.15. - z_0 : int - Aerodynamic surface roughness length for momention, assumed constant - for all ice/snow surfaces. Default is 0.001. - R_d : int - Gas constant of dry air. Default is 287.05. - eps : int - Default is 0.622. - es_0 : int - Saturation vapour pressure at the melting point (hPa). Default is 6.1071. - es_100 : int - Saturation vapour pressure at steam point temperature (hPa). Default is - 1013.246. + Freezing point temperature. Default is 273.15. ''' ds = L2 ds.attrs['level'] = 'L3' T_100 = _getTempK(T_0) # Get steam point temperature as K - # Upper boom bulk calculation - T_h_u = ds['t_u'].copy() # Copy for processing - p_h_u = ds['p_u'].copy() - WS_h_u = ds['wspd_u'].copy() - RH_cor_h_u = ds['rh_u_cor'].copy() - Tsurf_h = ds['t_surf'].copy() # T surf from derived upper boom product. TODO is this okay to use with lower boom parameters? - z_WS_u = ds['z_boom_u'].copy() + 0.4 # Get height of Anemometer - z_T_u = ds['z_boom_u'].copy() - 0.1 # Get height of thermometer + # Turbulent heat flux calculation + if ('t_u' in ds.keys()) and \ + ('p_u' in ds.keys()) and \ + ('rh_u_cor' in ds.keys()): + # Upper boom bulk calculation + T_h_u = ds['t_u'].copy() # Copy for processing + p_h_u = ds['p_u'].copy() + RH_cor_h_u = ds['rh_u_cor'].copy() + + q_h_u = calcSpHumid(T_0, T_100, T_h_u, p_h_u, RH_cor_h_u) # Calculate specific humidity + if ('wspd_u' in ds.keys()) and \ + ('t_surf' in ds.keys()) and \ + ('z_boom_u' in ds.keys()): + WS_h_u = ds['wspd_u'].copy() + Tsurf_h = ds['t_surf'].copy() # T surf from derived upper boom product. TODO is this okay to use with lower boom parameters? + z_WS_u = ds['z_boom_u'].copy() + 0.4 # Get height of Anemometer + z_T_u = ds['z_boom_u'].copy() - 0.1 # Get height of thermometer + + if not ds.attrs['bedrock']: + SHF_h_u, LHF_h_u= calcHeatFlux(T_0, T_h_u, Tsurf_h, WS_h_u, # Calculate latent and sensible heat fluxes + z_WS_u, z_T_u, q_h_u, p_h_u) - rho_atm_u = 100 * p_h_u / R_d / (T_h_u + T_0) # Calculate atmospheric density - nu_u = calcVisc(T_h_u, T_0, rho_atm_u) # Calculate kinematic viscosity - q_h_u = calcHumid(T_0, T_100, T_h_u, es_0, es_100, eps, # Calculate specific humidity - p_h_u, RH_cor_h_u) - if not ds.attrs['bedrock']: - SHF_h_u, LHF_h_u= calcHeatFlux(T_0, T_h_u, Tsurf_h, rho_atm_u, WS_h_u, # Calculate latent and sensible heat fluxes - z_WS_u, z_T_u, nu_u, q_h_u, p_h_u) - SHF_h_u, LHF_h_u = cleanHeatFlux(SHF_h_u, LHF_h_u, T_h_u, Tsurf_h, p_h_u, # Clean heat flux values - WS_h_u, RH_cor_h_u, ds['z_boom_u']) - ds['dshf_u'] = (('time'), SHF_h_u.data) - ds['dlhf_u'] = (('time'), LHF_h_u.data) - q_h_u = 1000 * q_h_u # Convert sp.humid from kg/kg to g/kg - q_h_u = cleanSpHumid(q_h_u, T_h_u, Tsurf_h, p_h_u, RH_cor_h_u) # Clean sp.humid values - ds['qh_u'] = (('time'), q_h_u.data) + ds['dshf_u'] = (('time'), SHF_h_u.data) + ds['dlhf_u'] = (('time'), LHF_h_u.data) + else: + logger.info('wspd_u, t_surf or z_boom_u missing, cannot calulate tubrulent heat fluxes') + + q_h_u = 1000 * q_h_u # Convert sp.humid from kg/kg to g/kg + ds['qh_u'] = (('time'), q_h_u.data) + else: + logger.info('t_u, p_u or rh_u_cor missing, cannot calulate tubrulent heat fluxes') # Lower boom bulk calculation - if ds.attrs['number_of_booms']==2: - # ds['wdir_l'] = _calcWindDir(ds['wspd_x_l'], ds['wspd_y_l']) # Calculatate wind direction + if ds.attrs['number_of_booms']==2: + if ('t_l' in ds.keys()) and \ + ('p_l' in ds.keys()) and \ + ('rh_l_cor' in ds.keys()): + T_h_l = ds['t_l'].copy() # Copy for processing + p_h_l = ds['p_l'].copy() + RH_cor_h_l = ds['rh_l_cor'].copy() - T_h_l = ds['t_l'].copy() # Copy for processing - p_h_l = ds['p_l'].copy() - WS_h_l = ds['wspd_l'].copy() - RH_cor_h_l = ds['rh_l_cor'].copy() - z_WS_l = ds['z_boom_l'].copy() + 0.4 # Get height of W - z_T_l = ds['z_boom_l'].copy() - 0.1 # Get height of thermometer - - rho_atm_l = 100 * p_h_l / R_d / (T_h_l + T_0) # Calculate atmospheric density - nu_l = calcVisc(T_h_l, T_0, rho_atm_l) # Calculate kinematic viscosity - q_h_l = calcHumid(T_0, T_100, T_h_l, es_0, es_100, eps, # Calculate sp.humidity - p_h_l, RH_cor_h_l) - if not ds.attrs['bedrock']: - SHF_h_l, LHF_h_l= calcHeatFlux(T_0, T_h_l, Tsurf_h, rho_atm_l, WS_h_l, # Calculate latent and sensible heat fluxes - z_WS_l, z_T_l, nu_l, q_h_l, p_h_l) - SHF_h_l, LHF_h_l = cleanHeatFlux(SHF_h_l, LHF_h_l, T_h_l, Tsurf_h, p_h_l, # Clean heat flux values - WS_h_l, RH_cor_h_l, ds['z_boom_l']) - ds['dshf_l'] = (('time'), SHF_h_l.data) - ds['dlhf_l'] = (('time'), LHF_h_l.data) - q_h_l = 1000 * q_h_l # Convert sp.humid from kg/kg to g/kg - q_h_l = cleanSpHumid(q_h_l, T_h_l, Tsurf_h, p_h_l, RH_cor_h_l) # Clean sp.humid values - ds['qh_l'] = (('time'), q_h_l.data) + q_h_l = calcSpHumid(T_0, T_100, T_h_l, p_h_l, RH_cor_h_l) # Calculate sp.humidity + if ('wspd_l' in ds.keys()) and \ + ('t_surf' in ds.keys()) and \ + ('z_boom_l' in ds.keys()): + z_WS_l = ds['z_boom_l'].copy() + 0.4 # Get height of W + z_T_l = ds['z_boom_l'].copy() - 0.1 # Get height of thermometer + WS_h_l = ds['wspd_l'].copy() + if not ds.attrs['bedrock']: + SHF_h_l, LHF_h_l= calcHeatFlux(T_0, T_h_l, Tsurf_h, WS_h_l, # Calculate latent and sensible heat fluxes + z_WS_l, z_T_l, q_h_l, p_h_l) + + ds['dshf_l'] = (('time'), SHF_h_l.data) + ds['dlhf_l'] = (('time'), LHF_h_l.data) + else: + logger.info('wspd_l, t_surf or z_boom_l missing, cannot calulate tubrulent heat fluxes') + + q_h_l = 1000 * q_h_l # Convert sp.humid from kg/kg to g/kg + ds['qh_l'] = (('time'), q_h_l.data) + else: + logger.info('t_l, p_l or rh_l_cor missing, cannot calulate tubrulent heat fluxes') + + # Smoothing and inter/extrapolation of GPS coordinates + for var in ['gps_lat', 'gps_lon', 'gps_alt']: + ds[var.replace('gps_','')] = ('time', gpsCoordinatePostprocessing(ds, var, station_config)) + + # adding average coordinate as attribute + for v in ['lat','lon','alt']: + ds.attrs[v+'_avg'] = ds[v].mean(dim='time').item() return ds +def gpsCoordinatePostprocessing(ds, var, station_config={}): + # saving the static value of 'lat','lon' or 'alt' stored in attribute + # as it might be the only coordinate available for certain stations (e.g. bedrock) + var_out = var.replace('gps_','') + coord_names = {'alt':'altitude', 'lat':'latitude','lon':'longitude'} -def calcHeatFlux(T_0, T_h, Tsurf_h, rho_atm, WS_h, z_WS, z_T, nu, q_h, p_h, + if var_out+'_avg' in list(ds.attrs.keys()): + static_value = float(ds.attrs[var_out+'_avg']) + elif coord_names[var_out] in list(ds.attrs.keys()): + static_value = float(ds.attrs[coord_names[var_out]]) + else: + static_value = np.nan + + # if there is no gps observations, then we use the static value repeated + # for each time stamp + if var not in ds.data_vars: + print('no',var,'at', ds.attrs['station_id']) + return np.ones_like(ds['t_u'].data)*static_value + + if ds[var].isnull().all(): + print('no',var,'at',ds.attrs['station_id']) + return np.ones_like(ds['t_u'].data)*static_value + + # Extract station relocations from the TOML data + station_relocations = station_config.get("station_relocation", []) + + # Convert the ISO8601 strings to pandas datetime objects + breaks = [pd.to_datetime(date_str) for date_str in station_relocations] + if len(breaks)==0: + logger.info('processing '+var+' without relocation') + else: + logger.info('processing '+var+' with relocation on ' + ', '.join([br.strftime('%Y-%m-%dT%H:%M:%S') for br in breaks])) + + return piecewise_smoothing_and_interpolation(ds[var].to_series(), breaks) + + +def calcHeatFlux(T_0, T_h, Tsurf_h, WS_h, z_WS, z_T, q_h, p_h, kappa=0.4, WS_lim=1., z_0=0.001, g=9.82, es_0=6.1071, eps=0.622, gamma=16., L_sub=2.83e6, L_dif_max=0.01, c_pd=1005., aa=0.7, - bb=0.75, cc=5., dd=0.35): + bb=0.75, cc=5., dd=0.35, R_d=287.05): '''Calculate latent and sensible heat flux using the bulk calculation method Parameters ---------- T_0 : int - Steam point temperature + Freezing point temperature T_h : xarray.DataArray Air temperature Tsurf_h : xarray.DataArray @@ -112,8 +165,6 @@ def calcHeatFlux(T_0, T_h, Tsurf_h, rho_atm, WS_h, z_WS, z_T, nu, q_h, p_h, Height of anemometer z_T : float Height of thermometer - nu : float - Kinematic viscosity of air q_h : xarray.DataArray Specific humidity p_h : xarray.DataArray @@ -128,7 +179,7 @@ def calcHeatFlux(T_0, T_h, Tsurf_h, rho_atm, WS_h, z_WS, z_T, nu, q_h, p_h, g : int Gravitational acceleration (m/s2). Default is 9.82. es_0 : int - Saturation vapour pressure at the melting point (hPa). Default is 6.1071. + Saturation vapour pressure at the melting point (hPa). Default is 6.1071. eps : int Ratio of molar masses of vapor and dry air (0.622). gamma : int @@ -151,6 +202,8 @@ def calcHeatFlux(T_0, T_h, Tsurf_h, rho_atm, WS_h, z_WS, z_T, nu, q_h, p_h, dd : int Flux profile correction constants (Holtslag & De Bruin '88). Default is 0.35. + R_d : int + Gas constant of dry air. Default is 287.05. Returns ------- @@ -159,6 +212,9 @@ def calcHeatFlux(T_0, T_h, Tsurf_h, rho_atm, WS_h, z_WS, z_T, nu, q_h, p_h, LHF_h : xarray.DataArray Latent heat flux ''' + rho_atm = 100 * p_h / R_d / (T_h + T_0) # Calculate atmospheric density + nu = calcVisc(T_h, T_0, rho_atm) # Calculate kinematic viscosity + SHF_h = xr.zeros_like(T_h) # Create empty xarrays LHF_h = xr.zeros_like(T_h) L = xr.full_like(T_h, 1E5) @@ -244,7 +300,11 @@ def calcHeatFlux(T_0, T_h, Tsurf_h, rho_atm, WS_h, z_WS, z_T, nu, q_h, p_h, # If n_elements(where(L_dif > L_dif_max)) eq 1 then break if np.all(L_dif <= L_dif_max): break - + + HF_nan = np.isnan(p_h) | np.isnan(T_h) | np.isnan(Tsurf_h) \ + | np.isnan(q_h) | np.isnan(WS_h) | np.isnan(z_T) + SHF_h[HF_nan] = np.nan + LHF_h[HF_nan] = np.nan return SHF_h, LHF_h def calcVisc(T_h, T_0, rho_atm): @@ -270,9 +330,8 @@ def calcVisc(T_h, T_0, rho_atm): # Kinematic viscosity of air in m^2/s return mu / rho_atm -def calcHumid(T_0, T_100, T_h, es_0, es_100, eps, p_h, RH_cor_h): +def calcSpHumid(T_0, T_100, T_h, p_h, RH_cor_h, es_0=6.1071, es_100=1013.246, eps=0.622): '''Calculate specific humidity - Parameters ---------- T_0 : float @@ -281,16 +340,16 @@ def calcHumid(T_0, T_100, T_h, es_0, es_100, eps, p_h, RH_cor_h): Steam point temperature in Kelvin T_h : xarray.DataArray Air temperature - eps : int - ratio of molar masses of vapor and dry air (0.622) - es_0 : float - Saturation vapour pressure at the melting point (hPa) - es_100 : float - Saturation vapour pressure at steam point temperature (hPa) p_h : xarray.DataArray Air pressure RH_cor_h : xarray.DataArray Relative humidity corrected + es_0 : float + Saturation vapour pressure at the melting point (hPa) + es_100 : float + Saturation vapour pressure at steam point temperature (hPa) + eps : int + ratio of molar masses of vapor and dry air (0.622) Returns ------- @@ -315,72 +374,67 @@ def calcHumid(T_0, T_100, T_h, es_0, es_100, eps, p_h, RH_cor_h): freezing = T_h < 0 q_sat[freezing] = eps * es_ice[freezing] / (p_h[freezing] - (1 - eps) * es_ice[freezing]) + q_nan = np.isnan(T_h) | np.isnan(p_h) + q_sat[q_nan] = np.nan + # Convert to kg/kg return RH_cor_h * q_sat / 100 -def cleanHeatFlux(SHF, LHF, T, Tsurf, p, WS, RH_cor, z_boom): - '''Find invalid heat flux data values and replace with NaNs, based on - air temperature, surface temperature, air pressure, wind speed, - corrected relative humidity, and boom height +def piecewise_smoothing_and_interpolation(data_series, breaks): + '''Smoothes, inter- or extrapolate the GPS observations. The processing is + done piecewise so that each period between station relocations are done + separately (no smoothing of the jump due to relocation). Piecewise linear + regression is then used to smooth the available observations. Then this + smoothed curve is interpolated linearly over internal gaps. Eventually, this + interpolated curve is extrapolated linearly for timestamps before the first + valid measurement and after the last valid measurement. Parameters ---------- - SHF : xarray.DataArray - Sensible heat flux - LHF : xarray.DataArray - Latent heat flux - T : xarray.DataArray - Air temperature - Tsurf : xarray.DataArray - Surface temperature - p : xarray.DataArray - Air pressure - WS : xarray.DataArray - Wind speed - RH_cor : xarray.DataArray - Relative humidity corrected - z_boom : xarray.DataArray - Boom height - + data_series : pandas.Series + Series of observed latitude, longitude or elevation with datetime index. + breaks: list + List of timestamps of station relocation. First and last item should be + None so that they can be used in slice(breaks[i], breaks[i+1]) + Returns ------- - SHF : xarray.DataArray - Sensible heat flux corrected - LHF : xarray.DataArray - Latent heat flux corrected + np.ndarray + Smoothed and interpolated values corresponding to the input series. ''' - HF_nan = np.isnan(p) | np.isnan(T) | np.isnan(Tsurf) \ - | np.isnan(RH_cor) | np.isnan(WS) | np.isnan(z_boom) - SHF[HF_nan] = np.nan - LHF[HF_nan] = np.nan - return SHF, LHF - -def cleanSpHumid(q_h, T, Tsurf, p, RH_cor): - '''Find invalid specific humidity data values and replace with NaNs, - based on air temperature, surface temperature, air pressure, - and corrected relative humidity - - Parameters - ---------- - q_h : xarray.DataArray - Specific humidity - T : xarray.DataArray - Air temperature - Tsurf : xarray.DataArray - Surface temperature - p : xarray.DataArray - Air pressure - RH_cor : xarray.DataArray - Relative humidity corrected + df_all = pd.Series(dtype=float) # Initialize an empty Series to gather all smoothed pieces + breaks = [None] + breaks + [None] + _inferred_series = [] + for i in range(len(breaks) - 1): + df = data_series.loc[slice(breaks[i], breaks[i+1])] + + # Drop NaN values and calculate the number of segments based on valid data + df_valid = df.dropna() + if df_valid.shape[0] > 2: + # Fit linear regression model to the valid data range + x = pd.to_numeric(df_valid.index).values.reshape(-1, 1) + y = df_valid.values.reshape(-1, 1) + + model = LinearRegression() + model.fit(x, y) + + # Predict using the model for the entire segment range + x_pred = pd.to_numeric(df.index).values.reshape(-1, 1) + + y_pred = model.predict(x_pred) + df = pd.Series(y_pred.flatten(), index=df.index) + # adds to list the predicted values for the current segment + _inferred_series.append(df) + + df_all = pd.concat(_inferred_series) - Returns - ------- - q_h : xarray.DataArray - Specific humidity corrected''' - q_nan = np.isnan(T) | np.isnan(RH_cor) | np.isnan(p) | np.isnan(Tsurf) - q_h[q_nan] = np.nan - return q_h - + # Fill internal gaps with linear interpolation + df_all = df_all.interpolate(method='linear', limit_area='inside') + + # Remove duplicate indices and return values as numpy array + df_all = df_all[~df_all.index.duplicated(keep='last')] + return df_all.values + def _calcAtmosDens(p_h, R_d, T_h, T_0): # TODO: check this shouldn't be in this step somewhere '''Calculate atmospheric density''' diff --git a/src/pypromice/process/aws.py b/src/pypromice/process/aws.py index 7314a19e..b3db8d9e 100644 --- a/src/pypromice/process/aws.py +++ b/src/pypromice/process/aws.py @@ -91,7 +91,7 @@ def getL1(self): logger.info('Level 1 processing...') self.L0 = [utilities.addBasicMeta(item, self.vars) for item in self.L0] self.L1 = [toL1(item, self.vars) for item in self.L0] - self.L1A = reduce(xr.Dataset.combine_first, self.L1) + self.L1A = reduce(xr.Dataset.combine_first, reversed(self.L1)) def getL2(self): '''Perform L1 to L2 data processing''' diff --git a/src/pypromice/process/get_l2tol3.py b/src/pypromice/process/get_l2tol3.py index 6f845287..ca08b17c 100644 --- a/src/pypromice/process/get_l2tol3.py +++ b/src/pypromice/process/get_l2tol3.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -import os, logging, sys +import os, logging, sys, toml import xarray as xr from argparse import ArgumentParser import pypromice @@ -12,6 +12,9 @@ def parse_arguments_l2tol3(debug_args=None): parser = ArgumentParser(description="AWS L3 script for the processing L3 "+ "data from L2. An hourly, daily and monthly L3 "+ "data product is outputted to the defined output path") + parser.add_argument('-c', '--config_folder', type=str, required=True, + default='../aws-l0/metadata/station_configurations/', + help='Path to folder with sites configuration (TOML) files') parser.add_argument('-i', '--inpath', type=str, required=True, help='Path to Level 2 .nc data file') parser.add_argument('-o', '--outpath', default=None, type=str, required=False, @@ -24,7 +27,7 @@ def parse_arguments_l2tol3(debug_args=None): args = parser.parse_args(args=debug_args) return args -def get_l2tol3(inpath, outpath, variables, metadata): +def get_l2tol3(config_folder, inpath, outpath, variables, metadata): logging.basicConfig( format="%(asctime)s; %(levelname)s; %(name)s; %(message)s", level=logging.INFO, @@ -38,15 +41,19 @@ def get_l2tol3(inpath, outpath, variables, metadata): # Remove encoding attributes from NetCDF for varname in l2.variables: if l2[varname].encoding!={}: - l2[varname].encoding = {} - + l2[varname].encoding = {} + if 'bedrock' in l2.attrs.keys(): l2.attrs['bedrock'] = l2.attrs['bedrock'] == 'True' if 'number_of_booms' in l2.attrs.keys(): l2.attrs['number_of_booms'] = int(l2.attrs['number_of_booms']) + # importing station_config (dict) from config_folder (str path) + config_file = config_folder+l2.attrs['station_id']+'.toml' + with open(config_file) as fp: + station_config = toml.load(fp) # Perform Level 3 processing - l3 = toL3(l2) + l3 = toL3(l2, station_config) # Write Level 3 dataset to file if output directory given v = getVars(variables) @@ -59,7 +66,7 @@ def get_l2tol3(inpath, outpath, variables, metadata): def main(): args = parse_arguments_l2tol3() - _ = get_l2tol3(args.inpath, args.outpath, args.variables, args.metadata) + _ = get_l2tol3(args.config_folder, args.inpath, args.outpath, args.variables, args.metadata) if __name__ == "__main__": main() diff --git a/src/pypromice/process/join_l2.py b/src/pypromice/process/join_l2.py index 06f3d8c5..6a4a1109 100644 --- a/src/pypromice/process/join_l2.py +++ b/src/pypromice/process/join_l2.py @@ -29,7 +29,7 @@ def loadArr(infile): elif infile.split('.')[-1].lower() == 'nc': with xr.open_dataset(infile) as ds: ds.load() - # Remove encoding attributes from NetCDF + # Remove encoding attributes from NetCDF for varname in ds.variables: if ds[varname].encoding!={}: ds[varname].encoding = {} diff --git a/src/pypromice/process/join_l3.py b/src/pypromice/process/join_l3.py index e062911c..cf5b1e16 100644 --- a/src/pypromice/process/join_l3.py +++ b/src/pypromice/process/join_l3.py @@ -6,6 +6,11 @@ import numpy as np import pandas as pd import xarray as xr +logging.basicConfig( + format="%(asctime)s; %(levelname)s; %(name)s; %(message)s", + level=logging.INFO, + stream=sys.stdout, +) logger = logging.getLogger(__name__) def parse_arguments_joinl3(debug_args=None): @@ -100,8 +105,20 @@ def readNead(infile): # combining thermocouple and CS100 temperatures ds['TA1'] = ds['TA1'].combine_first(ds['TA3']) ds['TA2'] = ds['TA2'].combine_first(ds['TA4']) - + ds=ds.rename(var_name) + + standard_vars_to_drop = ["NR", "TA3", "TA4", "TA5", "NR_cor", + "z_surf_1", "z_surf_2", "z_surf_combined", + "TA2m", "RH2m", "VW10m", "SZA", "SAA", + "depth_t_i_1", "depth_t_i_2", "depth_t_i_3", "depth_t_i_4", "depth_t_i_5", + "depth_t_i_6", "depth_t_i_7", "depth_t_i_8", "depth_t_i_9", "depth_t_i_10", "t_i_10m" + ] + standard_vars_to_drop = standard_vars_to_drop + [v for v in list(ds.keys()) if v.endswith("_adj_flag")] + + # Drop the variables if they are present in the dataset + ds = ds.drop_vars([var for var in standard_vars_to_drop if var in ds]) + ds=ds.rename({'timestamp':'time'}) return ds @@ -116,7 +133,8 @@ def loadArr(infile, isNead): ds = xr.Dataset.from_dataframe(df) elif infile.split('.')[-1].lower() in 'nc': - ds = xr.open_dataset(infile) + with xr.open_dataset(infile) as ds: + ds.load() # Remove encoding attributes from NetCDF for varname in ds.variables: if ds[varname].encoding!={}: @@ -202,16 +220,23 @@ def join_l3(config_folder, site, folder_l3, folder_gcnet, outpath, variables, me filepath = os.path.join(folder_l3, stid, stid+'_hour.nc') isNead = False - if station_info["project"].lower() in ["historical gc-net", "glaciobasis"]: + if station_info["project"].lower() in ["historical gc-net"]: filepath = os.path.join(folder_gcnet, stid+'.csv') isNead = True - if not os.path.isfile(filepath): - logger.info(stid+' is from an project '+folder_l3+' or '+folder_gcnet) + if not os.path.isfile(filepath): + logger.info(stid+' was listed as station but could not be found in '+folder_l3+' nor '+folder_gcnet) continue - l3, _ = loadArr(filepath, isNead) + l3, _ = loadArr(filepath, isNead) + + # removing specific variable from a given file + specific_vars_to_drop = station_info.get("skipped_variables",[]) + if len(specific_vars_to_drop)>0: + logger.info("Skipping %s from %s"%(specific_vars_to_drop, stid)) + l3 = l3.drop_vars([var for var in specific_vars_to_drop if var in l3]) + list_station_data.append((l3, station_info)) - + # Sort the list in reverse chronological order so that we start with the latest data sorted_list_station_data = sorted(list_station_data, key=lambda x: x[0].time.max(), reverse=True) sorted_stids = [info["stid"] for _, info in sorted_list_station_data] @@ -246,19 +271,10 @@ def join_l3(config_folder, site, folder_l3, folder_gcnet, outpath, variables, me for v in l3_merged.data_vars: if v not in l3.data_vars: l3[v] = l3.t_u*np.nan - - # if l3 (older data) has variables that does not have l3_merged (newer data) - # then they are removed from l3 - list_dropped = [] for v in l3.data_vars: - if v not in l3_merged.data_vars: - if v != 'z_stake': - list_dropped.append(v) - l3 = l3.drop(v) - else: - l3_merged[v] = ('time', l3_merged.t_u.data*np.nan) - logger.info('Unused variables in older dataset: '+' '.join(list_dropped)) - + if v not in l3_merged.data_vars: + l3_merged[v] = l3_merged.t_u*np.nan + # saving attributes of station under an attribute called $stid st_attrs = l3_merged.attrs.get('stations_attributes', {}) st_attrs[stid] = l3.attrs.copy() @@ -280,6 +296,8 @@ def join_l3(config_folder, site, folder_l3, folder_gcnet, outpath, variables, me # Assign site id + if not l3_merged: + logger.error('No level 2 data file found for '+site) l3_merged.attrs['site_id'] = site l3_merged.attrs['stations'] = ' '.join(sorted_stids) l3_merged.attrs['level'] = 'L3' diff --git a/src/pypromice/process/resample.py b/src/pypromice/process/resample.py index 7c7e2ed7..698a5fab 100644 --- a/src/pypromice/process/resample.py +++ b/src/pypromice/process/resample.py @@ -8,6 +8,7 @@ import logging import numpy as np import xarray as xr +from pypromice.process.L1toL2 import calcDirWindSpeeds logger = logging.getLogger(__name__) def resample_dataset(ds_h, t): @@ -35,12 +36,15 @@ def resample_dataset(ds_h, t): # recalculating wind direction from averaged directional wind speeds for var in ['wdir_u','wdir_l']: + boom = var.split('_')[1] if var in df_d.columns: - if ('wspd_x_'+var.split('_')[1] in df_d.columns) & ('wspd_y_'+var.split('_')[1] in df_d.columns): - df_d[var] = _calcWindDir(df_d['wspd_x_'+var.split('_')[1]], - df_d['wspd_y_'+var.split('_')[1]]) + if ('wspd_x_'+boom in df_d.columns) & ('wspd_y_'+boom in df_d.columns): + df_d[var] = _calcWindDir(df_d['wspd_x_'+boom], df_d['wspd_y_'+boom]) else: - logger.info(var+' in dataframe but not wspd_x_'+var.split('_')[1]+' nor wspd_y_'+var.split('_')[1]) + logger.info(var+' in dataframe but not wspd_x_'+boom+' nor wspd_y_'+boom+', recalculating them') + ds_h['wspd_x_'+boom], ds_h['wspd_y_'+boom] = calcDirWindSpeeds(ds_h['wspd_'+boom], ds_h['wdir_'+boom]) + df_d[['wspd_x_'+boom, 'wspd_y_'+boom]] = ds_h[['wspd_x_'+boom, 'wspd_y_'+boom]].to_dataframe().resample(t).mean() + df_d[var] = _calcWindDir(df_d['wspd_x_'+boom], df_d['wspd_y_'+boom]) # recalculating relative humidity from average vapour pressure and average # saturation vapor pressure diff --git a/src/pypromice/process/write.py b/src/pypromice/process/write.py index cc4d8fe5..647d6f75 100644 --- a/src/pypromice/process/write.py +++ b/src/pypromice/process/write.py @@ -48,7 +48,7 @@ def prepare_and_write(dataset, outpath, vars_df=None, meta_dict=None, time='60mi if 'gps_lon' in d2.keys(): d2 = reformat_lon(d2) else: - logger.info('%s does not have gpd_lon'%name) + logger.info('%s does not have gps_lon'%name) # Add variable attributes and metadata if vars_df is None: @@ -63,7 +63,11 @@ def prepare_and_write(dataset, outpath, vars_df=None, meta_dict=None, time='60mi d2 = roundValues(d2, vars_df) # Get variable names to write out - col_names = getColNames(vars_df, d2, remove_nan_fields=True) + if 'site_id' in d2.attrs.keys(): + remove_nan_fields = True + else: + remove_nan_fields = False + col_names = getColNames(vars_df, d2, remove_nan_fields=remove_nan_fields) # Define filename based on resample rate t = int(pd.Timedelta((d2['time'][1] - d2['time'][0]).values).total_seconds()) @@ -100,7 +104,6 @@ def prepare_and_write(dataset, outpath, vars_df=None, meta_dict=None, time='60mi writeCSV(out_csv, d2, col_names) # Write to netcdf file - col_names = col_names + ['lat', 'lon', 'alt'] writeNC(out_nc, d2, col_names) logger.info(f'Written to {out_csv}') logger.info(f'Written to {out_nc}') @@ -245,16 +248,28 @@ def addMeta(ds, meta): ds : xarray.Dataset Dataset with metadata ''' - if 'gps_lon' in ds.keys(): - ds['lon'] = ds['gps_lon'].mean() - ds['lon'].attrs = ds['gps_lon'].attrs - - ds['lat'] = ds['gps_lat'].mean() - ds['lat'].attrs = ds['gps_lat'].attrs - - ds['alt'] = ds['gps_alt'].mean() - ds['alt'].attrs = ds['gps_alt'].attrs - + if 'lon' in ds.keys(): + # caluclating average coordinates based on the extra/interpolated coords + for v in ['lat','lon','alt']: + ds.attrs[v+'_avg'] = ds[v].mean().item() + # dropping the less accurate standard coordinates given in the + # raw or tx config files + for v in ['latitude','longitude']: + if v in ds.attrs.keys(): + del ds.attrs[v] + elif 'gps_lon' in ds.keys(): + # caluclating average coordinates based on the measured coords (can be gappy) + for v in ['gps_lat','gps_lon','gps_alt']: + if v in ds.keys(): + ds.attrs[v+'_avg'] = ds[v].mean().item() + else: + ds.attrs[v+'_avg'] = np.nan + # dropping the less accurate standard coordinates given in the + # raw or tx config files + for v in ['latitude','longitude']: + if v in ds.attrs.keys(): + del ds.attrs[v] + # Attribute convention for data discovery # https://wiki.esipfed.org/Attribute_Convention_for_Data_Discovery_1-3 @@ -283,19 +298,61 @@ def addMeta(ds, meta): ds.attrs['date_metadata_modified'] = ds.attrs['date_created'] ds.attrs['processing_level'] = ds.attrs['level'].replace('L','level ') + + if 'lat' in ds.keys(): + lat_min = ds['lat'].min().values + lat_max = ds['lat'].max().values + elif 'gps_lat' in ds.keys(): + lat_min = ds['gps_lat'].min().values + lat_max = ds['gps_lat'].max().values + elif 'latitude' in ds.attrs.keys(): + lat_min = ds.attrs['latitude'] + lat_max = ds.attrs['latitude'] + else: + lat_min =np.nan + lat_max = np.nan + + + if 'lon' in ds.keys(): + lon_min = ds['lon'].min().values + lon_max = ds['lon'].max().values + elif 'gps_lon' in ds.keys(): + lon_min = ds['gps_lon'].min().values + lon_max = ds['gps_lon'].max().values + elif 'longitude' in ds.attrs.keys(): + lon_min = ds.attrs['longitude'] + lon_max = ds.attrs['longitude'] + else: + lon_min =np.nan + lon_max = np.nan + + if 'alt' in ds.keys(): + alt_min = ds['alt'].min().values + alt_max = ds['alt'].max().values + elif 'gps_alt' in ds.keys(): + alt_min = ds['gps_alt'].min().values + alt_max = ds['gps_alt'].max().values + elif 'altitude' in ds.attrs.keys(): + alt_min = ds.attrs['altitude'] + alt_max = ds.attrs['altitude'] + else: + alt_min =np.nan + alt_max = np.nan + ds.attrs['geospatial_bounds'] = "POLYGON((" + \ - f"{ds['lat'].min().values} {ds['lon'].min().values}, " + \ - f"{ds['lat'].min().values} {ds['lon'].max().values}, " + \ - f"{ds['lat'].max().values} {ds['lon'].max().values}, " + \ - f"{ds['lat'].max().values} {ds['lon'].min().values}, " + \ - f"{ds['lat'].min().values} {ds['lon'].min().values}))" - - ds.attrs['geospatial_lat_min'] = str(ds['lat'].min().values) - ds.attrs['geospatial_lat_max'] = str(ds['lat'].max().values) - ds.attrs['geospatial_lon_min'] = str(ds['lon'].min().values) - ds.attrs['geospatial_lon_max'] = str(ds['lon'].max().values) - ds.attrs['geospatial_vertical_min'] = str(ds['alt'].min().values) - ds.attrs['geospatial_vertical_max'] = str(ds['alt'].max().values) + f"{lat_min} {lon_min}, " + \ + f"{lat_min} {lon_max}, " + \ + f"{lat_max} {lon_max}, " + \ + f"{lat_max} {lon_min}, " + \ + f"{lat_min} {lon_min}))" + + ds.attrs['geospatial_lat_min'] = str(lat_min) + ds.attrs['geospatial_lat_max'] = str(lat_max) + ds.attrs['geospatial_lon_min'] = str(lon_min) + ds.attrs['geospatial_lon_max'] = str(lon_max) + ds.attrs['geospatial_vertical_min'] = str(alt_min) + ds.attrs['geospatial_vertical_max'] = str(alt_max) + ds.attrs['geospatial_vertical_positive'] = 'up' ds.attrs['time_coverage_start'] = str(ds['time'][0].values) ds.attrs['time_coverage_end'] = str(ds['time'][-1].values) @@ -386,5 +443,5 @@ def reformat_lon(dataset, exempt=['UWN', 'Roof_GEUS', 'Roof_PROMICE']): if id not in exempt: if 'gps_lon' not in dataset.keys(): return dataset - dataset['gps_lon'] = dataset['gps_lon'] * -1 - return dataset \ No newline at end of file + dataset['gps_lon'] = np.abs(dataset['gps_lon']) * -1 + return dataset diff --git a/src/pypromice/qc/github_data_issues.py b/src/pypromice/qc/github_data_issues.py index fd4d96e0..41603ed8 100644 --- a/src/pypromice/qc/github_data_issues.py +++ b/src/pypromice/qc/github_data_issues.py @@ -65,10 +65,10 @@ def flagNAN(ds_in, for v in varlist: if v in list(ds.keys()): - logger.info(f'---> flagging {t0} {t1} {v}') + logger.debug(f'---> flagging {t0} {t1} {v}') ds[v] = ds[v].where((ds['time'] < t0) | (ds['time'] > t1)) else: - logger.info(f'---> could not flag {v} not in dataset') + logger.debug(f'---> could not flag {v} not in dataset') return ds @@ -206,13 +206,14 @@ def adjustData(ds, t1 = pd.to_datetime(t1, utc=True).tz_localize(None) index_slice = dict(time=slice(t0, t1)) - if len(ds_out[var].loc[index_slice].time.time) == 0: + logger.info(f'---> {t0} {t1} {var} {func} {val}') logger.info("Time range does not intersect with dataset") continue - logger.info(f'---> {t0} {t1} {var} {func} {val}') - + else: + logger.debug(f'---> {t0} {t1} {var} {func} {val}') + if func == "add": ds_out[var].loc[index_slice] = ds_out[var].loc[index_slice].values + val # flagging adjusted values diff --git a/src/pypromice/qc/persistence.py b/src/pypromice/qc/persistence.py index 5f5d55f4..963ff786 100644 --- a/src/pypromice/qc/persistence.py +++ b/src/pypromice/qc/persistence.py @@ -83,7 +83,7 @@ def persistence_qc( mask = mask & (df[v]<99) n_masked = mask.sum() n_samples = len(mask) - logger.info( + logger.debug( f"Applying persistent QC in {v}. Filtering {n_masked}/{n_samples} samples" ) # setting outliers to NaN @@ -96,7 +96,7 @@ def persistence_qc( n_masked = mask.sum() n_samples = len(mask) - logger.info( + logger.debug( f"Applying persistent QC in {v}. Filtering {n_masked}/{n_samples} samples" ) # setting outliers to NaN diff --git a/src/pypromice/resources/variable_aliases_GC-Net.csv b/src/pypromice/resources/variable_aliases_GC-Net.csv index a0b4fe14..d84c0a71 100644 --- a/src/pypromice/resources/variable_aliases_GC-Net.csv +++ b/src/pypromice/resources/variable_aliases_GC-Net.csv @@ -47,9 +47,9 @@ t_i_11, tilt_x, tilt_y, rot, -gps_lat,latitude -gps_lon,longitude -gps_alt,elevation +lat,latitude +lon,longitude +alt,elevation gps_time, gps_geounit, gps_hdop, diff --git a/src/pypromice/resources/variables.csv b/src/pypromice/resources/variables.csv index ab8e4317..78354105 100644 --- a/src/pypromice/resources/variables.csv +++ b/src/pypromice/resources/variables.csv @@ -1,91 +1,91 @@ field,standard_name,long_name,units,coverage_content_type,coordinates,instantaneous_hourly,where_to_find,lo,hi,OOL,station_type,L0,L2,L3,max_decimals -time,time,Time,yyyy-mm-dd HH:MM:SS,physicalMeasurement,time lat lon alt,,,,,,all,1,1,1, -rec,record,Record,-,referenceInformation,time lat lon alt,,L0 or L2,,,,all,1,1,0,0 -p_u,air_pressure,Air pressure (upper boom),hPa,physicalMeasurement,time lat lon alt,FALSE,,650,1100,z_pt z_pt_cor dshf_u dlhf_u qh_u,all,1,1,1,4 -p_l,air_pressure,Air pressure (lower boom),hPa,physicalMeasurement,time lat lon alt,FALSE,,650,1100,dshf_l dlhf_l qh_l,two-boom,1,1,1,4 -t_u,air_temperature,Air temperature (upper boom),degrees_C,physicalMeasurement,time lat lon alt,FALSE,,-80,40,rh_u_cor cc dsr_cor usr_cor z_boom z_stake dshf_u dlhf_u qh_u,all,1,1,1,4 -t_l,air_temperature,Air temperature (lower boom),degrees_C,physicalMeasurement,time lat lon alt,FALSE,,-80,40,rh_l_cor z_boom_l dshf_l dlhf_l qh_l,two-boom,1,1,1,4 -rh_u,relative_humidity,Relative humidity (upper boom),%,physicalMeasurement,time lat lon alt,FALSE,,0,100,rh_u_cor,all,1,1,1,4 -rh_u_cor,relative_humidity_corrected,Relative humidity (upper boom) - corrected,%,modelResult,time lat lon alt,FALSE,L2 or later,0,150,dshf_u dlhf_u qh_u,all,0,1,1,4 -qh_u,specific_humidity,Specific humidity (upper boom),kg/kg,modelResult,time lat lon alt,FALSE,L2 or later,0,100,,all,0,1,1,4 -rh_l,relative_humidity,Relative humidity (lower boom),%,physicalMeasurement,time lat lon alt,FALSE,,0,100,rh_l_cor,two-boom,1,1,1,4 -rh_l_cor,relative_humidity_corrected,Relative humidity (lower boom) - corrected,%,modelResult,time lat lon alt,FALSE,L2 or later,0,150,dshf_l dlhf_l qh_l,two-boom,0,1,1,4 -qh_l,specific_humidity,Specific humidity (lower boom),kg/kg,modelResult,time lat lon alt,FALSE,L2 or later,0,100,,two-boom,0,1,1,4 -wspd_u,wind_speed,Wind speed (upper boom),m s-1,physicalMeasurement,time lat lon alt,FALSE,,0,100,"wdir_u wspd_x_u wspd_y_u dshf_u dlhf_u qh_u, precip_u",all,1,1,1,4 -wspd_l,wind_speed,Wind speed (lower boom),m s-1,physicalMeasurement,time lat lon alt,FALSE,,0,100,"wdir_l wspd_x_l wspd_y_l dshf_l dlhf_l qh_l , precip_l",two-boom,1,1,1,4 -wdir_u,wind_from_direction,Wind from direction (upper boom),degrees,physicalMeasurement,time lat lon alt,FALSE,,1,360,wspd_x_u wspd_y_u,all,1,1,1,4 -wdir_std_u,wind_from_direction_standard_deviation,Wind from direction (standard deviation),degrees,qualityInformation,time lat lon alt,FALSE,L0 or L2,,,,one-boom,1,1,0,4 -wdir_l,wind_from_direction,Wind from direction (lower boom),degrees,physicalMeasurement,time lat lon alt,FALSE,,1,360,wspd_x_l wspd_y_l,two-boom,1,1,1,4 -wspd_x_u,wind_speed_from_x_direction,Wind speed from x direction (upper boom),m s-1,modelResult,time lat lon alt,FALSE,L0 or L2,-100,100,wdir_u wspd_u,all,0,1,1,4 -wspd_y_u,wind_speed_from_y_direction,Wind speed from y direction (upper boom),m s-1,modelResult,time lat lon alt,FALSE,L0 or L2,-100,100,wdir_u wspd_u,all,0,1,1,4 -wspd_x_l,wind_speed_from_x_direction,Wind speed from x direction (lower boom),m s-1,modelResult,time lat lon alt,FALSE,L0 or L2,-100,100,wdir_l wspd_l,two-boom,0,1,1,4 -wspd_y_l,wind_speed_from_y_direction,Wind speed from y direction (lower boom),m s-1,modelResult,time lat lon alt,FALSE,L0 or L2,-100,100,wdir_l wspd_l,two-boom,0,1,1,4 -dsr,surface_downwelling_shortwave_flux,Downwelling shortwave radiation,W m-2,physicalMeasurement,time lat lon alt,FALSE,,-10,1500,albedo dsr_cor usr_cor,all,1,1,1,4 -dsr_cor,surface_downwelling_shortwave_flux_corrected,Downwelling shortwave radiation - corrected,W m-2,modelResult,time lat lon alt,FALSE,L2 or later,,,,all,0,1,1,4 -usr,surface_upwelling_shortwave_flux,Upwelling shortwave radiation,W m-2,physicalMeasurement,time lat lon alt,FALSE,,-10,1000,albedo dsr_cor usr_cor,all,1,1,1,4 -usr_cor,surface_upwelling_shortwave_flux_corrected,Upwelling shortwave radiation - corrected,W m-2,modelResult,time lat lon alt,FALSE,L2 or later,0,1000,,all,0,1,1,4 -albedo,surface_albedo,Albedo,-,modelResult,time lat lon alt,FALSE,L2 or later,,,,all,0,1,1,4 -dlr,surface_downwelling_longwave_flux,Downwelling longwave radiation,W m-2,physicalMeasurement,time lat lon alt,FALSE,,50,500,albedo dsr_cor usr_cor cc t_surf,all,1,1,1,4 -ulr,surface_upwelling_longwave_flux,Upwelling longwave radiation,W m-2,physicalMeasurement,time lat lon alt,FALSE,,50,500,t_surf,all,1,1,1,4 -cc,cloud_area_fraction,Cloud cover,%,modelResult,time lat lon alt,FALSE,L2 or later,,,,all,0,1,1,4 -t_surf,surface_temperature,Surface temperature,C,modelResult,time lat lon alt,FALSE,L2 or later,-80,40,dshf_u dlhf_u qh_u,all,0,1,1,4 -dlhf_u,surface_downward_latent_heat_flux,Latent heat flux (upper boom),W m-2,modelResult,time lat lon alt,FALSE,L3 or later,,,,all,0,0,1,4 -dlhf_l,surface_downward_latent_heat_flux,Latent heat flux (lower boom),W m-2,modelResult,time lat lon alt,FALSE,L3 or later,,,,two-boom,0,0,1,4 -dshf_u,surface_downward_sensible_heat_flux,Sensible heat flux (upper boom),W m-2,modelResult,time lat lon alt,FALSE,L3 or later,,,,all,0,0,1,4 -dshf_l,surface_downward_sensible_heat_flux,Sensible heat flux (lower boom),W m-2,modelResult,time lat lon alt,FALSE,L3 or later,,,,two-boom,0,0,1,4 -z_boom_u,distance_to_surface_from_boom,Upper boom height,m,physicalMeasurement,time lat lon alt,TRUE,,0.3,10,dshf_u dlhf_u qh_u,all,1,1,1,4 -z_boom_q_u,distance_to_surface_from_boom_quality,Upper boom height (quality),-,qualityInformation,time lat lon alt,TRUE,L0 or L2,,,,all,1,1,0,4 -z_boom_l,distance_to_surface_from_boom,Lower boom height,m,physicalMeasurement,time lat lon alt,TRUE,,0.3,5,dshf_l dlhf_l qh_l,two-boom,1,1,1,4 -z_boom_q_l,distance_to_surface_from_boom_quality,Lower boom height (quality),-,qualityInformation,time lat lon alt,TRUE,L0 or L2,,,,two-boom,1,1,0,4 -z_stake,distance_to_surface_from_stake_assembly,Stake height,m,physicalMeasurement,time lat lon alt,TRUE,,0.3,8,,one-boom,1,1,1,4 -z_stake_q,distance_to_surface_from_stake_assembly_quality,Stake height (quality),-,qualityInformation,time lat lon alt,TRUE,L0 or L2,,,,one-boom,1,1,0,4 -z_pt,depth_of_pressure_transducer_in_ice,Depth of pressure transducer in ice,m,physicalMeasurement,time lat lon alt,FALSE,,0,30,z_pt_cor,one-boom,1,1,1,4 -z_pt_cor,depth_of_pressure_transducer_in_ice_corrected,Depth of pressure transducer in ice - corrected,m,modelResult,time lat lon alt,FALSE,L2 or later,0,30,,one-boom,0,1,1,4 -precip_u,precipitation,Precipitation (upper boom) (cumulative solid & liquid),mm,physicalMeasurement,time lat lon alt,TRUE,,0,,precip_u_cor precip_u_rate,all,1,1,1,4 -precip_u_cor,precipitation_corrected,Precipitation (upper boom) (cumulative solid & liquid) – corrected,mm,modelResult,time lat lon alt,TRUE,L2 or later,0,,,all,0,1,1,4 -precip_u_rate,precipitation_rate,Precipitation rate (upper boom) (cumulative solid & liquid) – corrected,mm,modelResult,time lat lon alt,TRUE,L2 or later,0,,,all,0,1,1,4 -precip_l,precipitation,Precipitation (lower boom) (cumulative solid & liquid),mm,physicalMeasurement,time lat lon alt,TRUE,,0,,precip_l_cor precip_l_rate,two-boom,1,1,1,4 -precip_l_cor,precipitation_corrected,Precipitation (lower boom) (cumulative solid & liquid) – corrected,mm,modelResult,time lat lon alt,TRUE,L2 or later,0,,,two-boom,0,1,1,4 -precip_l_rate,precipitation_rate,Precipitation rate (lower boom) (cumulative solid & liquid) – corrected,mm,modelResult,time lat lon alt,TRUE,L2 or later,0,,,two-boom,0,1,1,4 -t_i_1,ice_temperature_at_t1,Ice temperature at sensor 1,degrees_C,physicalMeasurement,time lat lon alt,FALSE,,-80,1,,all,1,1,1,4 -t_i_2,ice_temperature_at_t2,Ice temperature at sensor 2,degrees_C,physicalMeasurement,time lat lon alt,FALSE,,-80,1,,all,1,1,1,4 -t_i_3,ice_temperature_at_t3,Ice temperature at sensor 3,degrees_C,physicalMeasurement,time lat lon alt,FALSE,,-80,1,,all,1,1,1,4 -t_i_4,ice_temperature_at_t4,Ice temperature at sensor 4,degrees_C,physicalMeasurement,time lat lon alt,FALSE,,-80,1,,all,1,1,1,4 -t_i_5,ice_temperature_at_t5,Ice temperature at sensor 5,degrees_C,physicalMeasurement,time lat lon alt,FALSE,,-80,1,,all,1,1,1,4 -t_i_6,ice_temperature_at_t6,Ice temperature at sensor 6,degrees_C,physicalMeasurement,time lat lon alt,FALSE,,-80,1,,all,1,1,1,4 -t_i_7,ice_temperature_at_t7,Ice temperature at sensor 7,degrees_C,physicalMeasurement,time lat lon alt,FALSE,,-80,1,,all,1,1,1,4 -t_i_8,ice_temperature_at_t8,Ice temperature at sensor 8,degrees_C,physicalMeasurement,time lat lon alt,FALSE,,-80,1,,all,1,1,1,4 -t_i_9,ice_temperature_at_t9,Ice temperature at sensor 9,degrees_C,physicalMeasurement,time lat lon alt,FALSE,,-80,1,,two-boom,1,1,1,4 -t_i_10,ice_temperature_at_t10,Ice temperature at sensor 10,degrees_C,physicalMeasurement,time lat lon alt,FALSE,,-80,1,,two-boom,1,1,1,4 -t_i_11,ice_temperature_at_t11,Ice temperature at sensor 11,degrees_C,physicalMeasurement,time lat lon alt,FALSE,,-80,1,,two-boom,1,1,1,4 -tilt_x,platform_view_angle_x,Tilt to east,degrees,physicalMeasurement,time lat lon alt,FALSE,,-30,30,dsr_cor usr_cor albedo,all,1,1,1,4 -tilt_y,platform_view_angle_y,Tilt to north,degrees,physicalMeasurement,time lat lon alt,FALSE,,-30,30,dsr_cor usr_cor albedo,all,1,1,1,4 -rot,platform_azimuth_angle,Station rotation from true North,degrees,physicalMeasurement,time lat lon alt,FALSE,,0,360,,all,1,1,1,2 -gps_lat,gps_latitude,Latitude,degrees_north,coordinate,time lat lon alt,TRUE,,50,83,,all,1,1,1,6 -gps_lon,gps_longitude,Longitude,degrees_east,coordinate,time lat lon alt,TRUE,,5,70,,all,1,1,1,6 -gps_alt,gps_altitude,Altitude,m,coordinate,time lat lon alt,TRUE,,0,3000,,all,1,1,1,2 -gps_time,gps_time,GPS time,s,physicalMeasurement,time lat lon alt,TRUE,L0 or L2,0,240000,,all,1,1,0, -gps_geoid,gps_geoid_separation,Height of EGM96 geoid over WGS84 ellipsoid,m,physicalMeasurement,time lat lon alt,TRUE,L0 or L2,,,,one-boom,1,1,0, -gps_geounit,gps_geounit,GeoUnit,-,qualityInformation,time lat lon alt,TRUE,L0 or L2,,,,all,1,1,0, -gps_hdop,gps_hdop,GPS horizontal dillution of precision (HDOP),m,qualityInformation,time lat lon alt,TRUE,L0 or L2,,,,all,1,1,0,2 -gps_numsat,gps_numsat,GPS number of satellites,-,qualityInformation,time lat lon alt,TRUE,L0 or L2,,,,,1,1,0,0 -gps_q,gps_q,Quality,-,qualityInformation,time lat lon alt,TRUE,L0 or L2,,,,,1,1,0, -lat,gps_mean_latitude,GPS mean latitude (from all time-series),degrees,modelResult,time lat lon alt,TRUE,,,,,all,1,1,1,6 -lon,gps_mean_longitude,GPS mean longitude (from all time-series),degrees,modelResult,time lat lon alt,TRUE,,,,,all,1,1,1,6 -alt,gps_mean_altitude,GPS mean altitude (from all time-series),degrees,modelResult,time lat lon alt,TRUE,,,,,all,1,1,1,6 -batt_v,battery_voltage,Battery voltage,V,physicalMeasurement,time lat lon alt,TRUE,,0,30,,all,1,1,1,2 -batt_v_ini,,,-,physicalMeasurement,time lat lon alt,TRUE,L0 or L2,0,30,,,1,1,0,2 -batt_v_ss,battery_voltage_at_sample_start,Battery voltage (sample start),V,physicalMeasurement,time lat lon alt,TRUE,L0 or L2,0,30,,,1,1,0,2 -fan_dc_u,fan_current,Fan current (upper boom),mA,physicalMeasurement,time lat lon alt,TRUE,L0 or L2,0,200,,all,1,1,0,2 -fan_dc_l,fan_current,Fan current (lower boom),mA,physicalMeasurement,time lat lon alt,TRUE,,0,200,,two-boom,1,1,0,2 -freq_vw,frequency_of_precipitation_wire_vibration,Frequency of vibrating wire in precipitation gauge,Hz,physicalMeasurement,time lat lon alt,TRUE,L0 or L2,0,10000,precip_u,,1,1,0, -t_log,temperature_of_logger,Logger temperature,degrees_C,physicalMeasurement,time lat lon alt,TRUE,,-80,40,,one-boom,1,1,0,4 -t_rad,temperature_of_radiation_sensor,Radiation sensor temperature,degrees_C,physicalMeasurement,time lat lon alt,FALSE,,-80,40,t_surf dlr ulr,all,1,1,1,4 -p_i,air_pressure,Air pressure (instantaneous) minus 1000,hPa,physicalMeasurement,time lat lon alt,TRUE,,-350,100,,all,1,1,1,4 -t_i,air_temperature,Air temperature (instantaneous),degrees_C,physicalMeasurement,time lat lon alt,TRUE,,-80,40,,all,1,1,1,4 -rh_i,relative_humidity,Relative humidity (instantaneous),%,physicalMeasurement,time lat lon alt,TRUE,,0,150,rh_i_cor,all,1,1,1,4 -rh_i_cor,relative_humidity_corrected,Relative humidity (instantaneous) – corrected,%,modelResult,time lat lon alt,TRUE,L2 or later,0,100,,all,0,1,1,4 -wspd_i,wind_speed,Wind speed (instantaneous),m s-1,physicalMeasurement,time lat lon alt,TRUE,,0,100,wdir_i wspd_x_i wspd_y_i,all,1,1,1,4 -wdir_i,wind_from_direction,Wind from direction (instantaneous),degrees,physicalMeasurement,time lat lon alt,TRUE,,1,360,wspd_x_i wspd_y_i,all,1,1,1,4 -wspd_x_i,wind_speed_from_x_direction,Wind speed from x direction (instantaneous),m s-1,modelResult,time lat lon alt,TRUE,L2 or later,-100,100,wdir_i wspd_i,all,0,1,1,4 -wspd_y_i,wind_speed_from_y_direction,Wind speed from y direction (instantaneous),m s-1,modelResult,time lat lon alt,TRUE,L2 or later,-100,100,wdir_i wspd_i,all,0,1,1,4 \ No newline at end of file +time,time,Time,yyyy-mm-dd HH:MM:SS,physicalMeasurement,time,,,,,,all,1,1,1, +rec,record,Record,-,referenceInformation,time,,L0 or L2,,,,all,1,1,0,0 +p_u,air_pressure,Air pressure (upper boom),hPa,physicalMeasurement,time,FALSE,,650,1100,z_pt z_pt_cor dshf_u dlhf_u qh_u,all,1,1,1,4 +p_l,air_pressure,Air pressure (lower boom),hPa,physicalMeasurement,time,FALSE,,650,1100,dshf_l dlhf_l qh_l,two-boom,1,1,1,4 +t_u,air_temperature,Air temperature (upper boom),degrees_C,physicalMeasurement,time,FALSE,,-80,40,rh_u_cor cc dsr_cor usr_cor z_boom z_stake dshf_u dlhf_u qh_u,all,1,1,1,4 +t_l,air_temperature,Air temperature (lower boom),degrees_C,physicalMeasurement,time,FALSE,,-80,40,rh_l_cor z_boom_l dshf_l dlhf_l qh_l,two-boom,1,1,1,4 +rh_u,relative_humidity,Relative humidity (upper boom),%,physicalMeasurement,time,FALSE,,0,100,rh_u_cor,all,1,1,1,4 +rh_u_cor,relative_humidity_corrected,Relative humidity (upper boom) - corrected,%,modelResult,time,FALSE,L2 or later,0,150,dshf_u dlhf_u qh_u,all,0,1,1,4 +qh_u,specific_humidity,Specific humidity (upper boom),kg/kg,modelResult,time,FALSE,L2 or later,0,100,,all,0,1,1,4 +rh_l,relative_humidity,Relative humidity (lower boom),%,physicalMeasurement,time,FALSE,,0,100,rh_l_cor,two-boom,1,1,1,4 +rh_l_cor,relative_humidity_corrected,Relative humidity (lower boom) - corrected,%,modelResult,time,FALSE,L2 or later,0,150,dshf_l dlhf_l qh_l,two-boom,0,1,1,4 +qh_l,specific_humidity,Specific humidity (lower boom),kg/kg,modelResult,time,FALSE,L2 or later,0,100,,two-boom,0,1,1,4 +wspd_u,wind_speed,Wind speed (upper boom),m s-1,physicalMeasurement,time,FALSE,,0,100,"wdir_u wspd_x_u wspd_y_u dshf_u dlhf_u qh_u, precip_u",all,1,1,1,4 +wspd_l,wind_speed,Wind speed (lower boom),m s-1,physicalMeasurement,time,FALSE,,0,100,"wdir_l wspd_x_l wspd_y_l dshf_l dlhf_l qh_l , precip_l",two-boom,1,1,1,4 +wdir_u,wind_from_direction,Wind from direction (upper boom),degrees,physicalMeasurement,time,FALSE,,1,360,wspd_x_u wspd_y_u,all,1,1,1,4 +wdir_std_u,wind_from_direction_standard_deviation,Wind from direction (standard deviation),degrees,qualityInformation,time,FALSE,L0 or L2,,,,one-boom,1,1,0,4 +wdir_l,wind_from_direction,Wind from direction (lower boom),degrees,physicalMeasurement,time,FALSE,,1,360,wspd_x_l wspd_y_l,two-boom,1,1,1,4 +wspd_x_u,wind_speed_from_x_direction,Wind speed from x direction (upper boom),m s-1,modelResult,time,FALSE,L0 or L2,-100,100,wdir_u wspd_u,all,0,1,1,4 +wspd_y_u,wind_speed_from_y_direction,Wind speed from y direction (upper boom),m s-1,modelResult,time,FALSE,L0 or L2,-100,100,wdir_u wspd_u,all,0,1,1,4 +wspd_x_l,wind_speed_from_x_direction,Wind speed from x direction (lower boom),m s-1,modelResult,time,FALSE,L0 or L2,-100,100,wdir_l wspd_l,two-boom,0,1,1,4 +wspd_y_l,wind_speed_from_y_direction,Wind speed from y direction (lower boom),m s-1,modelResult,time,FALSE,L0 or L2,-100,100,wdir_l wspd_l,two-boom,0,1,1,4 +dsr,surface_downwelling_shortwave_flux,Downwelling shortwave radiation,W m-2,physicalMeasurement,time,FALSE,,-10,1500,albedo dsr_cor usr_cor,all,1,1,1,4 +dsr_cor,surface_downwelling_shortwave_flux_corrected,Downwelling shortwave radiation - corrected,W m-2,modelResult,time,FALSE,L2 or later,,,,all,0,1,1,4 +usr,surface_upwelling_shortwave_flux,Upwelling shortwave radiation,W m-2,physicalMeasurement,time,FALSE,,-10,1000,albedo dsr_cor usr_cor,all,1,1,1,4 +usr_cor,surface_upwelling_shortwave_flux_corrected,Upwelling shortwave radiation - corrected,W m-2,modelResult,time,FALSE,L2 or later,0,1000,,all,0,1,1,4 +albedo,surface_albedo,Albedo,-,modelResult,time,FALSE,L2 or later,,,,all,0,1,1,4 +dlr,surface_downwelling_longwave_flux,Downwelling longwave radiation,W m-2,physicalMeasurement,time,FALSE,,50,500,albedo dsr_cor usr_cor cc t_surf,all,1,1,1,4 +ulr,surface_upwelling_longwave_flux,Upwelling longwave radiation,W m-2,physicalMeasurement,time,FALSE,,50,500,t_surf,all,1,1,1,4 +cc,cloud_area_fraction,Cloud cover,%,modelResult,time,FALSE,L2 or later,,,,all,0,1,1,4 +t_surf,surface_temperature,Surface temperature,C,modelResult,time,FALSE,L2 or later,-80,40,dshf_u dlhf_u qh_u,all,0,1,1,4 +dlhf_u,surface_downward_latent_heat_flux,Latent heat flux (upper boom),W m-2,modelResult,time,FALSE,L3 or later,,,,all,0,0,1,4 +dlhf_l,surface_downward_latent_heat_flux,Latent heat flux (lower boom),W m-2,modelResult,time,FALSE,L3 or later,,,,two-boom,0,0,1,4 +dshf_u,surface_downward_sensible_heat_flux,Sensible heat flux (upper boom),W m-2,modelResult,time,FALSE,L3 or later,,,,all,0,0,1,4 +dshf_l,surface_downward_sensible_heat_flux,Sensible heat flux (lower boom),W m-2,modelResult,time,FALSE,L3 or later,,,,two-boom,0,0,1,4 +z_boom_u,distance_to_surface_from_boom,Upper boom height,m,physicalMeasurement,time,TRUE,,0.3,10,dshf_u dlhf_u qh_u,all,1,1,1,4 +z_boom_q_u,distance_to_surface_from_boom_quality,Upper boom height (quality),-,qualityInformation,time,TRUE,L0 or L2,,,,all,1,1,0,4 +z_boom_l,distance_to_surface_from_boom,Lower boom height,m,physicalMeasurement,time,TRUE,,0.3,5,dshf_l dlhf_l qh_l,two-boom,1,1,1,4 +z_boom_q_l,distance_to_surface_from_boom_quality,Lower boom height (quality),-,qualityInformation,time,TRUE,L0 or L2,,,,two-boom,1,1,0,4 +z_stake,distance_to_surface_from_stake_assembly,Stake height,m,physicalMeasurement,time,TRUE,,0.3,8,,one-boom,1,1,1,4 +z_stake_q,distance_to_surface_from_stake_assembly_quality,Stake height (quality),-,qualityInformation,time,TRUE,L0 or L2,,,,one-boom,1,1,0,4 +z_pt,depth_of_pressure_transducer_in_ice,Depth of pressure transducer in ice,m,physicalMeasurement,time,FALSE,,0,30,z_pt_cor,one-boom,1,1,1,4 +z_pt_cor,depth_of_pressure_transducer_in_ice_corrected,Depth of pressure transducer in ice - corrected,m,modelResult,time,FALSE,L2 or later,0,30,,one-boom,0,1,1,4 +precip_u,precipitation,Precipitation (upper boom) (cumulative solid & liquid),mm,physicalMeasurement,time,TRUE,,0,,precip_u_cor precip_u_rate,all,1,1,1,4 +precip_u_cor,precipitation_corrected,Precipitation (upper boom) (cumulative solid & liquid) – corrected,mm,modelResult,time,TRUE,L2 or later,0,,,all,0,1,1,4 +precip_u_rate,precipitation_rate,Precipitation rate (upper boom) (cumulative solid & liquid) – corrected,mm,modelResult,time,TRUE,L2 or later,0,,,all,0,1,1,4 +precip_l,precipitation,Precipitation (lower boom) (cumulative solid & liquid),mm,physicalMeasurement,time,TRUE,,0,,precip_l_cor precip_l_rate,two-boom,1,1,1,4 +precip_l_cor,precipitation_corrected,Precipitation (lower boom) (cumulative solid & liquid) – corrected,mm,modelResult,time,TRUE,L2 or later,0,,,two-boom,0,1,1,4 +precip_l_rate,precipitation_rate,Precipitation rate (lower boom) (cumulative solid & liquid) – corrected,mm,modelResult,time,TRUE,L2 or later,0,,,two-boom,0,1,1,4 +t_i_1,ice_temperature_at_t1,Ice temperature at sensor 1,degrees_C,physicalMeasurement,time,FALSE,,-80,1,,all,1,1,1,4 +t_i_2,ice_temperature_at_t2,Ice temperature at sensor 2,degrees_C,physicalMeasurement,time,FALSE,,-80,1,,all,1,1,1,4 +t_i_3,ice_temperature_at_t3,Ice temperature at sensor 3,degrees_C,physicalMeasurement,time,FALSE,,-80,1,,all,1,1,1,4 +t_i_4,ice_temperature_at_t4,Ice temperature at sensor 4,degrees_C,physicalMeasurement,time,FALSE,,-80,1,,all,1,1,1,4 +t_i_5,ice_temperature_at_t5,Ice temperature at sensor 5,degrees_C,physicalMeasurement,time,FALSE,,-80,1,,all,1,1,1,4 +t_i_6,ice_temperature_at_t6,Ice temperature at sensor 6,degrees_C,physicalMeasurement,time,FALSE,,-80,1,,all,1,1,1,4 +t_i_7,ice_temperature_at_t7,Ice temperature at sensor 7,degrees_C,physicalMeasurement,time,FALSE,,-80,1,,all,1,1,1,4 +t_i_8,ice_temperature_at_t8,Ice temperature at sensor 8,degrees_C,physicalMeasurement,time,FALSE,,-80,1,,all,1,1,1,4 +t_i_9,ice_temperature_at_t9,Ice temperature at sensor 9,degrees_C,physicalMeasurement,time,FALSE,,-80,1,,two-boom,1,1,1,4 +t_i_10,ice_temperature_at_t10,Ice temperature at sensor 10,degrees_C,physicalMeasurement,time,FALSE,,-80,1,,two-boom,1,1,1,4 +t_i_11,ice_temperature_at_t11,Ice temperature at sensor 11,degrees_C,physicalMeasurement,time,FALSE,,-80,1,,two-boom,1,1,1,4 +tilt_x,platform_view_angle_x,Tilt to east,degrees,physicalMeasurement,time,FALSE,,-30,30,dsr_cor usr_cor albedo,all,1,1,1,4 +tilt_y,platform_view_angle_y,Tilt to north,degrees,physicalMeasurement,time,FALSE,,-30,30,dsr_cor usr_cor albedo,all,1,1,1,4 +rot,platform_azimuth_angle,Station rotation from true North,degrees,physicalMeasurement,time,FALSE,,0,360,,all,1,1,1,2 +gps_lat,gps_latitude,Latitude,degrees_north,coordinate,time,TRUE,,50,83,,all,1,1,1,6 +gps_lon,gps_longitude,Longitude,degrees_east,coordinate,time,TRUE,,5,70,,all,1,1,1,6 +gps_alt,gps_altitude,Altitude,m,coordinate,time,TRUE,,0,3000,,all,1,1,1,2 +gps_time,gps_time,GPS time,s,physicalMeasurement,time,TRUE,L0 or L2,0,240000,,all,1,1,0, +gps_geoid,gps_geoid_separation,Height of EGM96 geoid over WGS84 ellipsoid,m,physicalMeasurement,time,TRUE,L0 or L2,,,,one-boom,1,1,0, +gps_geounit,gps_geounit,GeoUnit,-,qualityInformation,time,TRUE,L0 or L2,,,,all,1,1,0, +gps_hdop,gps_hdop,GPS horizontal dillution of precision (HDOP),m,qualityInformation,time,TRUE,L0 or L2,,,,all,1,1,0,2 +gps_numsat,gps_numsat,GPS number of satellites,-,qualityInformation,time,TRUE,L0 or L2,,,,,1,1,0,0 +gps_q,gps_q,Quality,-,qualityInformation,time,TRUE,L0 or L2,,,,,1,1,0, +lat,latitude_postprocessed,smoothed and interpolated latitude of station (best estimate),degrees_north,modelResult,time,TRUE,L3,,,,all,0,0,1,6 +lon,longitude_postprocessed,smoothed and interpolated longitude of station (best estimate),degrees_east,modelResult,time,TRUE,L3,,,,all,0,0,1,6 +alt,altitude_postprocessed,smoothed and interpolated altitude of station (best estimate),m,modelResult,time,TRUE,L3,,,,all,0,0,1,2 +batt_v,battery_voltage,Battery voltage,V,physicalMeasurement,time,TRUE,,0,30,,all,1,1,1,2 +batt_v_ini,,,-,physicalMeasurement,time,TRUE,L0 or L2,0,30,,,1,1,0,2 +batt_v_ss,battery_voltage_at_sample_start,Battery voltage (sample start),V,physicalMeasurement,time,TRUE,L0 or L2,0,30,,,1,1,0,2 +fan_dc_u,fan_current,Fan current (upper boom),mA,physicalMeasurement,time,TRUE,L0 or L2,0,200,,all,1,1,0,2 +fan_dc_l,fan_current,Fan current (lower boom),mA,physicalMeasurement,time,TRUE,,0,200,,two-boom,1,1,0,2 +freq_vw,frequency_of_precipitation_wire_vibration,Frequency of vibrating wire in precipitation gauge,Hz,physicalMeasurement,time,TRUE,L0 or L2,0,10000,precip_u,,1,1,0, +t_log,temperature_of_logger,Logger temperature,degrees_C,physicalMeasurement,time,TRUE,,-80,40,,one-boom,1,1,0,4 +t_rad,temperature_of_radiation_sensor,Radiation sensor temperature,degrees_C,physicalMeasurement,time,FALSE,,-80,40,t_surf dlr ulr,all,1,1,1,4 +p_i,air_pressure,Air pressure (instantaneous) minus 1000,hPa,physicalMeasurement,time,TRUE,,-350,100,,all,1,1,1,4 +t_i,air_temperature,Air temperature (instantaneous),degrees_C,physicalMeasurement,time,TRUE,,-80,40,,all,1,1,1,4 +rh_i,relative_humidity,Relative humidity (instantaneous),%,physicalMeasurement,time,TRUE,,0,150,rh_i_cor,all,1,1,1,4 +rh_i_cor,relative_humidity_corrected,Relative humidity (instantaneous) – corrected,%,modelResult,time,TRUE,L2 or later,0,100,,all,0,1,1,4 +wspd_i,wind_speed,Wind speed (instantaneous),m s-1,physicalMeasurement,time,TRUE,,0,100,wdir_i wspd_x_i wspd_y_i,all,1,1,1,4 +wdir_i,wind_from_direction,Wind from direction (instantaneous),degrees,physicalMeasurement,time,TRUE,,1,360,wspd_x_i wspd_y_i,all,1,1,1,4 +wspd_x_i,wind_speed_from_x_direction,Wind speed from x direction (instantaneous),m s-1,modelResult,time,TRUE,L2 or later,-100,100,wdir_i wspd_i,all,0,1,1,4 +wspd_y_i,wind_speed_from_y_direction,Wind speed from y direction (instantaneous),m s-1,modelResult,time,TRUE,L2 or later,-100,100,wdir_i wspd_i,all,0,1,1,4 \ No newline at end of file