pypromice 1.3.6__py3-none-any.whl → 1.4.1__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 (53) hide show
  1. pypromice/postprocess/bufr_to_csv.py +15 -3
  2. pypromice/postprocess/bufr_utilities.py +91 -18
  3. pypromice/postprocess/create_bufr_files.py +178 -0
  4. pypromice/postprocess/get_bufr.py +248 -397
  5. pypromice/postprocess/make_metadata_csv.py +214 -0
  6. pypromice/postprocess/real_time_utilities.py +41 -11
  7. pypromice/process/L0toL1.py +12 -5
  8. pypromice/process/L1toL2.py +69 -14
  9. pypromice/process/L2toL3.py +1034 -186
  10. pypromice/process/aws.py +139 -808
  11. pypromice/process/get_l2.py +90 -0
  12. pypromice/process/get_l2tol3.py +111 -0
  13. pypromice/process/join_l2.py +112 -0
  14. pypromice/process/join_l3.py +551 -120
  15. pypromice/process/load.py +161 -0
  16. pypromice/process/resample.py +147 -0
  17. pypromice/process/utilities.py +68 -0
  18. pypromice/process/write.py +503 -0
  19. pypromice/qc/github_data_issues.py +10 -16
  20. pypromice/qc/persistence.py +52 -30
  21. pypromice/resources/__init__.py +28 -0
  22. pypromice/{process/metadata.csv → resources/file_attributes.csv} +0 -2
  23. pypromice/resources/variable_aliases_GC-Net.csv +78 -0
  24. pypromice/resources/variables.csv +106 -0
  25. pypromice/station_configuration.py +118 -0
  26. pypromice/tx/get_l0tx.py +7 -4
  27. pypromice/tx/payload_formats.csv +1 -0
  28. pypromice/tx/tx.py +27 -6
  29. pypromice/utilities/__init__.py +0 -0
  30. pypromice/utilities/git.py +62 -0
  31. {pypromice-1.3.6.dist-info → pypromice-1.4.1.dist-info}/METADATA +4 -4
  32. pypromice-1.4.1.dist-info/RECORD +53 -0
  33. {pypromice-1.3.6.dist-info → pypromice-1.4.1.dist-info}/WHEEL +1 -1
  34. pypromice-1.4.1.dist-info/entry_points.txt +13 -0
  35. pypromice/postprocess/station_configurations.toml +0 -762
  36. pypromice/process/get_l3.py +0 -46
  37. pypromice/process/variables.csv +0 -92
  38. pypromice/qc/persistence_test.py +0 -150
  39. pypromice/test/test_config1.toml +0 -69
  40. pypromice/test/test_config2.toml +0 -54
  41. pypromice/test/test_email +0 -75
  42. pypromice/test/test_payload_formats.csv +0 -4
  43. pypromice/test/test_payload_types.csv +0 -7
  44. pypromice/test/test_percentile.py +0 -229
  45. pypromice/test/test_raw1.txt +0 -4468
  46. pypromice/test/test_raw_DataTable2.txt +0 -11167
  47. pypromice/test/test_raw_SlimTableMem1.txt +0 -1155
  48. pypromice/test/test_raw_transmitted1.txt +0 -15411
  49. pypromice/test/test_raw_transmitted2.txt +0 -28
  50. pypromice-1.3.6.dist-info/RECORD +0 -53
  51. pypromice-1.3.6.dist-info/entry_points.txt +0 -8
  52. {pypromice-1.3.6.dist-info → pypromice-1.4.1.dist-info}/LICENSE.txt +0 -0
  53. {pypromice-1.3.6.dist-info → pypromice-1.4.1.dist-info}/top_level.txt +0 -0
@@ -2,135 +2,1052 @@
2
2
  """
3
3
  AWS Level 2 (L2) to Level 3 (L3) data processing
4
4
  """
5
+ import pandas as pd
5
6
  import numpy as np
6
7
  import xarray as xr
8
+ from sklearn.linear_model import LinearRegression
9
+ from pypromice.qc.github_data_issues import adjustData
10
+ from scipy.interpolate import interp1d
11
+ from pathlib import Path
12
+ import logging
7
13
 
8
- def toL3(L2, T_0=273.15, z_0=0.001, R_d=287.05, eps=0.622, es_0=6.1071,
9
- es_100=1013.246):
10
- '''Process one Level 2 (L2) product to Level 3 (L3)
14
+ logger = logging.getLogger(__name__)
15
+
16
+ def toL3(L2,
17
+ data_adjustments_dir: Path,
18
+ station_config={},
19
+ T_0=273.15):
20
+ '''Process one Level 2 (L2) product to Level 3 (L3) meaning calculating all
21
+ derived variables:
22
+ - Turbulent fluxes
23
+ - smoothed and inter/extrapolated GPS coordinates
24
+ - continuous surface height, ice surface height, snow height
25
+ - thermistor depths
26
+
11
27
 
12
28
  Parameters
13
29
  ----------
14
30
  L2 : xarray:Dataset
15
31
  L2 AWS data
32
+ station_config : Dict
33
+ Dictionary containing the information necessary for the processing of
34
+ L3 variables (relocation dates for coordinates processing, or thermistor
35
+ string maintenance date for the thermistors depth)
16
36
  T_0 : int
