pypromice 1.5.2__py3-none-any.whl → 1.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pypromice might be problematic. Click here for more details.

Files changed (65) hide show
  1. pypromice/__init__.py +2 -0
  2. pypromice/{qc → core/qc}/percentiles/compute_thresholds.py +2 -2
  3. pypromice/{qc → core/qc}/persistence.py +22 -29
  4. pypromice/{process → core/qc}/value_clipping.py +3 -3
  5. pypromice/core/variables/__init__.py +1 -0
  6. pypromice/core/variables/air_temperature.py +64 -0
  7. pypromice/core/variables/gps.py +221 -0
  8. pypromice/core/variables/humidity.py +111 -0
  9. pypromice/core/variables/precipitation.py +108 -0
  10. pypromice/core/variables/pressure_transducer_depth.py +79 -0
  11. pypromice/core/variables/radiation.py +422 -0
  12. pypromice/core/variables/station_boom_height.py +49 -0
  13. pypromice/core/variables/station_pose.py +375 -0
  14. pypromice/core/variables/wind.py +66 -0
  15. pypromice/io/bufr/__init__.py +0 -0
  16. pypromice/{postprocess → io/bufr}/bufr_to_csv.py +1 -1
  17. pypromice/{postprocess → io/bufr}/create_bufr_files.py +2 -2
  18. pypromice/{postprocess → io/bufr}/get_bufr.py +6 -6
  19. pypromice/{postprocess → io/bufr}/real_time_utilities.py +3 -3
  20. pypromice/io/ingest/__init__.py +0 -0
  21. pypromice/{utilities → io/ingest}/git.py +1 -3
  22. pypromice/io/ingest/l0.py +294 -0
  23. pypromice/io/ingest/l0_repository.py +103 -0
  24. pypromice/io/ingest/toa5.py +87 -0
  25. pypromice/{process → io}/write.py +1 -1
  26. pypromice/pipeline/L0toL1.py +291 -0
  27. pypromice/pipeline/L1toL2.py +233 -0
  28. pypromice/{process → pipeline}/L2toL3.py +102 -120
  29. pypromice/pipeline/__init__.py +4 -0
  30. pypromice/{process → pipeline}/aws.py +10 -82
  31. pypromice/{process → pipeline}/get_l2.py +2 -2
  32. pypromice/{process → pipeline}/get_l2tol3.py +19 -22
  33. pypromice/{process → pipeline}/join_l2.py +31 -32
  34. pypromice/{process → pipeline}/join_l3.py +16 -14
  35. pypromice/{process → pipeline}/resample.py +59 -46
  36. pypromice/{process → pipeline}/utilities.py +0 -22
  37. pypromice/resources/file_attributes.csv +4 -4
  38. pypromice/resources/variables.csv +27 -24
  39. {pypromice-1.5.2.dist-info → pypromice-1.6.0.dist-info}/METADATA +1 -2
  40. pypromice-1.6.0.dist-info/RECORD +64 -0
  41. {pypromice-1.5.2.dist-info → pypromice-1.6.0.dist-info}/WHEEL +1 -1
  42. pypromice-1.6.0.dist-info/entry_points.txt +12 -0
  43. pypromice/get/__init__.py +0 -1
  44. pypromice/get/get.py +0 -211
  45. pypromice/get/get_promice_data.py +0 -56
  46. pypromice/process/L0toL1.py +0 -536
  47. pypromice/process/L1toL2.py +0 -839
  48. pypromice/process/__init__.py +0 -4
  49. pypromice/process/load.py +0 -161
  50. pypromice-1.5.2.dist-info/RECORD +0 -53
  51. pypromice-1.5.2.dist-info/entry_points.txt +0 -13
  52. /pypromice/{postprocess → core}/__init__.py +0 -0
  53. /pypromice/{utilities → core}/dependency_graph.py +0 -0
  54. /pypromice/{qc → core/qc}/__init__.py +0 -0
  55. /pypromice/{qc → core/qc}/github_data_issues.py +0 -0
  56. /pypromice/{qc → core/qc}/percentiles/__init__.py +0 -0
  57. /pypromice/{qc → core/qc}/percentiles/outlier_detector.py +0 -0
  58. /pypromice/{qc → core/qc}/percentiles/thresholds.csv +0 -0
  59. /pypromice/{utilities → io}/__init__.py +0 -0
  60. /pypromice/{postprocess → io/bufr}/bufr_utilities.py +0 -0
  61. /pypromice/{postprocess → io/bufr}/positions_seed.csv +0 -0
  62. /pypromice/{station_configuration.py → io/bufr/station_configuration.py} +0 -0
  63. /pypromice/{postprocess → io}/make_metadata_csv.py +0 -0
  64. {pypromice-1.5.2.dist-info → pypromice-1.6.0.dist-info}/licenses/LICENSE.txt +0 -0
  65. {pypromice-1.5.2.dist-info → pypromice-1.6.0.dist-info}/top_level.txt +0 -0