17
- Steam point temperature. Default is 273.15.
18
- z_0 : int
19
- Aerodynamic surface roughness length for momention, assumed constant
20
- for all ice/snow surfaces. Default is 0.001.
21
- R_d : int
22
- Gas constant of dry air. Default is 287.05.
23
- eps : int
24
- Default is 0.622.
25
- es_0 : int
26
- Saturation vapour pressure at the melting point (hPa). Default is 6.1071.
27
- es_100 : int
28
- Saturation vapour pressure at steam point temperature (hPa). Default is
29
- 1013.246.
37
+ Freezing point temperature. Default is 273.15.
30
38
  '''
31
39
  ds = L2
40
+ ds.attrs['level'] = 'L3'
32
41
 
33
- T_100 = _getTempK(T_0) # Get steam point temperature as K
42
+ T_100 = T_0+100 # Get steam point temperature as K
34
43
 
35
- ds['wdir_u'] = ds['wdir_u'].where(ds['wspd_u'] != 0) # Get directional wind speed
36
- ds['wspd_x_u'], ds['wspd_y_u'] = calcDirWindSpeeds(ds['wspd_u'], ds['wdir_u'])
37
-
38
- # Upper boom bulk calculation
39
- T_h_u = ds['t_u'].copy() # Copy for processing
40
- p_h_u = ds['p_u'].copy()
41
- WS_h_u = ds['wspd_u'].copy()
42
- RH_cor_h_u = ds['rh_u_cor'].copy()
43
- Tsurf_h = ds['t_surf'].copy() # T surf from derived upper boom product. TODO is this okay to use with lower boom parameters?
44
- z_WS_u = ds['z_boom_u'].copy() + 0.4 # Get height of Anemometer
45
- z_T_u = ds['z_boom_u'].copy() - 0.1 # Get height of thermometer
44
+ # Turbulent heat flux calculation
45
+ if ('t_u' in ds.keys()) and \
46
+ ('p_u' in ds.keys()) and \
47
+ ('rh_u_cor' in ds.keys()):
48
+ # Upper boom bulk calculation
49
+ T_h_u = ds['t_u'].copy() # Copy for processing
50
+ p_h_u = ds['p_u'].copy()
51
+ RH_cor_h_u = ds['rh_u_cor'].copy()
52
+
53
+ q_h_u = calculate_specific_humidity(T_0, T_100, T_h_u, p_h_u, RH_cor_h_u) # Calculate specific humidity
54
+ if ('wspd_u' in ds.keys()) and \
55
+ ('t_surf' in ds.keys()) and \
56
+ ('z_boom_u' in ds.keys()):
57
+ WS_h_u = ds['wspd_u'].copy()
58
+ Tsurf_h = ds['t_surf'].copy() # T surf from derived upper boom product. TODO is this okay to use with lower boom parameters?
59
+ z_WS_u = ds['z_boom_u'].copy() + 0.4 # Get height of Anemometer
60
+ z_T_u = ds['z_boom_u'].copy() - 0.1 # Get height of thermometer
61
+
62
+ if not ds.attrs['bedrock']:
63
+ SHF_h_u, LHF_h_u= calculate_tubulent_heat_fluxes(T_0, T_h_u, Tsurf_h, WS_h_u, # Calculate latent and sensible heat fluxes
64
+ z_WS_u, z_T_u, q_h_u, p_h_u)
46
65
 
47
- rho_atm_u = 100 * p_h_u / R_d / (T_h_u + T_0) # Calculate atmospheric density
48
- nu_u = calcVisc(T_h_u, T_0, rho_atm_u) # Calculate kinematic viscosity
49
- q_h_u = calcHumid(T_0, T_100, T_h_u, es_0, es_100, eps, # Calculate specific humidity
50
- p_h_u, RH_cor_h_u)
51
- if not ds.attrs['bedrock']:
52
- 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
53
- z_WS_u, z_T_u, nu_u, q_h_u, p_h_u)
54
- 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
55
- WS_h_u, RH_cor_h_u, ds['z_boom_u'])
56
- ds['dshf_u'] = (('time'), SHF_h_u.data)
57
- ds['dlhf_u'] = (('time'), LHF_h_u.data)
58
- q_h_u = 1000 * q_h_u # Convert sp.humid from kg/kg to g/kg
59
- q_h_u = cleanSpHumid(q_h_u, T_h_u, Tsurf_h, p_h_u, RH_cor_h_u) # Clean sp.humid values
60
- ds['qh_u'] = (('time'), q_h_u.data)
66
+ ds['dshf_u'] = (('time'), SHF_h_u.data)
67
+ ds['dlhf_u'] = (('time'), LHF_h_u.data)
68
+ else:
69
+ logger.info('wspd_u, t_surf or z_boom_u missing, cannot calulate tubrulent heat fluxes')
70
+
71
+ q_h_u = 1000 * q_h_u # Convert sp.humid from kg/kg to g/kg
72
+ ds['qh_u'] = (('time'), q_h_u.data)
73
+ else:
74
+ logger.info('t_u, p_u or rh_u_cor missing, cannot calulate tubrulent heat fluxes')
61
75
 
62
76
  # Lower boom bulk calculation
63
- if ds.attrs['number_of_booms']==2:
64
- # ds['wdir_l'] = _calcWindDir(ds['wspd_x_l'], ds['wspd_y_l']) # Calculatate wind direction
65
-
66
- T_h_l = ds['t_l'].copy() # Copy for processing
67
- p_h_l = ds['p_l'].copy()
68
- WS_h_l = ds['wspd_l'].copy()
69
- RH_cor_h_l = ds['rh_l_cor'].copy()
70
- z_WS_l = ds['z_boom_l'].copy() + 0.4 # Get height of W
71
- z_T_l = ds['z_boom_l'].copy() - 0.1 # Get height of thermometer
72
-
73
- rho_atm_l = 100 * p_h_l / R_d / (T_h_l + T_0) # Calculate atmospheric density
74
- nu_l = calcVisc(T_h_l, T_0, rho_atm_l) # Calculate kinematic viscosity
75
- q_h_l = calcHumid(T_0, T_100, T_h_l, es_0, es_100, eps, # Calculate sp.humidity
76
- p_h_l, RH_cor_h_l)
77
- if not ds.attrs['bedrock']:
78
- 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
79
- z_WS_l, z_T_l, nu_l, q_h_l, p_h_l)
80
- 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
81
- WS_h_l, RH_cor_h_l, ds['z_boom_l'])
82
- ds['dshf_l'] = (('time'), SHF_h_l.data)
83
- ds['dlhf_l'] = (('time'), LHF_h_l.data)
84
- q_h_l = 1000 * q_h_l # Convert sp.humid from kg/kg to g/kg
85
- q_h_l = cleanSpHumid(q_h_l, T_h_l, Tsurf_h, p_h_l, RH_cor_h_l) # Clean sp.humid values
86
- ds['qh_l'] = (('time'), q_h_l.data)
87
-
88
- ds['wdir_l'] = ds['wdir_l'].where(ds['wspd_l'] != 0) # Get directional wind speed
89
- ds['wspd_x_l'], ds['wspd_y_l'] = calcDirWindSpeeds(ds['wspd_l'], ds['wdir_l'])
90
-
91
- if hasattr(ds, 'wdir_i'):
92
- if ~ds['wdir_i'].isnull().all() and ~ds['wspd_i'].isnull().all(): # Instantaneous msg processing
93
- ds['wdir_i'] = ds['wdir_i'].where(ds['wspd_i'] != 0) # Get directional wind speed
94
- ds['wspd_x_i'], ds['wspd_y_i'] = calcDirWindSpeeds(ds['wspd_i'], ds['wdir_i'])
77
+ if ds.attrs['number_of_booms']==2:
78
+ if ('t_l' in ds.keys()) and \
79
+ ('p_l' in ds.keys()) and \
80
+ ('rh_l_cor' in ds.keys()):
81
+ T_h_l = ds['t_l'].copy() # Copy for processing
82
+ p_h_l = ds['p_l'].copy()
83
+ RH_cor_h_l = ds['rh_l_cor'].copy()
84
+
85
+ q_h_l = calculate_specific_humidity(T_0, T_100, T_h_l, p_h_l, RH_cor_h_l) # Calculate sp.humidity
86
+
87
+ if ('wspd_l' in ds.keys()) and \
88
+ ('t_surf' in ds.keys()) and \
89
+ ('z_boom_l' in ds.keys()):
90
+ z_WS_l = ds['z_boom_l'].copy() + 0.4 # Get height of W
91
+ z_T_l = ds['z_boom_l'].copy() - 0.1 # Get height of thermometer
92
+ WS_h_l = ds['wspd_l'].copy()
93
+ if not ds.attrs['bedrock']:
94
+ SHF_h_l, LHF_h_l= calculate_tubulent_heat_fluxes(T_0, T_h_l, Tsurf_h, WS_h_l, # Calculate latent and sensible heat fluxes
95
+ z_WS_l, z_T_l, q_h_l, p_h_l)
96
+
97
+ ds['dshf_l'] = (('time'), SHF_h_l.data)
98
+ ds['dlhf_l'] = (('time'), LHF_h_l.data)
99
+ else:
100
+ logger.info('wspd_l, t_surf or z_boom_l missing, cannot calulate tubrulent heat fluxes')
101
+
102
+ q_h_l = 1000 * q_h_l # Convert sp.humid from kg/kg to g/kg
103
+ ds['qh_l'] = (('time'), q_h_l.data)
104
+ else:
105
+ logger.info('t_l, p_l or rh_l_cor missing, cannot calulate tubrulent heat fluxes')
106
+
107
+ if len(station_config)==0:
108
+ logger.warning('\n***\nThe station configuration file is missing or improperly passed to pypromice. Some processing steps might fail.\n***\n')
109
+
110
+ # Smoothing and inter/extrapolation of GPS coordinates
111
+ for var in ['gps_lat', 'gps_lon', 'gps_alt']:
112
+ ds[var.replace('gps_','')] = ('time', gps_coordinate_postprocessing(ds, var, station_config))
113
+
114
+ # processing continuous surface height, ice surface height, snow height
115
+ try:
116
+ ds = process_surface_height(ds, data_adjustments_dir, station_config)
117
+ except Exception as e:
118
+ logger.error("Error processing surface height at %s"%L2.attrs['station_id'])
119
+ logging.error(e, exc_info=True)
120
+
121
+ # making sure dataset has the attributes contained in the config files
122
+ if 'project' in station_config.keys():
123
+ ds.attrs['project'] = station_config['project']
124
+ else:
125
+ logger.error('No project info in station_config. Using \"PROMICE\".')
126
+ ds.attrs['project'] = "PROMICE"
127
+
128
+ if 'location_type' in station_config.keys():
129
+ ds.attrs['location_type'] = station_config['location_type']
130
+ else:
131
+ logger.error('No project info in station_config. Using \"ice sheet\".')
132
+ ds.attrs['location_type'] = "ice sheet"
95
133
 
96
134
  return ds
97
135
 
98
136
 
99
- def calcDirWindSpeeds(wspd, wdir, deg2rad=np.pi/180):
100
- '''Calculate directional wind speed from wind speed and direction
101
-
137
+ def process_surface_height(ds, data_adjustments_dir, station_config={}):
138
+ """
139
+ Process surface height data for different site types and create
140
+ surface height variables.
141
+
102
142
  Parameters
103
143
  ----------
104
- wspd : xr.Dataarray
105
- Wind speed data array
106
- wdir : xr.Dataarray
107
- Wind direction data array
108
- deg2rad : float
109
- Degree to radians coefficient. The default is np.pi/180
110
-
144
+ ds : xarray.Dataset
145
+ The dataset containing various measurements and attributes including
146
+ 'site_type' which determines the type of site (e.g., 'ablation',
147
+ 'accumulation', 'bedrock') and other relevant data variables such as
148
+ 'z_boom_u', 'z_stake', 'z_pt_cor', etc.
149
+
111
150
  Returns
112
151
  -------
113
- wspd_x : xr.Dataarray
114
- Wind speed in X direction
115
- wspd_y : xr.Datarray
116
- Wind speed in Y direction
117
- '''
118
- wspd_x = wspd * np.sin(wdir * deg2rad)
119
- wspd_y = wspd * np.cos(wdir * deg2rad)
120
- return wspd_x, wspd_y
152
+ xarray.Dataset
153
+ The dataset with additional processed surface height variables:
154
+ 'z_surf_1', 'z_surf_2', 'z_ice_surf', 'z_surf_combined', 'snow_height',
155
+ and possibly depth variables derived from temperature measurements.
156
+ """
157
+ # Initialize surface height variables with NaNs
158
+ ds['z_surf_1'] = ('time', ds['z_boom_u'].data * np.nan)
159
+ ds['z_surf_2'] = ('time', ds['z_boom_u'].data * np.nan)
121
160
 
161
+ if ds.attrs['site_type'] == 'ablation':
162
+ # Calculate surface heights for ablation sites
163
+ ds['z_surf_1'] = 2.6 - ds['z_boom_u']
164
+ if ds.z_stake.notnull().any():
165
+ first_valid_index = ds.time.where((ds.z_stake + ds.z_boom_u).notnull(), drop=True).data[0]
166
+ ds['z_surf_2'] = ds.z_surf_1.sel(time=first_valid_index) + ds.z_stake.sel(time=first_valid_index) - ds['z_stake']
167
+
168
+ # Use corrected point data if available
169
+ if 'z_pt_cor' in ds.data_vars:
170
+ ds['z_ice_surf'] = ('time', ds['z_pt_cor'].data)
171
+
172
+ else:
173
+ # Calculate surface heights for other site types
174
+ first_valid_index = ds.time.where(ds.z_boom_u.notnull(), drop=True).data[0]
175
+ ds['z_surf_1'] = ds.z_boom_u.sel(time=first_valid_index) - ds['z_boom_u']
176
+ if 'z_stake' in ds.data_vars and ds.z_stake.notnull().any():
177
+ first_valid_index = ds.time.where(ds.z_stake.notnull(), drop=True).data[0]
178
+ ds['z_surf_2'] = ds.z_stake.sel(time=first_valid_index) - ds['z_stake']
179
+ if 'z_boom_l' in ds.data_vars:
180
+ # need a combine first because KAN_U switches from having a z_stake
181
+ # to having a z_boom_l
182
+ first_valid_index = ds.time.where(ds.z_boom_l.notnull(), drop=True).data[0]
183
+ ds['z_surf_2'] = ds['z_surf_2'].combine_first(
184
+ ds.z_boom_l.sel(time=first_valid_index) - ds['z_boom_l'])
185
+
186
+ # Adjust data for the created surface height variables
187
+ ds = adjustData(ds, data_adjustments_dir, var_list=['z_surf_1', 'z_surf_2', 'z_ice_surf'])
188
+
189
+ # Convert to dataframe and combine surface height variables
190
+ df_in = ds[[v for v in ['z_surf_1', 'z_surf_2', 'z_ice_surf'] if v in ds.data_vars]].to_dataframe()
191
+
192
+ (ds['z_surf_combined'], ds['z_ice_surf'],
193
+ ds['z_surf_1_adj'], ds['z_surf_2_adj']) = combine_surface_height(df_in, ds.attrs['site_type'])
194
+
195
+
196
+ if ds.attrs['site_type'] == 'ablation':
197
+ # Calculate rolling minimum for ice surface height and snow height
198
+ ts_interpolated = np.minimum(
199
+ xr.where(ds.z_ice_surf.notnull(),
200
+ ds.z_ice_surf,ds.z_surf_combined),
201
+ ds.z_surf_combined).to_series().resample('h').interpolate(limit=72)
202
+
203
+ if len(ts_interpolated)>24*7:
204
+ # Apply the rolling window with median calculation
205
+ z_ice_surf = (ts_interpolated
206
+ .rolling('14D', center=True, min_periods=1)
207
+ .median())
208
+ # Overprint the first and last 7 days with interpolated values
209
+ # because of edge effect of rolling windows
210
+ z_ice_surf.iloc[:24*7] = (ts_interpolated.iloc[:24*7]
211
+ .rolling('1D', center=True, min_periods=1)
212
+ .median().values)
213
+ z_ice_surf.iloc[-24*7:] = (ts_interpolated.iloc[-24*7:]
214
+ .rolling('1D', center=True, min_periods=1)
215
+ .median().values)
216
+ else:
217
+ z_ice_surf = (ts_interpolated
218
+ .rolling('1D', center=True, min_periods=1)
219
+ .median())
220
+
221
+ z_ice_surf = z_ice_surf.loc[ds.time]
222
+ # here we make sure that the periods where both z_stake and z_pt are
223
+ # missing are also missing in z_ice_surf
224
+ msk = ds['z_ice_surf'].notnull() | ds['z_surf_2_adj'].notnull()
225
+ z_ice_surf = z_ice_surf.where(msk)
226
+
227
+ # taking running minimum to get ice
228
+ z_ice_surf = z_ice_surf.cummin()
229
+
230
+ # filling gaps only if they are less than a year long and if values on both
231
+ # sides are less than 0.01 m appart
232
+
233
+ # Forward and backward fill to identify bounds of gaps
234
+ df_filled = z_ice_surf.fillna(method='ffill').fillna(method='bfill')
235
+
236
+ # Identify gaps and their start and end dates
237
+ gaps = pd.DataFrame(index=z_ice_surf[z_ice_surf.isna()].index)
238
+ gaps['prev_value'] = df_filled.shift(1)
239
+ gaps['next_value'] = df_filled.shift(-1)
240
+ gaps['gap_start'] = gaps.index.to_series().shift(1)
241
+ gaps['gap_end'] = gaps.index.to_series().shift(-1)
242
+ gaps['gap_duration'] = (gaps['gap_end'] - gaps['gap_start']).dt.days
243
+ gaps['value_diff'] = (gaps['next_value'] - gaps['prev_value']).abs()
244
+
245
+ # Determine which gaps to fill
246
+ mask = (gaps['gap_duration'] < 365) & (gaps['value_diff'] < 0.01)
247
+ gaps_to_fill = gaps[mask].index
248
+
249
+ # Fill gaps in the original Series
250
+ z_ice_surf.loc[gaps_to_fill] = df_filled.loc[gaps_to_fill]
251
+
252
+ # bringing the variable into the dataset
253
+ ds['z_ice_surf'] = z_ice_surf
254
+
255
+ ds['z_surf_combined'] = np.maximum(ds['z_surf_combined'], ds['z_ice_surf'])
256
+ ds['snow_height'] = np.maximum(0, ds['z_surf_combined'] - ds['z_ice_surf'])
257
+ ds['z_ice_surf'] = ds['z_ice_surf'].where(ds.snow_height.notnull())
258
+ elif ds.attrs['site_type'] in ['accumulation', 'bedrock']:
259
+ # Handle accumulation and bedrock site types
260
+ ds['z_ice_surf'] = ('time', ds['z_surf_1'].data * np.nan)
261
+ ds['snow_height'] = ds['z_surf_combined']
262
+ else:
263
+ # Log info for other site types
264
+ logger.info('other site type')
265
+
266
+ if ds.attrs['site_type'] != 'bedrock':
267
+ # Process ice temperature data and create depth variables
268
+ ice_temp_vars = [v for v in ds.data_vars if 't_i_' in v]
269
+ vars_out = [v.replace('t', 'd_t') for v in ice_temp_vars]
270
+ vars_out.append('t_i_10m')
271
+ df_out = get_thermistor_depth(
272
+ ds[ice_temp_vars + ['z_surf_combined']].to_dataframe(),
273
+ ds.attrs['station_id'],
274
+ station_config)
275
+ for var in df_out.columns:
276
+ ds[var] = ('time', df_out[var].values)
277
+
278
+ return ds
279
+
280
+ def combine_surface_height(df, site_type, threshold_ablation = -0.0002):
281
+ '''Combines the data from three sensor: the two sonic rangers and the
282
+ pressure transducer, to recreate the surface height, the ice surface height
283
+ and the snow depth through the years. For the accumulation sites, it is
284
+ only the average of the two sonic rangers (after manual adjustments to
285
+ correct maintenance shifts). For the ablation sites, first an ablation
286
+ period is estimated each year (either the period when z_pt_cor decreases
287
+ or JJA if no better estimate) then different adjustmnents are conducted
288
+ to stitch the three time series together: z_ice_surface (adjusted from
289
+ z_pt_cor) or if unvailable, z_surf_2 (adjusted from z_stake)
290
+ are used in the ablation period while an average of z_surf_1 and z_surf_2
291
+ are used otherwise, after they are being adjusted to z_ice_surf at the end
292
+ of the ablation season.
293
+
294
+ Parameters
295
+ ----------
296
+ df : pandas.dataframe
297
+ Dataframe with datetime index and variables z_surf_1, z_surf_2 and z_ice_surf
298
+ site_type : str
299
+ Either 'accumulation' or 'ablation'
300
+ threshold_ablation : float
301
+ Threshold to which a z_pt_cor hourly decrease is compared. If the decrease
302
+ is higher, then there is ablation.
303
+ '''
304
+ logger.info('Combining surface height')
305
+
306
+ if 'z_surf_2' not in df.columns:
307
+ logger.info('-> did not find z_surf_2')
308
+ df["z_surf_2"] = df["z_surf_1"].values*np.nan
309
+
310
+ if 'z_ice_surf' not in df.columns:
311
+ logger.info('-> did not find z_ice_surf')
312
+ df["z_ice_surf"] = df["z_surf_1"].values*np.nan
313
+
314
+ if site_type in ['accumulation', 'bedrock']:
315
+ logger.info('-> no z_pt or accumulation site: averaging z_surf_1 and z_surf_2')
316
+ df["z_surf_1_adj"] = hampel(df["z_surf_1"].interpolate(limit=72)).values
317
+ df["z_surf_2_adj"] = hampel(df["z_surf_2"].interpolate(limit=72)).values
318
+ # adjusting z_surf_2 to z_surf_1
319
+ df["z_surf_2_adj"] = df["z_surf_2_adj"] + (df["z_surf_1_adj"]- df["z_surf_2_adj"]).mean()
320
+ # z_surf_combined is the average of the two z_surf
321
+ if df.z_surf_1_adj.notnull().any() & df.z_surf_2_adj.notnull().any():
322
+ df['z_surf_combined'] = df[['z_surf_1_adj', 'z_surf_2_adj']].mean(axis = 1).values
323
+ elif df.z_surf_1_adj.notnull().any():
324
+ df['z_surf_combined'] = df.z_surf_1_adj.values
325
+ elif df.z_surf_2_adj.notnull().any():
326
+ df['z_surf_combined'] = df.z_surf_2_adj.values
122
327
 