@@ -1,536 +0,0 @@
1
- #!/usr/bin/env python
2
- """
3
- AWS Level 0 (L0) to Level 1 (L1) data processing
4
- """
5
- import numpy as np
6
- import pandas as pd
7
- import xarray as xr
8
- import re, logging
9
- from pypromice.process.value_clipping import clip_values
10
- logger = logging.getLogger(__name__)
11
-
12
-
13
- def toL1(L0, vars_df, T_0=273.15, tilt_threshold=-100):
14
- '''Process one Level 0 (L0) product to Level 1
15
-
16
- Parameters
17
- ----------
18
- L0 : xarray.Dataset
19
- Level 0 dataset
20
- vars_df : pd.DataFrame
21
- Metadata dataframe
22
- T_0 : int
23
- Air temperature for sonic ranger adjustment
24
- tilt_threshold : int
25
- Tilt-o-meter threshold for valid measurements
26
-
27
- Returns
28
- -------
29
- ds : xarray.Dataset
30
- Level 1 dataset
31
- '''
32
- assert(type(L0) == xr.Dataset)
33
- ds = L0
34
- ds.attrs['level'] = 'L1'
35
-
36
- for l in list(ds.keys()):
37
- if l not in ['time', 'msg_i', 'gps_lat', 'gps_lon', 'gps_alt', 'gps_time']:
38
- ds[l] = _reformatArray(ds[l])
39
-
40
- # ds['time_orig'] = ds['time'] # Not used
41
-
42
- # The following drops duplicate datetime indices. Needs to run before _addTimeShift!
43
- # We can optionally also drop duplicates within _addTimeShift using pandas duplicated,
44
- # but retaining the following code instead to preserve previous methods. PJW
45
- _, index = np.unique(ds['time'], return_index=True)
46
- ds = ds.isel(time=index)
47
-
48
- # If we do not want to shift hourly average values back -1 hr, then comment the following line.
49
- ds = addTimeShift(ds, vars_df)
50
-
51
- if hasattr(ds, 'dsr_eng_coef'):
52
- ds['dsr'] = (ds['dsr'] * 10) / ds.attrs['dsr_eng_coef'] # Convert radiation from engineering to physical units
53
- if hasattr(ds, 'usr_eng_coef'): # TODO add metadata to indicate whether radiometer values are corrected with calibration values or not
54
- ds['usr'] = (ds['usr'] * 10) / ds.attrs['usr_eng_coef']
55
- if hasattr(ds, 'dlr_eng_coef'):
56
- ds['dlr'] = ((ds['dlr'] * 10) / ds.attrs['dlr_eng_coef']) + 5.67E-8*(ds['t_rad'] + T_0)**4
57
- if hasattr(ds, 'ulr_eng_coef'):
58
- ds['ulr'] = ((ds['ulr'] * 10) / ds.attrs['ulr_eng_coef']) + 5.67E-8*(ds['t_rad'] + T_0)**4
59
-
60
- ds['z_boom_u'] = _reformatArray(ds['z_boom_u']) # Reformat boom height
61
-
62
- ds['t_u_interp'] = interpTemp(ds['t_u'], vars_df)
63
- ds['z_boom_u'] = ds['z_boom_u'] * ((ds['t_u_interp'] + T_0)/T_0)**0.5 # Adjust sonic ranger readings for sensitivity to air temperature
64
-
65
- if ds['gps_lat'].dtype.kind == 'O': # Decode and reformat GPS information
66
- if 'NH' in ds['gps_lat'].dropna(dim='time').values[1]:
67
- ds = decodeGPS(ds, ['gps_lat','gps_lon','gps_time'])
68
- elif 'L' in ds['gps_lat'].dropna(dim='time').values[1]:
69
- logger.info('Found L in GPS string')
70
- ds = decodeGPS(ds, ['gps_lat','gps_lon','gps_time'])
71
- for l in ['gps_lat', 'gps_lon']:
72
- ds[l] = ds[l]/100000
73
- else:
74
- try:
75
- ds = decodeGPS(ds, ['gps_lat','gps_lon','gps_time']) # TODO this is a work around specifically for L0 RAW processing for THU_U. Find a way to make this slicker
76
-
77
- except:
78
- print('Invalid GPS type {ds["gps_lat"].dtype} for decoding')
79
-
80
- for l in ['gps_lat', 'gps_lon', 'gps_alt','gps_time']:
81
- ds[l] = _reformatArray(ds[l])
82
-
83
- if hasattr(ds, 'latitude') and hasattr(ds, 'longitude'):
84
- ds['gps_lat'] = reformatGPS(ds['gps_lat'], ds.attrs['latitude'])
85
- ds['gps_lon'] = reformatGPS(ds['gps_lon'], ds.attrs['longitude'])
86
-
87
- if hasattr(ds, 'logger_type'): # Convert tilt voltage to degrees
88
- if ds.attrs['logger_type'].upper() == 'CR1000':
89
- ds['tilt_x'] = getTiltDegrees(ds['tilt_x'], tilt_threshold)
90
- ds['tilt_y'] = getTiltDegrees(ds['tilt_y'], tilt_threshold)
91
-
92
- if hasattr(ds, 'tilt_y_factor'): # Apply tilt factor (e.g. -1 will invert tilt angle)
93
- ds['tilt_y'] = ds['tilt_y']*ds.attrs['tilt_y_factor']
94
-
95
- # Smooth everything
96
- # Note that this should be OK for CR1000 tx (data only every 6 hrs),
97
- # since we interpolate above in _getTiltDegrees. PJW
98
- ds['tilt_x'] = smoothTilt(ds['tilt_x'], 7) # Smooth tilt
99
- ds['tilt_y'] = smoothTilt(ds['tilt_y'], 7)
100
-
101
- if hasattr(ds, 'bedrock'): # Fix tilt to zero if station is on bedrock
102
- if ds.attrs['bedrock']==True or ds.attrs['bedrock'].lower() in 'true':
103
- ds.attrs['bedrock'] = True # ensures all AWS objects have a 'bedrock' attribute
104
- ds['tilt_x'] = (('time'), np.arange(ds['time'].size)*0)
105
- ds['tilt_y'] = (('time'), np.arange(ds['time'].size)*0)
106
- else:
107
- ds.attrs['bedrock'] = False # ensures all AWS objects have a 'bedrock' attribute
108
- else:
109
- ds.attrs['bedrock'] = False # ensures all AWS objects have a 'bedrock' attribute
110
-
111
- if ds.attrs['number_of_booms']==1: # 1-boom processing
112
- if ~ds['z_pt'].isnull().all(): # Calculate pressure transducer fluid density
113
- if hasattr(ds, 'pt_z_offset'): # Apply SR50 stake offset
114
- ds['z_pt'] = ds['z_pt'] + int(ds.attrs['pt_z_offset'])
115
- ds['z_pt_cor'],ds['z_pt']=getPressDepth(ds['z_pt'], ds['p_u'],
116
- ds.attrs['pt_antifreeze'],
117
- ds.attrs['pt_z_factor'],
118
- ds.attrs['pt_z_coef'],
119
- ds.attrs['pt_z_p_coef'])
120
- ds['z_stake'] = _reformatArray(ds['z_stake']) # Reformat boom height
121
- ds['z_stake'] = ds['z_stake'] * ((ds['t_u'] + T_0)/T_0)**0.5 # Adjust sonic ranger readings for sensitivity to air temperature
122
-
123
- elif ds.attrs['number_of_booms']==2: # 2-boom processing
124
- ds['z_boom_l'] = _reformatArray(ds['z_boom_l']) # Reformat boom height
125
- ds['t_l_interp'] = interpTemp(ds['t_l'], vars_df)
126
- ds['z_boom_l'] = ds['z_boom_l'] * ((ds['t_l_interp']+ T_0)/T_0)**0.5 # Adjust sonic ranger readings for sensitivity to air temperature
127
-
128
- ds = clip_values(ds, vars_df)
129
- for key in ['hygroclip_t_offset', 'dsr_eng_coef', 'usr_eng_coef',
130
- 'dlr_eng_coef', 'ulr_eng_coef', 'pt_z_coef', 'pt_z_p_coef',
131
- 'pt_z_factor', 'pt_antifreeze', 'boom_azimuth', 'nodata',
132
- 'conf', 'file']:
133
- ds.attrs.pop(key, None)
134
-
135
- return ds
136
-
137
- def addTimeShift(ds, vars_df):
138
- '''Shift times based on file format and logger type (shifting only hourly averaged values,
139
- and not instantaneous variables). For raw (10 min), all values are sampled instantaneously
140
- so do not shift. For STM (1 hour), values are averaged and assigned to end-of-hour by the
141
- logger, so shift by -1 hr. For TX (time frequency depends on v2 or v3) then time is shifted
142
- depending on logger type. We use the 'instantaneous_hourly' boolean from variables.csv to
143
- determine if a variable is considered instantaneous at hourly samples.
144
-
145
- This approach creates two separate sub-dataframes, one for hourly-averaged variables
146
- and another for instantaneous variables. The instantaneous dataframe should never be
147
- shifted. We apply shifting only to the hourly average dataframe, then concat the two
148
- dataframes back together.
149
-
150
- It is possible to use pandas merge or join instead of concat, there are equivalent methods
151
- in each. In this case, we use concat throughout.
152
-
153
- Fausto et al. 2021 specifies the convention of assigning hourly averages to start-of-hour,
154
- so we need to retain this unless clearly communicated to users.
155
-
156
- Parameters
157
- ----------
158
- ds : xarray.Dataset
159
- Dataset to apply time shift to
160
- vars_df : pd.DataFrame
161
- Metadata dataframe
162
-
163
- Returns
164
- -------
165
- ds_out : xarray.Dataset
166
- Dataset with shifted times
167
- '''
168
- df = ds.to_dataframe()
169
- # No need to drop duplicates here if performed prior to calling this function.
170
- # df = df[~df.index.duplicated(keep='first')] # drop duplicates, keep=first is arbitrary
171
- df['doy'] = df.index.dayofyear
172
- i_cols = [x for x in df.columns if x in vars_df.index and vars_df['instantaneous_hourly'][x] is True] # instantaneous only, list of columns
173
- df_i = df.filter(items=i_cols, axis=1) # instantaneous only dataframe
174
- df_a = df.drop(df_i.columns, axis=1) # hourly ave dataframe
175
-
176
- if ds.attrs['format'] == 'raw':
177
- # 10-minute data, no shifting
178
- df_out = df
179
- elif ds.attrs['format'] == 'STM':
180
- # hourly-averaged, non-transmitted
181
- # shift everything except instantaneous, any logger type
182
- df_a = df_a.shift(periods=-1, freq="h")
183
- df_out = pd.concat([df_a, df_i], axis=1) # different columns, same datetime indices
184
- df_out = df_out.sort_index()
185
- elif ds.attrs['format'] == 'TX':
186
- if ds.attrs['logger_type'] == 'CR1000X':
187
- # v3, data is hourly all year long
188
- # shift everything except instantaneous
189
- df_a = df_a.shift(periods=-1, freq="h")
190
- df_out = pd.concat([df_a, df_i], axis=1) # different columns, same datetime indices
191
- df_out = df_out.sort_index()
192
- elif ds.attrs['logger_type'] == 'CR1000':
193
- # v2, data is hourly (6-hr for instantaneous) for DOY 100-300, otherwise daily at 00 UTC
194
- # shift non-instantaneous hourly for DOY 100-300, else do not shift daily
195
- df_a_hourly = df_a.loc[(df_a['doy'] >= 100) & (df_a['doy'] <= 300)]
196
- # df_a_hourly = df_a.loc[df_a['doy'].between(100, 300, inclusive='both')] # equivalent to above
197
- df_a_daily_1 = df_a.loc[(df_a['doy'] < 100)]
198
- df_a_daily_2 = df_a.loc[(df_a['doy'] > 300)]
199
-
200
- # shift the hourly ave data
201
- df_a_hourly = df_a_hourly.shift(periods=-1, freq="h")
202
-
203
- # stitch everything back together
204
- df_concat_u = pd.concat([df_a_daily_1, df_a_daily_2, df_a_hourly], axis=0) # same columns, different datetime indices
205
- # It's now possible for df_concat_u to have duplicate datetime indices
206
- df_concat_u = df_concat_u[~df_concat_u.index.duplicated(keep='first')] # drop duplicates, keep=first is arbitrary
207
-
208
- df_out = pd.concat([df_concat_u, df_i], axis=1) # different columns, same datetime indices
209
- df_out = df_out.sort_index()
210
-
211
- # Back to xarray, and re-assign the original attrs
212
- df_out = df_out.drop('doy', axis=1)
213
- ds_out = df_out.to_xarray()
214
- ds_out = ds_out.assign_attrs(ds.attrs) # Dataset attrs
215
- for x in ds_out.data_vars: # variable-specific attrs
216
- ds_out[x].attrs = ds[x].attrs
217
-
218
- # equivalent to above:
219
- # vals = [xr.DataArray(data=df_out[c], dims=['time'], coords={'time':df_out.index}, attrs=ds[c].attrs) for c in df_out.columns]
220
- # ds_out = xr.Dataset(dict(zip(df_out.columns, vals)), attrs=ds.attrs)
221
- return ds_out
222
-
223
- def getPressDepth(z_pt, p, pt_antifreeze, pt_z_factor, pt_z_coef, pt_z_p_coef):
224
- '''Adjust pressure depth and calculate pressure transducer depth based on
225
- pressure transducer fluid density
226
-
227
- Parameters
228
- ----------
229
- z_pt : xr.Dataarray
230
- Pressure transducer height (corrected for offset)
231
- p : xr.Dataarray
232
- Air pressure
233
- pt_antifreeze : float
234
- Pressure transducer anti-freeze percentage for fluid density
235
- correction
236
- pt_z_factor : float
237
- Pressure transducer factor
238
- pt_z_coef : float
239
- Pressure transducer coefficient
240
- pt_z_p_coef : float
241
- Pressure transducer coefficient
242
-
243
- Returns
244
- -------
245
- z_pt_cor : xr.Dataarray
246
- Pressure transducer height corrected
247
- z_pt : xr.Dataarray
248
- Pressure transducer depth
249
- '''
250
- # Calculate pressure transducer fluid density
251
- if pt_antifreeze == 50: #TODO: Implement function w/ reference (analytical or from LUT)
252
- rho_af = 1092 #TODO: Track uncertainty
253
- elif pt_antifreeze == 100:
254
- rho_af = 1145
255
- else:
256
- rho_af = np.nan
257
- logger.info('ERROR: Incorrect metadata: "pt_antifreeze" = ' +
258
- f'{pt_antifreeze}. Antifreeze mix only supported at 50% or 100%')
259
- # assert(False)
260
-
261
- # Correct pressure depth
262
- z_pt_cor = z_pt * pt_z_coef * pt_z_factor * 998.0 / rho_af + 100 * (pt_z_p_coef - p) / (rho_af * 9.81)
263
-
264
- # Calculate pressure transducer depth
265
- z_pt = z_pt * pt_z_coef * pt_z_factor * 998.0 / rho_af
266
-
267
- return z_pt_cor, z_pt
268
-
269
-
270
- def interpTemp(temp, var_configurations, max_interp=pd.Timedelta(12,'h')):
271
- '''Clip and interpolate temperature dataset for use in corrections
272
-
273
- Parameters
274
- ----------
275
- temp : `xarray.DataArray`
276
- Array of temperature data
277
- vars_df : `pandas.DataFrame`
278
- Dataframe to retrieve attribute hi-lo values from for temperature clipping
279
- max_interp : `pandas.Timedelta`
280
- Maximum time steps to interpolate across. The default is 12 hours.
281
-
282
- Returns
283
- -------
284
- temp_interp : `xarray.DataArray`
285
- Array of interpolatedtemperature data
286
- '''
287
- # Determine if upper or lower temperature array
288
- var = temp.name.lower()
289
-
290
- # Find range threshold and use it to clip measurements
291
- cols = ["lo", "hi", "OOL"]
292
- assert set(cols) <= set(var_configurations.columns)
293
- variable_limits = var_configurations[cols].dropna(how="all")
294
- temp = temp.where(temp >= variable_limits.loc[var,'lo'])
295
- temp = temp.where(temp <= variable_limits.loc[var, 'hi'])
296
-
297
- # Drop duplicates and interpolate across NaN values
298
- # temp_interp = temp.drop_duplicates(dim='time', keep='first')
299
- temp_interp = temp.interpolate_na(dim='time', max_gap=max_interp)
300
-
301
- return temp_interp
302
-
303
-
304
- def smoothTilt(tilt, win_size):
305
- '''Smooth tilt values using a rolling window. This is translated from the
306
- previous IDL/GDL smoothing algorithm:
307
- tiltX = smooth(tiltX,7,/EDGE_MIRROR,MISSING=-999) & tiltY = smooth(tiltY,7,/EDGE_MIRROR, MISSING=-999)
308
- endif
309
- In Python, this should be
310
- dstxy = dstxy.rolling(time=7, win_type='boxcar', center=True).mean()
311
- But the EDGE_MIRROR makes it a bit more complicated
312
-
313
- Parameters
314
- ----------
315
- tilt : xarray.DataArray
316
- Array (either 'tilt_x' or 'tilt_y'), tilt values (can be in degrees or voltage)
317
- win_size : int
318
- Window size to use in pandas 'rolling' method.
319
- e.g. a value of 7 spans 70 minutes using 10 minute data.
320
-
321
- Returns
322
- -------
323
- tdf_rolling : tuple, as: (str, numpy.ndarray)
324
- The numpy array is the tilt values, smoothed with a rolling mean
325
- '''
326
- s = int(win_size/2)
327
- tdf = tilt.to_dataframe()
328
- mirror_start = tdf.iloc[:s][::-1]
329
- mirror_end = tdf.iloc[-s:][::-1]
330
- mirrored_tdf = pd.concat([mirror_start, tdf, mirror_end])
331
-
332
- tdf_rolling = (
333
- ('time'),
334
- mirrored_tdf.rolling(
335
- win_size, win_type='boxcar', min_periods=1, center=True
336
- ).mean()[s:-s].values.flatten()
337
- )
338
- return tdf_rolling
339
-
340
- def getTiltDegrees(tilt, threshold):
341
- '''Filter tilt with given threshold, and convert from voltage to degrees.
342
- Voltage-to-degrees converseion is based on the equation in 3.2.9 at
343
- https://essd.copernicus.org/articles/13/3819/2021/#section3
344
-
345
- Parameters
346
- ----------
347
- tilt : xarray.DataArray
348
- Array (either 'tilt_x' or 'tilt_y'), tilt values (voltage)
349
- threshold : int
350
- Values below this threshold (-100) will not be retained.
351
-
352
- Returns
353
- -------
354
- dst.interpolate_na() : xarray.DataArray
355
- Array (either 'tilt_x' or 'tilt_y'), tilt values (degrees)
356
- '''
357
- # notOKtiltX = where(tiltX lt -100, complement=OKtiltX) & notOKtiltY = where(tiltY lt -100, complement=OKtiltY)
358
- notOKtilt = (tilt < threshold)
359
- OKtilt = (tilt >= threshold)
360
- tilt = tilt / 10
361
-
362
- # IDL version:
363
- # tiltX = tiltX/10.
364
- # tiltnonzero = where(tiltX ne 0 and tiltX gt -40 and tiltX lt 40)
365
- # if n_elements(tiltnonzero) ne 1 then tiltX[tiltnonzero] = tiltX[tiltnonzero]/abs(tiltX[tiltnonzero])*(-0.49*(abs(tiltX[tiltnonzero]))^4 + 3.6*(abs(tiltX[tiltnonzero]))^3 - 10.4*(abs(tiltX[tiltnonzero]))^2 +21.1*(abs(tiltX[tiltnonzero])))
366
- # tiltY = tiltY/10.
367
- # tiltnonzero = where(tiltY ne 0 and tiltY gt -40 and tiltY lt 40)
368
- # if n_elements(tiltnonzero) ne 1 then tiltY[tiltnonzero] = tiltY[tiltnonzero]/abs(tiltY[tiltnonzero])*(-0.49*(abs(tiltY[tiltnonzero]))^4 + 3.6*(abs(tiltY[tiltnonzero]))^3 - 10.4*(abs(tiltY[tiltnonzero]))^2 +21.1*(abs(tiltY[tiltnonzero])))
369
-
370
- dst = tilt
371
- nz = (dst != 0) & (np.abs(dst) < 40)
372
-
373
- dst = dst.where(~nz, other = dst / np.abs(dst)
374
- * (-0.49
375
- * (np.abs(dst))**4 + 3.6
376
- * (np.abs(dst))**3 - 10.4
377
- * (np.abs(dst))**2 + 21.1
378
- * (np.abs(dst))))
379
-
380
- # if n_elements(OKtiltX) gt 1 then tiltX[notOKtiltX] = interpol(tiltX[OKtiltX],OKtiltX,notOKtiltX) ; Interpolate over gaps for radiation correction; set to -999 again below.
381
- dst = dst.where(~notOKtilt)
382
- return dst.interpolate_na(dim='time', use_coordinate=False) #TODO: Filling w/o considering time gaps to re-create IDL/GDL outputs. Should fill with coordinate not False. Also consider 'max_gap' option?
383
-
384
-
385
- def decodeGPS(ds, gps_names):
386
- '''Decode GPS information based on names of GPS attributes. This should be
387
- applied if gps information does not consist of float values
388
-
389
- Parameters
390
- ----------
391
- ds : xr.Dataset
392
- Data set
393
- gps_names : list
394
- Variable names for GPS information, such as "gps_lat", "gps_lon" and
395
- "gps_alt"
396
-
397
- Returns
398
- -------
399
- ds : xr.Dataset
400
- Data set with decoded GPS information
401
- '''
402
- for v in gps_names:
403
- a = ds[v].attrs
404
- str2nums = [re.findall(r"[-+]?\d*\.\d+|\d+", _) if isinstance(_, str) else [np.nan] for _ in ds[v].values]
405
- ds[v][:] = pd.DataFrame(str2nums).astype(float).T.values[0]
406
- ds[v] = ds[v].astype(float)
407
- ds[v].attrs = a
408
- return ds
409
-
410
- def reformatGPS(pos_arr, attrs):
411
- '''Correct latitude and longitude from native format to decimal degrees.
412
-
413
- v2 stations should send "NH6429.01544","WH04932.86061" (NUK_L 2022)
414
- v3 stations should send coordinates as "6628.93936","04617.59187" (DY2) or 6430,4916 (NUK_Uv3)
415
- decodeGPS should have decoded these strings to floats in ddmm.mmmm format
416
- v1 stations however only saved decimal minutes (mm.mmmmm) as float<=60. '
417
- In this case, we use the integer part of the latitude given in the config
418
- file and append the gps value after it.
419
-
420
- Parameters
421
- ----------
422
- pos_arr : xr.Dataarray
423
- Array of latitude or longitude measured by the GPS
424
- attrs : dict
425
- The global attribute 'latitude' or 'longitude' associated with the
426
- file being processed. It is the standard latitude/longitude given in the
427
- config file for that station.
428
-
429
- Returns
430
- -------
431
- pos_arr : xr.Dataarray
432
- Formatted GPS position array in decimal degree
433
- '''
434
- if np.any((pos_arr <= 90) & (pos_arr > 0)):
435
- # then pos_arr is in decimal minutes, so we add to it the integer
436
- # part of the latitude given in the config file x100
437
- # so that it reads ddmm.mmmmmm like for v2 and v3 files
438
- # Note that np.sign and np.attrs handles negative longitudes.
439
- pos_arr = np.sign(attrs) * (pos_arr + 100*np.floor(np.abs(attrs)))
440
- a = pos_arr.attrs
441
- pos_arr = np.floor(pos_arr / 100) + (pos_arr / 100 - np.floor(pos_arr / 100)) * 100 / 60
442
- pos_arr.attrs = a
443
- return pos_arr
444
-
445
- def _reformatArray(ds_arr):
446
- '''Reformat DataArray values and attributes
447
-
448
- Parameters
449
- ----------
450
- ds_arr : xr.Dataarray
451
- Data array
452
-
453
- Returns
454
- -------
455
- ds_arr : xr.Dataarray
456
- Formatted data array
457
- '''
458
- a = ds_arr.attrs # Store
459
- ds_arr.values = pd.to_numeric(ds_arr, errors='coerce')
460
- ds_arr.attrs = a # Reformat
461
- return ds_arr
462
-
463
- def _removeVars(ds, v_names):
464
- '''Remove redundant variables if present in dataset
465
-
466
- Parameters
467
- ----------
468
- ds : xr.Dataset
469
- Data set
470
- v_names : list
471
- List of column names to drop
472
-
473
- Returns
474
- -------
475
- ds : xr.Dataset
476
- Data set with removed variables
477
- '''
478
- for v in v_names:
479
- if v in list(ds.variables): ds = ds.drop_vars(v)
480
- return ds
481
-
482
- def _popCols(ds, booms, data_type, vars_df, cols):
483
- '''Populate data array columns with given variable names from look-up table
484
-
485
- Parameters
486
- ----------
487
- ds : xr.Dataset
488
- Data set
489
- booms : int
490
- Number of booms (1 or 2)
491
- data_type : str
492
- Type of data ("tx", "raw")
493
- vars_df : pd.DataFrame
494
- Variables lookup table
495
- cols : list
496
- Names of columns to populate
497
-
498
- Returns
499
- -------
500
- ds : xr.Dataset
501
- Data with populated columns
502
- '''
503
- if booms==1:
504
- names = vars_df.loc[(vars_df[cols[0]]!='two-boom')]
505
-
506
- elif booms==2:
507
- names = vars_df.loc[(vars_df[cols[0]]!='one-boom')]
508
-
509
- for v in list(names.index):
510
- if v not in list(ds.variables):
511
- ds[v] = (('time'), np.arange(ds['time'].size)*np.nan)
512
- return ds
513
-
514
- # def _popCols(ds, booms, data_type, vars_df, cols):
515
- # if booms==1:
516
- # if data_type !='TX':
517
- # names = vars_df.loc[(vars_df[cols[0]]!='two-boom')]
518
- # else:
519
- # names = vars_df.loc[(vars_df[cols[0]] != 'two-boom') & vars_df[cols[1]] != 'tx']
520
-
521
- # elif booms==2:
522
- # if data_type !='TX':
523
- # names = vars_df.loc[(vars_df[cols[0]]!='two-boom')]
524
- # else:
525
- # names = vars_df.loc[(vars_df[cols[0]] != 'two-boom') & vars_df[cols[1]] != 'tx']
526
-
527
- # for v in list(names.index):
528
- # if v not in list(ds.variables):
529
- # ds[v] = (('time'), np.arange(ds['time'].size)*np.nan)
530
- # return ds
531
-
532
- #------------------------------------------------------------------------------
533
-
534
- if __name__ == "__main__":
535
- # unittest.main()
536
- pass