123
- def calcHeatFlux(T_0, T_h, Tsurf_h, rho_atm, WS_h, z_WS, z_T, nu, q_h, p_h,
328
+ # df["z_surf_combined"] = hampel(df["z_surf_combined"].interpolate(limit=72)).values
329
+ return (df['z_surf_combined'], df["z_surf_combined"]*np.nan,
330
+ df["z_surf_1_adj"], df["z_surf_2_adj"])
331
+
332
+ else:
333
+ logger.info('-> ablation site')
334
+ # smoothing and filtering pressure transducer data
335
+ df["z_ice_surf_adj"] = hampel(df["z_ice_surf"].interpolate(limit=72)).values
336
+ df["z_surf_1_adj"] = hampel(df["z_surf_1"].interpolate(limit=72)).values
337
+ df["z_surf_2_adj"] = hampel(df["z_surf_2"].interpolate(limit=72)).values
338
+
339
+ df["z_surf_1_adj"] = hampel(df["z_surf_1"].interpolate(limit=72), k=24, t0=5).values
340
+ df["z_surf_2_adj"] = hampel(df["z_surf_2"].interpolate(limit=72), k=24, t0=5).values
341
+
342
+ # defining ice ablation period from the decrease of a smoothed version of z_pt
343
+ # meaning when smoothed_z_pt.diff() < threshold_ablation
344
+ # first smoothing
345
+ smoothed_PT = (df['z_ice_surf']
346
+ .resample('h')
347
+ .interpolate(limit=72)
348
+ .rolling('14D',center=True, min_periods=1)
349
+ .mean())
350
+ # second smoothing
351
+ smoothed_PT = smoothed_PT.rolling('14D', center=True, min_periods=1).mean()
352
+
353
+ smoothed_PT = smoothed_PT.reindex(df.index,method='ffill')
354
+ # smoothed_PT.loc[df.z_ice_surf.isnull()] = np.nan
355
+
356
+ # logical index where ablation is detected
357
+ ind_ablation = np.logical_and(smoothed_PT.diff().values < threshold_ablation,
358
+ np.isin(smoothed_PT.diff().index.month, [6, 7, 8, 9]))
359
+
360
+
361
+ # finding the beginning and end of each period with True
362
+ idx = np.argwhere(np.diff(np.r_[False,ind_ablation, False])).reshape(-1, 2)
363
+ idx[:, 1] -= 1
364
+
365
+ # fill small gaps in the ice ablation periods.
366
+ for i in range(len(idx)-1):
367
+ ind = idx[i]
368
+ ind_next = idx[i+1]
369
+ # if the end of an ablation period is less than 60 days away from
370
+ # the next ablation, then it is still considered like the same ablation
371
+ # season
372
+ if df.index[ind_next[0]]-df.index[ind[1]]<pd.to_timedelta('60 days'):
373
+ ind_ablation[ind[1]:ind_next[0]]=True
374
+
375
+ # finding the beginning and end of each period with True
376
+ idx = np.argwhere(np.diff(np.r_[False,ind_ablation, False])).reshape(-1, 2)
377
+ idx[:, 1] -= 1
378
+
379
+ # because the smooth_PT sees 7 days ahead, it starts showing a decline
380
+ # 7 days in advance, we therefore need to exclude the first 7 days of
381
+ # each ablation period
382
+ for start, end in idx:
383
+ period_start = df.index[start]
384
+ period_end = period_start + pd.Timedelta(days=7)
385
+ exclusion_period = (df.index >= period_start) & (df.index < period_end)
386
+ ind_ablation[exclusion_period] = False
387
+
388
+ hs1=df["z_surf_1_adj"].interpolate(limit=24*2).copy()
389
+ hs2=df["z_surf_2_adj"].interpolate(limit=24*2).copy()
390
+ z=df["z_ice_surf_adj"].interpolate(limit=24*2).copy()
391
+
392
+ # the surface heights are adjusted so that they start at 0
393
+
394
+
395
+ if any(~np.isnan(hs2.iloc[:24*7])):
396
+ hs2 = hs2 - hs2.iloc[:24*7].mean()
397
+
398
+ if any(~np.isnan(hs2.iloc[:24*7])) & any(~np.isnan(hs1.iloc[:24*7])):
399
+ hs2 = hs2 + hs1.iloc[:24*7].mean() - hs2.iloc[:24*7].mean()
400
+
401
+ if any(~np.isnan(z.iloc[:24*7])):
402
+ # expressing ice surface height relative to its mean value in the
403
+ # first week of the record
404
+ z = z - z.iloc[:24*7].mean()
405
+ elif z.notnull().any():
406
+ # if there is no data in the first week but that there are some
407
+ # PT data afterwards
408
+ if ((z.first_valid_index() - hs1.first_valid_index()) < pd.to_timedelta('251D')) &\
409
+ ((z.first_valid_index() - hs1.first_valid_index()) > pd.to_timedelta('0H')):
410
+ # if the pressure transducer is installed the year after then
411
+ # we use the mean surface height 1 on its first week as a 0
412
+ # for the ice height
413
+ z = z - z.loc[
414
+ z.first_valid_index():(z.first_valid_index()+pd.to_timedelta('14D'))
415
+ ].mean() + hs1.iloc[:24*7].mean()
416
+ else:
417
+ # if there is more than a year (actually 251 days) between the
418
+ # initiation of the AWS and the installation of the pressure transducer
419
+ # we remove the intercept in the pressure transducer data.
420
+ # Removing the intercept
421
+ # means that we consider the ice surface height at 0 when the AWS
422
+ # is installed, and not when the pressure transducer is installed.
423
+ Y = z.iloc[:].values.reshape(-1, 1)
424
+ X = z.iloc[~np.isnan(Y)].index.astype(np.int64).values.reshape(-1, 1)
425
+ Y = Y[~np.isnan(Y)]
426
+ linear_regressor = LinearRegression()
427
+ linear_regressor.fit(X, Y)
428
+ Y_pred = linear_regressor.predict(z.index.astype(np.int64).values.reshape(-1, 1) )
429
+ z = z-Y_pred[0]
430
+
431
+ years = df.index.year.unique().values
432
+ ind_start = years.copy()
433
+ ind_end = years.copy()
434
+ logger.debug('-> estimating ablation period for each year')
435
+ for i, y in enumerate(years):
436
+ # for each year
437
+ ind_yr = df.index.year.values==y
438
+ ind_abl_yr = np.logical_and(ind_yr, ind_ablation)
439
+
440
+ if df.loc[
441
+ np.logical_and(ind_yr, df.index.month.isin([6,7,8])),
442
+ "z_ice_surf_adj"].isnull().all():
443
+
444
+ ind_abl_yr = np.logical_and(ind_yr, df.index.month.isin([6,7,8]))
445
+ ind_ablation[ind_yr] = ind_abl_yr[ind_yr]
446
+ logger.debug(str(y)+' no z_ice_surf, just using JJA')
447
+
448
+ else:
449
+ logger.debug(str(y)+ ' derived from z_ice_surf')
450
+
451
+ if np.any(ind_abl_yr):
452
+ # if there are some ablation flagged for that year
453
+ # then find begining and end
454
+ ind_start[i] = np.argwhere(ind_abl_yr)[0][0]
455
+ ind_end[i] = np.argwhere(ind_abl_yr)[-1][0]
456
+
457
+ else:
458
+ logger.debug(str(y) + ' could not estimate ablation season')
459
+ # otherwise left as nan
460
+ ind_start[i] = -999
461
+ ind_end[i] = -999
462
+
463
+ # adjustement loop
464
+ missing_hs2 = 0 # if hs2 is missing then when it comes back it is adjusted to hs1
465
+ hs2_ref = 0 # by default, the PT is the reference: hs1 and 2 will be adjusted to PT
466
+ # but if it is missing one year or one winter, then it needs to be rajusted
467
+ # to hs1 and hs2 the year after.
468
+
469
+ for i, y in enumerate(years):
470
+ # if y == 2014:
471
+ # import pdb; pdb.set_trace()
472
+ logger.debug(str(y))
473
+ # defining subsets of hs1, hs2, z
474
+ hs1_jja = hs1[str(y)+'-06-01':str(y)+'-09-01']
475
+ hs2_jja = hs2[str(y)+'-06-01':str(y)+'-09-01']
476
+ z_jja = z[str(y)+'-06-01':str(y)+'-09-01']
477
+
478
+ z_ablation = z.iloc[ind_start[i]:ind_end[i]]
479
+ hs2_ablation = hs2.iloc[ind_start[i]:ind_end[i]]
480
+
481
+ hs1_year = hs1[str(y)]
482
+ hs2_year = hs2[str(y)]
483
+
484
+ hs2_winter = hs2[str(y)+'-01-01':str(y)+'-03-01'].copy()
485
+ z_winter = z[str(y)+'-01-01':str(y)+'-03-01'].copy()
486
+
487
+ z_year = z[str(y)]
488
+ if hs1_jja.isnull().all() and hs2_jja.isnull().all() and z_jja.isnull().all():
489
+ # if there is no height for a year between June and September
490
+ # then the adjustment cannot be made automatically
491
+ # it needs to be specified manually on the adjustment files
492
+ # on https://github.com/GEUS-Glaciology-and-Climate/PROMICE-AWS-data-issues
493
+ continue
494
+
495
+ if all(np.isnan(z_jja)) and any(~np.isnan(hs2_jja)):
496
+ # if there is no PT for a given year, but there is some hs2
497
+ # then z will be adjusted to hs2 next time it is available
498
+ hs2_ref = 1
499
+
500
+ if all(np.isnan(z_winter)) and all(np.isnan(hs2_winter)):
501
+ # if there is no PT nor hs2 during the winter, then again
502
+ # we need to adjust z to match hs2 when ablation starts
503
+ hs2_ref = 1
504
+
505
+ # adjustment at the start of the ablation season
506
+ if hs2_ref:
507
+ # if hs2 has been taken as reference in the previous years
508
+ # then we check if pressure transducer is reinstalled and needs
509
+ # to be adjusted
510
+ if ind_start[i] != -999:
511
+ # the first year there is both ablation and PT data available
512
+ # then PT is adjusted to hs2
513
+ if any(~np.isnan(z_ablation)) and any(~np.isnan(hs2_ablation)):
514
+ tmp1 = z_ablation.copy()
515
+ tmp2 = hs2_ablation.copy()
516
+ # tmp1[np.isnan(tmp2)] = np.nan
517
+ # tmp2[np.isnan(tmp1)] = np.nan
518
+
519
+ # in some instances, the PT data is available but no ablation
520
+ # is recorded, then hs2 remains the reference during that time.
521
+ # When eventually there is ablation, then we need to find the
522
+ # first index in these preceding ablation-free years
523
+ # the shift will be applied back from this point
524
+ # first_index = z[:z[str(y)].first_valid_index()].isnull().iloc[::-1].idxmax()
525
+ # z[first_index:] = z[first_index:] - np.nanmean(tmp1) + np.nanmean(tmp2)
526
+ # hs2_ref = 0 # from now on PT is the reference
527
+
528
+ # in some other instance, z just need to be adjusted to hs2
529
+ # first_index = z[str(y)].first_valid_index()
530
+ first_index = z.iloc[ind_start[i]:].first_valid_index() # of ablation
531
+ if np.isnan(hs2[first_index]):
532
+ first_index_2 = hs2.iloc[ind_start[i]:].first_valid_index()
533
+ if (first_index_2 - first_index)>pd.Timedelta('30d'):
534
+ logger.debug('adjusting z to hs1')
535
+ if np.isnan(hs1[first_index]):
536
+ first_index = hs1.iloc[ind_start[i]:].first_valid_index()
537
+ z[first_index:] = z[first_index:] - z[first_index] + hs1[first_index]
538
+ else:
539
+ logger.debug('adjusting z to hs1')
540
+ first_index = hs2.iloc[ind_start[i]:].first_valid_index()
541
+ z[first_index:] = z[first_index:] - z[first_index] + hs2[first_index]
542
+ else:
543
+ logger.debug('adjusting z to hs1')
544
+ z[first_index:] = z[first_index:] - z[first_index] + hs2[first_index]
545
+ hs2_ref = 0 # from now on PT is the reference
546
+
547
+
548
+ else:
549
+ # if z_pt is the reference and there is some ablation
550
+ # then hs1 and hs2 are adjusted to z_pt
551
+ if (ind_start[i] != -999) & z_year.notnull().any():
552
+ # calculating first index with PT, hs1 and hs2
553
+ first_index = z_year.first_valid_index()
554
+ if hs1_year.notnull().any():
555
+ first_index = np.max(np.array(
556
+ [first_index,
557
+ hs1_year.first_valid_index()]))
558
+ if hs2_year.notnull().any():
559
+ first_index = np.max(np.array(
560
+ [first_index,
561
+ hs2_year.first_valid_index()]))
562
+
563
+ # if PT, hs1 and hs2 are all nan until station is reactivated, then
564
+ first_day_of_year = pd.to_datetime(str(y)+'-01-01')
565
+
566
+ if len(z[first_day_of_year:first_index-pd.to_timedelta('1D')])>0:
567
+ if z[first_day_of_year:first_index-pd.to_timedelta('1D')].isnull().all() & \
568
+ hs1[first_day_of_year:first_index-pd.to_timedelta('1D')].isnull().all() & \
569
+ hs2[first_day_of_year:first_index-pd.to_timedelta('1D')].isnull().all():
570
+ if (~np.isnan(np.nanmean(z[first_index:first_index+pd.to_timedelta('1D')])) \
571
+ and ~np.isnan(np.nanmean(hs2[first_index:first_index+pd.to_timedelta('1D')]))):
572
+ logger.debug(' ======= adjusting hs1 and hs2 to z_pt')
573
+ if ~np.isnan(np.nanmean(hs1[first_index:first_index+pd.to_timedelta('1D')]) ):
574
+ hs1[first_index:] = hs1[first_index:] \
575
+ - np.nanmean(hs1[first_index:first_index+pd.to_timedelta('1D')]) \
576
+ + np.nanmean(z[first_index:first_index+pd.to_timedelta('1D')])
577
+ if ~np.isnan(np.nanmean(hs2[first_index:first_index+pd.to_timedelta('1D')]) ):
578
+ hs2[first_index:] = hs2[first_index:] \
579
+ - np.nanmean(hs2[first_index:first_index+pd.to_timedelta('1D')]) \
580
+ + np.nanmean(z[first_index:first_index+pd.to_timedelta('1D')])
581
+
582
+ # adjustment taking place at the end of the ablation period
583
+ if (ind_end[i] != -999):
584
+ # if y == 2023:
585
+ # import pdb; pdb.set_trace()
586
+ # if there's ablation and
587
+ # if there are PT data available at the end of the melt season
588
+ if z.iloc[(ind_end[i]-24*7):(ind_end[i]+24*7)].notnull().any():
589
+ logger.debug('adjusting hs2 to z')
590
+ # then we adjust hs2 to the end-of-ablation z
591
+ # first trying at the end of melt season
592
+ if ~np.isnan(np.nanmean(hs2.iloc[(ind_end[i]-24*7):(ind_end[i]+24*30)])):
593
+ logger.debug('using end of melt season')
594
+ hs2.iloc[ind_end[i]:] = hs2.iloc[ind_end[i]:] - \
595
+ np.nanmean(hs2.iloc[(ind_end[i]-24*7):(ind_end[i]+24*30)]) + \
596
+ np.nanmean(z.iloc[(ind_end[i]-24*7):(ind_end[i]+24*30)])
597
+ # if not possible, then trying the end of the following accumulation season
598
+ elif (i+1 < len(ind_start)):
599
+ if ind_start[i+1]!=-999 and any(~np.isnan(hs2.iloc[(ind_start[i+1]-24*7):(ind_start[i+1]+24*7)]+ z.iloc[(ind_start[i+1]-24*7):(ind_start[i+1]+24*7)])):
600
+ logger.debug('using end of accumulation season')
601
+ hs2.iloc[ind_end[i]:] = hs2.iloc[ind_end[i]:] - \
602
+ np.nanmean(hs2.iloc[(ind_start[i+1]-24*7):(ind_start[i+1]+24*7)]) + \
603
+ np.nanmean(z.iloc[(ind_start[i+1]-24*7):(ind_start[i+1]+24*7)])
604
+ else:
605
+ logger.debug('no ablation')
606
+ hs1_following_winter = hs1[str(y)+'-09-01':str(y+1)+'-03-01'].copy()
607
+ hs2_following_winter = hs2[str(y)+'-09-01':str(y+1)+'-03-01'].copy()
608
+ if all(np.isnan(hs2_following_winter)):
609
+ logger.debug('no hs2')
610
+ missing_hs2 = 1
611
+ elif missing_hs2 == 1:
612
+ logger.debug('adjusting hs2')
613
+ # and if there are some hs2 during the accumulation period
614
+ if any(~np.isnan(hs1_following_winter)):
615
+ logger.debug('to hs1')
616
+ # then we adjust hs1 to hs2 during the accumulation area
617
+ # adjustment is done so that the mean hs1 and mean hs2 match
618
+ # for the period when both are available
619
+ hs2_following_winter[np.isnan(hs1_following_winter)] = np.nan
620
+ hs1_following_winter[np.isnan(hs2_following_winter)] = np.nan
621
+
622
+ hs2[str(y)+'-01-01':] = hs2[str(y)+'-01-01':] \
623
+ - np.nanmean(hs2_following_winter) + np.nanmean(hs1_following_winter)
624
+ missing_hs2 = 0
625
+
626
+
627
+ hs1_following_winter = hs1[str(y)+'-09-01':str(y+1)+'-03-01'].copy()
628
+ hs2_following_winter = hs2[str(y)+'-09-01':str(y+1)+'-03-01'].copy()
629
+ # adjusting hs1 to hs2 (no ablation case)
630
+ if any(~np.isnan(hs1_following_winter)):
631
+ logger.debug('adjusting hs1')
632
+ # and if there are some hs2 during the accumulation period
633
+ if any(~np.isnan(hs2_following_winter)):
634
+ logger.debug('to hs2')
635
+ # then we adjust hs1 to hs2 during the accumulation area
636
+ # adjustment is done so that the mean hs1 and mean hs2 match
637
+ # for the period when both are available
638
+ hs1_following_winter[np.isnan(hs2_following_winter)] = np.nan
639
+ hs2_following_winter[np.isnan(hs1_following_winter)] = np.nan
640
+
641
+ hs1[str(y)+'-09-01':] = hs1[str(y)+'-09-01':] \
642
+ - np.nanmean(hs1_following_winter) + np.nanmean(hs2_following_winter)
643
+ hs1_following_winter = hs1[str(y)+'-09-01':str(y+1)+'-03-01'].copy()
644
+
645
+ if ind_end[i] != -999:
646
+ # if there is some hs1
647
+ hs1_following_winter = hs1[str(y)+'-09-01':str(y+1)+'-03-01'].copy()
648
+ hs2_following_winter = hs2[str(y)+'-09-01':str(y+1)+'-03-01'].copy()
649
+ if any(~np.isnan(hs1_following_winter)):
650
+ logger.debug('adjusting hs1')
651
+ # and if there are some hs2 during the accumulation period
652
+ if any(~np.isnan(hs2_following_winter)):
653
+ logger.debug('to hs2, minimizing winter difference')
654
+ # then we adjust hs1 to hs2 during the accumulation area
655
+ # adjustment is done so that the mean hs1 and mean hs2 match
656
+ # for the period when both are available
657
+ tmp1 = hs1.iloc[ind_end[i]:min(len(hs1),ind_end[i]+24*30*9)].copy()
658
+ tmp2 = hs2.iloc[ind_end[i]:min(len(hs2),ind_end[i]+24*30*9)].copy()
659
+
660
+ tmp1[np.isnan(tmp2)] = np.nan
661
+ tmp2[np.isnan(tmp1)] = np.nan
662
+ if tmp1.isnull().all():
663
+ tmp1 = hs1_following_winter.copy()
664
+ tmp2 = hs2_following_winter.copy()
665
+
666
+ tmp1[np.isnan(tmp2)] = np.nan
667
+ tmp2[np.isnan(tmp1)] = np.nan
668
+ hs1.iloc[ind_end[i]:] = hs1.iloc[ind_end[i]:] - np.nanmean(tmp1) + np.nanmean(tmp2)
669
+
670
+ # if no hs2, then use PT data available at the end of the melt season
671
+ elif np.any(~np.isnan(z.iloc[(ind_end[i]-24*14):(ind_end[i]+24*7)])):
672
+ logger.debug('to z')
673
+ # then we adjust hs2 to the end-of-ablation z
674
+ # first trying at the end of melt season
675
+ if ~np.isnan(np.nanmean(hs1.iloc[(ind_end[i]-24*14):(ind_end[i]+24*30)])):
676
+ logger.debug('using end of melt season')
677
+ hs1.iloc[ind_end[i]:] = hs1.iloc[ind_end[i]:] - \
678
+ np.nanmean(hs1.iloc[(ind_end[i]-24*14):(ind_end[i]+24*30)]) + \
679
+ np.nanmean(z.iloc[(ind_end[i]-24*14):(ind_end[i]+24*30)])
680
+ # if not possible, then trying the end of the following accumulation season
681
+ elif ind_start[i+1]!=-999 and any(~np.isnan(hs1.iloc[(ind_start[i+1]-24*14):(ind_start[i+1]+24*7)]+ z.iloc[(ind_start[i+1]-24*14):(ind_start[i+1]+24*7)])):
682
+ logger.debug('using end of accumulation season')
683
+ hs1.iloc[ind_end[i]:] = hs1.iloc[ind_end[i]:] - \
684
+ np.nanmean(hs1.iloc[(ind_start[i+1]-24*14):(ind_start[i+1]+24*7)]) + \
685
+ np.nanmean(z.iloc[(ind_start[i+1]-24*14):(ind_start[i+1]+24*7)])
686
+ elif any(~np.isnan(hs2_year)):
687
+ logger.debug('to the last value of hs2')
688
+ # then we adjust hs1 to hs2 during the accumulation area
689
+ # adjustment is done so that the mean hs1 and mean hs2 match
690
+ # for the period when both are available
691
+ half_span = pd.to_timedelta('7D')
692
+ tmp1 = hs1_year.loc[(hs2_year.last_valid_index()-half_span):(hs2_year.last_valid_index()+half_span)].copy()
693
+ tmp2 = hs2_year.loc[(hs2_year.last_valid_index()-half_span):(hs2_year.last_valid_index()+half_span)].copy()
694
+
695
+ hs1.iloc[ind_end[i]:] = hs1.iloc[ind_end[i]:] - np.nanmean(tmp1) + np.nanmean(tmp2)
696
+
697
+ df["z_surf_1_adj"] = hs1.interpolate(limit=2*24).values
698
+ df["z_surf_2_adj"] = hs2.interpolate(limit=2*24).values
699
+ df["z_ice_surf_adj"] = z.interpolate(limit=2*24).values
700
+
701
+ # making a summary of the surface height
702
+ df["z_surf_combined"] = np.nan
703
+
704
+ # in winter, both SR1 and SR2 are used
705
+ df["z_surf_combined"] = df["z_surf_2_adj"].interpolate(limit=72).values
706
+
707
+
708
+ # in ablation season we use SR2 instead of the SR1&2 average
709
+ # here two options:
710
+ # 1) we ignore the SR1 and only use SR2
711
+ # 2) we use SR1 when SR2 is not available (commented)
712
+ # the later one can cause jumps when SR2 starts to be available few days after SR1
713
+ data_update = df[["z_surf_1_adj", "z_surf_2_adj"]].mean(axis=1).values
714
+
715
+ ind_update = ~ind_ablation
716
+ #ind_update = np.logical_and(ind_ablation, ~np.isnan(data_update))
717
+ df.loc[ind_update,"z_surf_combined"] = data_update[ind_update]
718
+
719
+ # in ablation season we use pressure transducer over all other options
720
+ data_update = df[ "z_ice_surf_adj"].interpolate(limit=72).values
721
+ ind_update = np.logical_and(ind_ablation, ~np.isnan(data_update))
722
+ df.loc[ind_update,"z_surf_combined"] = data_update[ind_update]
723
+
724
+ logger.info('surface height combination finished')
725
+ return df['z_surf_combined'], df["z_ice_surf_adj"], df["z_surf_1_adj"], df["z_surf_2_adj"]
726
+
727
+ def hampel(vals_orig, k=7*24, t0=15):
728
+ '''
729
+ vals: pandas series of values from which to remove outliers
730
+ k: size of window (including the sample; 7 is equal to 3 on either side of value)
731
+ '''
732
+ #Make copy so original not edited
733
+ vals=vals_orig.copy()
734
+ #Hampel Filter
735
+ L= 1.4826
736
+ rolling_median=vals.rolling(k).median()
737
+ difference=np.abs(rolling_median-vals)
738
+ median_abs_deviation=difference.rolling(k).median()
739
+ threshold= t0 *L * median_abs_deviation
740
+ outlier_idx=difference>threshold
741
+ outlier_idx[0:round(k/2)]=False
742
+ vals.loc[outlier_idx]=np.nan
743
+ return(vals)
744
+
745
+
746
+ def get_thermistor_depth(df_in, site, station_config):
747
+ '''Calculates the depth of the thermistors through time based on their
748
+ installation depth (collected in a google sheet) and on the change of surface
749
+ height: instruments getting buried under new snow or surfacing due to ablation.
750
+ There is a potential for additional filtering of thermistor data for surfaced
751
+ (or just noisy) thermistors, but that is currently deactivated because slow.
752
+
753
+ Parameters
754
+ ----------
755
+ df_in : pandas:dataframe
756
+ dataframe containing the ice/firn temperature t_i_* as well as the
757
+ combined surface height z_surf_combined
758
+ site : str
759
+ stid, so that maintenance date and sensor installation depths can be found
760
+ in database
761
+ station_config : dict
762
+ potentially containing the key string_maintenance
763
+ with station_config["string_maintenance"] being a list of dictionaries
764
+ containing maintenance information in the format:
765
+ [
766
+ {"date": "2007-08-20", "installation_depth": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 6.0, 6.0]},
767
+ {"date": "2008-07-17", "installation_depth": [1.2, 2.2, 3.2, 4.2, 5.2, 6.2, 7.2, 10.2]}
768
+ # Add more entries as needed
769
+ ]
770
+ '''
771
+
772
+ temp_cols_name = ['t_i_'+str(i) for i in range(12) if 't_i_'+str(i) in df_in.columns]
773
+ num_therm = len(temp_cols_name)
774
+ depth_cols_name = ['d_t_i_'+str(i) for i in range(1,num_therm+1)]
775
+
776
+ if df_in['z_surf_combined'].isnull().all():
777
+ logger.info('No valid surface height at '+site+', cannot calculate thermistor depth')
778
+ df_in[depth_cols_name + ['t_i_10m']] = np.nan
779
+ else:
780
+ logger.info('Calculating thermistor depth')
781
+
782
+ # Convert maintenance_info to DataFrame for easier manipulation
783
+ maintenance_string = pd.DataFrame(
784
+ station_config.get("string_maintenance",[]),
785
+ columns = ['date', 'installation_depths']
786
+ )
787
+ maintenance_string["date"] = pd.to_datetime(maintenance_string["date"])
788
+ maintenance_string = maintenance_string.sort_values(by='date', ascending=True)
789
+
790
+
791
+ if num_therm == 8:
792
+ ini_depth = [1, 2, 3, 4, 5, 6, 7, 10]
793
+ else:
794
+ ini_depth = [0, 0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5]
795
+ df_in[depth_cols_name] = np.nan
796
+
797
+ # filtering the surface height
798
+ surface_height = df_in["z_surf_combined"].copy()
799
+ ind_filter = surface_height.rolling(window=14, center=True).var() > 0.1
800
+ if any(ind_filter):
801
+ surface_height[ind_filter] = np.nan
802
+ df_in["z_surf_combined"] = surface_height.values
803
+ z_surf_interp = df_in["z_surf_combined"].interpolate()
804
+
805
+ # first initialization of the depths
806
+ for i, col in enumerate(depth_cols_name):
807
+ df_in[col] = (
808
+ ini_depth[i]
809
+ + z_surf_interp.values
810
+ - z_surf_interp[z_surf_interp.first_valid_index()]
811
+ )
812
+
813
+ # reseting depth at maintenance
814
+ if len(maintenance_string.date) == 0:
815
+ logger.info("No maintenance at "+site)
816
+
817
+ for date in maintenance_string.date:
818
+ if date > z_surf_interp.last_valid_index():
819
+ continue
820
+ new_depth = maintenance_string.loc[
821
+ maintenance_string.date == date
822
+ ].installation_depths.values[0]
823
+
824
+ for i, col in enumerate(depth_cols_name[:len(new_depth)]):
825
+ tmp = df_in[col].copy()
826
+ tmp.loc[date:] = (
827
+ new_depth[i]
828
+ + z_surf_interp[date:].values
829
+ - z_surf_interp[date:][
830
+ z_surf_interp[date:].first_valid_index()
831
+ ]
832
+ )
833
+ df_in[col] = tmp.values
834
+
835
+ # % Filtering thermistor data
836
+ for i in range(len(temp_cols_name)):
837
+ tmp = df_in[temp_cols_name[i]].copy()
838
+
839
+ # variance filter
840
+ # ind_filter = (
841
+ # df_in[temp_cols_name[i]]
842
+ # .interpolate(limit=14)
843
+ # .rolling(window=7)
844
+ # .var()
845
+ # > 0.5
846
+ # )
847
+ # month = (
848
+ # df_in[temp_cols_name[i]].interpolate(limit=14).index.month.values
849
+ # )
850
+ # ind_filter.loc[np.isin(month, [5, 6, 7])] = False
851
+ # if any(ind_filter):
852
+ # tmp.loc[ind_filter] = np.nan
853
+
854
+ # before and after maintenance adaptation filter
855
+ if len(maintenance_string.date) > 0:
856
+ for date in maintenance_string.date:
857
+ if isinstance(
858
+ maintenance_string.loc[
859
+ maintenance_string.date == date
860
+ ].installation_depths.values[0],
861
+ str,
862
+ ):
863
+ ind_adapt = np.abs(
864
+ tmp.interpolate(limit=14).index.values
865
+ - pd.to_datetime(date).to_datetime64()
866
+ ) < np.timedelta64(7, "D")
867
+ if any(ind_adapt):
868
+ tmp.loc[ind_adapt] = np.nan
869
+
870
+ # surfaced thermistor
871
+ ind_pos = df_in[depth_cols_name[i]] < 0.1
872
+ if any(ind_pos):
873
+ tmp.loc[ind_pos] = np.nan
874
+
875
+ # copying the filtered values to the original table
876
+ df_in[temp_cols_name[i]] = tmp.values
877
+
878
+ # removing negative depth
879
+ df_in.loc[df_in[depth_cols_name[i]]<0, depth_cols_name[i]] = np.nan
880
+ logger.info("interpolating 10 m firn/ice temperature")
881
+ df_in['t_i_10m'] = interpolate_temperature(
882
+ df_in.index.values,
883
+ df_in[depth_cols_name].values.astype(float),
884
+ df_in[temp_cols_name].values.astype(float),
885
+ kind="linear",
886
+ min_diff_to_depth=1.5,
887
+ ).set_index('date').values
888
+
889
+ # filtering
890
+ ind_pos = df_in["t_i_10m"] > 0.1
891
+ ind_low = df_in["t_i_10m"] < -70
892
+ df_in.loc[ind_pos, "t_i_10m"] = np.nan
893
+ df_in.loc[ind_low, "t_i_10m"] = np.nan
894
+
895
+ return df_in[depth_cols_name + ['t_i_10m']]
896
+
897
+
898
+ def interpolate_temperature(dates, depth_cor, temp, depth=10, min_diff_to_depth=2,
899
+ kind="quadratic"):
900
+ '''Calculates the depth of the thermistors through time based on their
901
+ installation depth (collected in a google sheet) and on the change of surface
902
+ height: instruments getting buried under new snow or surfacing due to ablation.
903
+ There is a potential for additional filtering of thermistor data for surfaced
904
+ (or just noisy) thermistors, but that is currently deactivated because slow.
905
+
906
+ Parameters
907
+ ----------
908
+ dates : numpy.array
909
+ array of datetime64
910
+ depth_cor : numpy.ndarray
911
+ matrix of depths
912
+ temp : numpy.ndarray
913
+ matrix of temperatures
914
+ depth : float
915
+ constant depth at which (depth_cor, temp) should be interpolated.
916
+ min_diff_to_depth: float
917
+ maximum difference allowed between the available depht and the target depth
918
+ for the interpolation to be done.
919
+ kind : str
920
+ type of interpolation from scipy.interpolate.interp1d
921
+ '''
922
+
923
+ depth_cor = depth_cor.astype(float)
924
+ df_interp = pd.DataFrame()
925
+ df_interp["date"] = dates
926
+ df_interp["temperatureObserved"] = np.nan
927
+
928
+ # preprocessing temperatures for small gaps
929
+ tmp = pd.DataFrame(temp)
930
+ tmp["time"] = dates
931
+ tmp = tmp.set_index("time")
932
+ # tmp = tmp.resample("H").mean()
933
+ # tmp = tmp.interpolate(limit=24*7)
934
+ temp = tmp.loc[dates].values
935
+ for i in (range(len(dates))):
936
+ x = depth_cor[i, :].astype(float)
937
+ y = temp[i, :].astype(float)
938
+ ind_no_nan = ~np.isnan(x + y)
939
+ x = x[ind_no_nan]
940
+ y = y[ind_no_nan]
941
+ x, indices = np.unique(x, return_index=True)
942
+ y = y[indices]
943
+ if len(x) < 2 or np.min(np.abs(x - depth)) > min_diff_to_depth:
944
+ continue
945
+ f = interp1d(x, y, kind, fill_value="extrapolate")
946
+ df_interp.iloc[i, 1] = np.min(f(depth), 0)
947
+
948
+ if df_interp.iloc[:5, 1].std() > 0.1:
949
+ df_interp.iloc[:5, 1] = np.nan
950
+
951
+ return df_interp
952
+
953
+ def gps_coordinate_postprocessing(ds, var, station_config={}):
954
+ # saving the static value of 'lat','lon' or 'alt' stored in attribute
955
+ # as it might be the only coordinate available for certain stations (e.g. bedrock)
956
+ var_out = var.replace('gps_','')
957
+ coord_names = {'lat':'latitude','lon':'longitude', 'alt':'altitude'}
958
+ if coord_names[var_out] in list(ds.attrs.keys()):
959
+ static_value = float(ds.attrs[coord_names[var_out]])
960
+ else:
961
+ static_value = np.nan
962
+
963
+ # if there is no gps observations, then we use the static value repeated
964
+ # for each time stamp
965
+ if var not in ds.data_vars:
966
+ print('no',var,'at', ds.attrs['station_id'])
967
+ return np.ones_like(ds['t_u'].data)*static_value
968
+
969
+ if ds[var].isnull().all():
970
+ print('no',var,'at',ds.attrs['station_id'])
971
+ return np.ones_like(ds['t_u'].data)*static_value
972
+
973
+ # Extract station relocations from the config dict
974
+ station_relocations = station_config.get("station_relocation", [])
975
+
976
+ # Convert the ISO8601 strings to pandas datetime objects
977
+ breaks = [pd.to_datetime(date_str) for date_str in station_relocations]
978
+ if len(breaks)==0:
979
+ logger.info('processing '+var+' without relocation')
980
+ else:
981
+ logger.info('processing '+var+' with relocation on ' + ', '.join([br.strftime('%Y-%m-%dT%H:%M:%S') for br in breaks]))
982
+
983
+ return piecewise_smoothing_and_interpolation(ds[var].to_series(), breaks)
984
+
985
+ def piecewise_smoothing_and_interpolation(data_series, breaks):
986
+ '''Smoothes, inter- or extrapolate the GPS observations. The processing is
987
+ done piecewise so that each period between station relocations are done
988
+ separately (no smoothing of the jump due to relocation). Piecewise linear
989
+ regression is then used to smooth the available observations. Then this
990
+ smoothed curve is interpolated linearly over internal gaps. Eventually, this
991
+ interpolated curve is extrapolated linearly for timestamps before the first
992
+ valid measurement and after the last valid measurement.
993
+
994
+ Parameters
995
+ ----------
996
+ data_series : pandas.Series
997
+ Series of observed latitude, longitude or elevation with datetime index.
998
+ breaks: list
999
+ List of timestamps of station relocation. First and last item should be
1000
+ None so that they can be used in slice(breaks[i], breaks[i+1])
1001
+
1002
+ Returns
1003
+ -------
1004
+ np.ndarray
1005
+ Smoothed and interpolated values corresponding to the input series.
1006
+ '''
1007
+ df_all = pd.Series(dtype=float) # Initialize an empty Series to gather all smoothed pieces
1008
+ breaks = [None] + breaks + [None]
1009
+ _inferred_series = []
1010
+ for i in range(len(breaks) - 1):
1011
+ df = data_series.loc[slice(breaks[i], breaks[i+1])]
1012
+
1013
+ # Drop NaN values and calculate the number of segments based on valid data
1014
+ df_valid = df.dropna()
1015
+ if df_valid.shape[0] > 2:
1016
+ # Fit linear regression model to the valid data range
1017
+ x = pd.to_numeric(df_valid.index).values.reshape(-1, 1)
1018
+ y = df_valid.values.reshape(-1, 1)
1019
+
1020
+ model = LinearRegression()
1021
+ model.fit(x, y)
1022
+
1023
+ # Predict using the model for the entire segment range
1024
+ x_pred = pd.to_numeric(df.index).values.reshape(-1, 1)
1025
+
1026
+ y_pred = model.predict(x_pred)
1027
+ df = pd.Series(y_pred.flatten(), index=df.index)
1028
+ # adds to list the predicted values for the current segment
1029
+ _inferred_series.append(df)
1030
+
1031
+ df_all = pd.concat(_inferred_series)
1032
+
1033
+ # Fill internal gaps with linear interpolation
1034
+ df_all = df_all.interpolate(method='linear', limit_area='inside')
1035
+
1036
+ # Remove duplicate indices and return values as numpy array
1037
+ df_all = df_all[~df_all.index.duplicated(keep='last')]
1038
+ return df_all.values
1039
+
1040
+ def calculate_tubulent_heat_fluxes(T_0, T_h, Tsurf_h, WS_h, z_WS, z_T, q_h, p_h,
124
1041
  kappa=0.4, WS_lim=1., z_0=0.001, g=9.82, es_0=6.1071, eps=0.622,
125
1042
  gamma=16., L_sub=2.83e6, L_dif_max=0.01, c_pd=1005., aa=0.7,
126
- bb=0.75, cc=5., dd=0.35):
1043
+ bb=0.75, cc=5., dd=0.35, R_d=287.05):
127
1044
  '''Calculate latent and sensible heat flux using the bulk calculation
128
1045
  method
129
1046
 
130
1047
  Parameters
131
1048
  ----------
132
1049
  T_0 : int
133
- Steam point temperature
1050
+ Freezing point temperature
134
1051
  T_h : xarray.DataArray
135
1052
  Air temperature
136
1053
  Tsurf_h : xarray.DataArray
@@ -143,8 +1060,6 @@ def calcHeatFlux(T_0, T_h, Tsurf_h, rho_atm, WS_h, z_WS, z_T, nu, q_h, p_h,
143
1060
  Height of anemometer
144
1061
  z_T : float
145
1062
  Height of thermometer
146
- nu : float
147
- Kinematic viscosity of air
148
1063
  q_h : xarray.DataArray
149
1064
  Specific humidity
150
1065
  p_h : xarray.DataArray
@@ -159,7 +1074,7 @@ def calcHeatFlux(T_0, T_h, Tsurf_h, rho_atm, WS_h, z_WS, z_T, nu, q_h, p_h,
159
1074
  g : int
160
1075
  Gravitational acceleration (m/s2). Default is 9.82.
161
1076
  es_0 : int
162
- Saturation vapour pressure at the melting point (hPa). Default is 6.1071.
1077
+ Saturation vapour pressure at the melting point (hPa). Default is 6.1071.
163
1078
  eps : int
164
1079
  Ratio of molar masses of vapor and dry air (0.622).
165
1080
  gamma : int
@@ -182,6 +1097,8 @@ def calcHeatFlux(T_0, T_h, Tsurf_h, rho_atm, WS_h, z_WS, z_T, nu, q_h, p_h,
182
1097
  dd : int
183
1098
  Flux profile correction constants (Holtslag & De Bruin '88). Default is
184
1099
  0.35.
1100
+ R_d : int
1101
+ Gas constant of dry air. Default is 287.05.
185
1102
 
186
1103
  Returns
187
1104
  -------
@@ -190,6 +1107,9 @@ def calcHeatFlux(T_0, T_h, Tsurf_h, rho_atm, WS_h, z_WS, z_T, nu, q_h, p_h,
190
1107
  LHF_h : xarray.DataArray
191
1108
  Latent heat flux
192
1109
  '''
1110
+ rho_atm = 100 * p_h / R_d / (T_h + T_0) # Calculate atmospheric density
1111
+ nu = calculate_viscosity(T_h, T_0, rho_atm) # Calculate kinematic viscosity
1112
+
193
1113
  SHF_h = xr.zeros_like(T_h) # Create empty xarrays
194
1114
  LHF_h = xr.zeros_like(T_h)
195
1115
  L = xr.full_like(T_h, 1E5)
@@ -275,10 +1195,14 @@ def calcHeatFlux(T_0, T_h, Tsurf_h, rho_atm, WS_h, z_WS, z_T, nu, q_h, p_h,
275
1195
  # If n_elements(where(L_dif > L_dif_max)) eq 1 then break
276
1196
  if np.all(L_dif <= L_dif_max):
277
1197
  break
278
-
1198
+
1199
+ HF_nan = np.isnan(p_h) | np.isnan(T_h) | np.isnan(Tsurf_h) \
1200
+ | np.isnan(q_h) | np.isnan(WS_h) | np.isnan(z_T)
1201
+ SHF_h[HF_nan] = np.nan
1202
+ LHF_h[HF_nan] = np.nan
279
1203
  return SHF_h, LHF_h
280
1204
 
281
- def calcVisc(T_h, T_0, rho_atm):
1205
+ def calculate_viscosity(T_h, T_0, rho_atm):
282
1206
  '''Calculate kinematic viscosity of air
283
1207
 
284
1208
  Parameters
@@ -301,9 +1225,8 @@ def calcVisc(T_h, T_0, rho_atm):
301
1225
  # Kinematic viscosity of air in m^2/s
302
1226
  return mu / rho_atm
303
1227
 
304
- def calcHumid(T_0, T_100, T_h, es_0, es_100, eps, p_h, RH_cor_h):
1228
+ def calculate_specific_humidity(T_0, T_100, T_h, p_h, RH_cor_h, es_0=6.1071, es_100=1013.246, eps=0.622):
305
1229
  '''Calculate specific humidity
306
-
307
1230
  Parameters
308
1231
  ----------
309
1232
  T_0 : float
@@ -312,16 +1235,16 @@ def calcHumid(T_0, T_100, T_h, es_0, es_100, eps, p_h, RH_cor_h):
312
1235
  Steam point temperature in Kelvin
313
1236
  T_h : xarray.DataArray
314
1237
  Air temperature
315
- eps : int
316
- ratio of molar masses of vapor and dry air (0.622)
317
- es_0 : float
318
- Saturation vapour pressure at the melting point (hPa)
319
- es_100 : float
320
- Saturation vapour pressure at steam point temperature (hPa)
321
1238
  p_h : xarray.DataArray
322
1239
  Air pressure
323
1240
  RH_cor_h : xarray.DataArray
324
1241
  Relative humidity corrected
1242
+ es_0 : float
1243
+ Saturation vapour pressure at the melting point (hPa)
1244
+ es_100 : float
1245
+ Saturation vapour pressure at steam point temperature (hPa)
1246
+ eps : int
1247
+ ratio of molar masses of vapor and dry air (0.622)
325
1248
 
326
1249
  Returns
327
1250
  -------
@@ -346,86 +1269,11 @@ def calcHumid(T_0, T_100, T_h, es_0, es_100, eps, p_h, RH_cor_h):
346
1269
  freezing = T_h < 0
347
1270
  q_sat[freezing] = eps * es_ice[freezing] / (p_h[freezing] - (1 - eps) * es_ice[freezing])
348
1271
 
1272
+ q_nan = np.isnan(T_h) | np.isnan(p_h)
1273
+ q_sat[q_nan] = np.nan
1274
+
349
1275
  # Convert to kg/kg
350
1276
  return RH_cor_h * q_sat / 100
351
-
352
- def cleanHeatFlux(SHF, LHF, T, Tsurf, p, WS, RH_cor, z_boom):
353
- '''Find invalid heat flux data values and replace with NaNs, based on
354
- air temperature, surface temperature, air pressure, wind speed,
355
- corrected relative humidity, and boom height
356
-
357
- Parameters
358
- ----------
359
- SHF : xarray.DataArray
360
- Sensible heat flux
361
- LHF : xarray.DataArray
362
- Latent heat flux
363
- T : xarray.DataArray
364
- Air temperature
365
- Tsurf : xarray.DataArray
366
- Surface temperature
367
- p : xarray.DataArray
368
- Air pressure
369
- WS : xarray.DataArray
370
- Wind speed
371
- RH_cor : xarray.DataArray
372
- Relative humidity corrected
373
- z_boom : xarray.DataArray
374
- Boom height
375
-
376
- Returns
377
- -------
378
- SHF : xarray.DataArray
379
- Sensible heat flux corrected
380
- LHF : xarray.DataArray
381
- Latent heat flux corrected
382
- '''
383
- HF_nan = np.isnan(p) | np.isnan(T) | np.isnan(Tsurf) \
384
- | np.isnan(RH_cor) | np.isnan(WS) | np.isnan(z_boom)
385
- SHF[HF_nan] = np.nan
386
- LHF[HF_nan] = np.nan
387
- return SHF, LHF
388
-
389
- def cleanSpHumid(q_h, T, Tsurf, p, RH_cor):
390
- '''Find invalid specific humidity data values and replace with NaNs,
391
- based on air temperature, surface temperature, air pressure,
392
- and corrected relative humidity
393
-
394
- Parameters
395
- ----------
396
- q_h : xarray.DataArray
397
- Specific humidity
398
- T : xarray.DataArray
399
- Air temperature
400
- Tsurf : xarray.DataArray
401
- Surface temperature
402
- p : xarray.DataArray
403
- Air pressure
404
- RH_cor : xarray.DataArray
405
- Relative humidity corrected
406
-
407
- Returns
408
- -------
409
- q_h : xarray.DataArray
410
- Specific humidity corrected'''
411
- q_nan = np.isnan(T) | np.isnan(RH_cor) | np.isnan(p) | np.isnan(Tsurf)
412
- q_h[q_nan] = np.nan
413
- return q_h
414
-
415
-
416
- def _calcAtmosDens(p_h, R_d, T_h, T_0): # TODO: check this shouldn't be in this step somewhere
417
- '''Calculate atmospheric density'''
418
- return 100 * p_h / R_d / (T_h + T_0)
419
-
420
- def _getTempK(T_0):
421
- '''Return steam point temperature in K'''
422
- return T_0+100
423
-
424
- def _getRotation():
425
- '''Return degrees-to-radians and radians-to-degrees'''
426
- deg2rad = np.pi / 180
427
- rad2deg = 1 / deg2rad
428
- return deg2rad, rad2deg
429
1277
 
430
1278
  if __name__ == "__main__":
431
1279
  # unittest.main()