pypromice 1.5.3__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/io/bufr/__init__.py +0 -0
  15. pypromice/{postprocess → io/bufr}/bufr_to_csv.py +1 -1
  16. pypromice/{postprocess → io/bufr}/create_bufr_files.py +2 -2
  17. pypromice/{postprocess → io/bufr}/get_bufr.py +6 -6
  18. pypromice/{postprocess → io/bufr}/real_time_utilities.py +3 -3
  19. pypromice/io/ingest/__init__.py +0 -0
  20. pypromice/{utilities → io/ingest}/git.py +1 -3
  21. pypromice/io/ingest/l0.py +294 -0
  22. pypromice/io/ingest/l0_repository.py +103 -0
  23. pypromice/io/ingest/toa5.py +87 -0
  24. pypromice/{process → io}/write.py +1 -1
  25. pypromice/pipeline/L0toL1.py +291 -0
  26. pypromice/pipeline/L1toL2.py +233 -0
  27. pypromice/{process → pipeline}/L2toL3.py +97 -118
  28. pypromice/pipeline/__init__.py +4 -0
  29. pypromice/{process → pipeline}/aws.py +10 -82
  30. pypromice/{process → pipeline}/get_l2.py +2 -2
  31. pypromice/{process → pipeline}/get_l2tol3.py +19 -22
  32. pypromice/{process → pipeline}/join_l2.py +31 -32
  33. pypromice/{process → pipeline}/join_l3.py +16 -14
  34. pypromice/{process → pipeline}/resample.py +58 -45
  35. pypromice/{process → pipeline}/utilities.py +0 -22
  36. pypromice/resources/file_attributes.csv +4 -4
  37. pypromice/resources/variables.csv +27 -24
  38. {pypromice-1.5.3.dist-info → pypromice-1.6.0.dist-info}/METADATA +1 -2
  39. pypromice-1.6.0.dist-info/RECORD +64 -0
  40. pypromice-1.6.0.dist-info/entry_points.txt +12 -0
  41. pypromice/get/__init__.py +0 -1
  42. pypromice/get/get.py +0 -211
  43. pypromice/get/get_promice_data.py +0 -56
  44. pypromice/process/L0toL1.py +0 -564
  45. pypromice/process/L1toL2.py +0 -824
  46. pypromice/process/__init__.py +0 -4
  47. pypromice/process/load.py +0 -161
  48. pypromice-1.5.3.dist-info/RECORD +0 -54
  49. pypromice-1.5.3.dist-info/entry_points.txt +0 -13
  50. /pypromice/{postprocess → core}/__init__.py +0 -0
  51. /pypromice/{utilities → core}/dependency_graph.py +0 -0
  52. /pypromice/{qc → core/qc}/__init__.py +0 -0
  53. /pypromice/{qc → core/qc}/github_data_issues.py +0 -0
  54. /pypromice/{qc → core/qc}/percentiles/__init__.py +0 -0
  55. /pypromice/{qc → core/qc}/percentiles/outlier_detector.py +0 -0
  56. /pypromice/{qc → core/qc}/percentiles/thresholds.csv +0 -0
  57. /pypromice/{process → core/variables}/wind.py +0 -0
  58. /pypromice/{utilities → io}/__init__.py +0 -0
  59. /pypromice/{postprocess → io/bufr}/bufr_utilities.py +0 -0
  60. /pypromice/{postprocess → io/bufr}/positions_seed.csv +0 -0
  61. /pypromice/{station_configuration.py → io/bufr/station_configuration.py} +0 -0
  62. /pypromice/{postprocess → io}/make_metadata_csv.py +0 -0
  63. {pypromice-1.5.3.dist-info → pypromice-1.6.0.dist-info}/WHEEL +0 -0
  64. {pypromice-1.5.3.dist-info → pypromice-1.6.0.dist-info}/licenses/LICENSE.txt +0 -0
  65. {pypromice-1.5.3.dist-info → pypromice-1.6.0.dist-info}/top_level.txt +0 -0
pypromice/__init__.py CHANGED
@@ -0,0 +1,2 @@
1
+ from importlib.metadata import version
2
+ __version__ = version("pypromice")
@@ -3,10 +3,10 @@ from datetime import datetime
3
3
 
4
4
  import pandas as pd
5
5
 
6
- from pypromice.process import AWS
6
+ from pypromice.pipeline.aws import AWS
7
7
  from pathlib import Path
8
8
  import logging
9
- from pypromice.qc.github_data_issues import adjustTime, flagNAN, adjustData
9
+ from pypromice.core.qc.github_data_issues import adjustTime, flagNAN, adjustData
10
10
 
11
11
 
12
12
  # %%
@@ -19,27 +19,22 @@ DEFAULT_VARIABLE_THRESHOLDS = {
19
19
  "t_i": {"max_diff": 0.0001, "period": 2},
20
20
  "t_u": {"max_diff": 0.0001, "period": 2},
21
21
  "t_l": {"max_diff": 0.0001, "period": 2},
22
- "p_i": {"max_diff": 0.0001, "period": 2},
23
- # "p_u": {"max_diff": 0.0001, "period": 2},
24
- # "p_l": {"max_diff": 0.0001, "period": 2},
25
- "gps_lat_lon": {
26
- "max_diff": 0.000001,
27
- "period": 6,
28
- }, # gets special handling to remove simultaneously constant gps_lat and gps_lon
22
+
23
+ "p_i": {"max_diff": 0.0001, "period": 3},
24
+ "p_u": {"max_diff": 0.0001, "period": 150},
25
+ "p_l": {"max_diff": 0.0001, "period": 150},
26
+
27
+ # gets special handling to remove simultaneously constant gps_lat and gps_lon
28
+ "gps_lat_lon": {"max_diff": 0.000001, "period": 6},
29
+
29
30
  "gps_alt": {"max_diff": 0.0001, "period": 6},
30
31
  "t_rad": {"max_diff": 0.0001, "period": 2},
31
- "rh_i": {
32
- "max_diff": 0.0001,
33
- "period": 2,
34
- }, # gets special handling to allow constant 100%
35
- "rh_u": {
36
- "max_diff": 0.0001,
37
- "period": 2,
38
- }, # gets special handling to allow constant 100%
39
- "rh_l": {
40
- "max_diff": 0.0001,
41
- "period": 2,
42
- }, # gets special handling to allow constant 100%
32
+
33
+ # gets special handling to allow constant 100%
34
+ "rh_i": {"max_diff": 0.0001, "period": 2},
35
+ "rh_u": {"max_diff": 0.0001, "period": 2},
36
+ "rh_l": {"max_diff": 0.0001, "period": 2},
37
+
43
38
  "wspd_i": {"max_diff": 0.0001, "period": 6},
44
39
  "wspd_u": {"max_diff": 0.0001, "period": 6},
45
40
  "wspd_l": {"max_diff": 0.0001, "period": 6},
@@ -83,15 +78,11 @@ def persistence_qc(
83
78
  variable_thresholds = DEFAULT_VARIABLE_THRESHOLDS
84
79
  logger.debug(f"Running persistence_qc using {variable_thresholds}")
85
80
  else:
86
- logger.info(f"Running persistence_qc using custom thresholds:\n {variable_thresholds}")
81
+ logger.info(f"Running persistence_qc using custom thresholds:\n {variable_thresholds}")
87
82
 
88
83
  for k in variable_thresholds.keys():
89
84
  if k in ["t", "p", "rh", "wspd", "wdir", "z_boom"]:
90
- var_all = [
91
- k + "_u",
92
- k + "_l",
93
- k + "_i",
94
- ] # apply to upper, lower boom, and instant
85
+ var_all = [k + l for l in ["_u", "_l", "_i"]] # apply to upper, lower boom, and instant
95
86
  else:
96
87
  var_all = [k]
97
88
  max_diff = variable_thresholds[k]["max_diff"] # loading persistent limit
@@ -140,10 +131,12 @@ def find_persistent_regions(
140
131
  """
141
132
  Algorithm that ensures values can stay the same within the outliers_mask
142
133
  """
143
- consecutive_true_df = count_consecutive_persistent_values(data, max_diff)
144
- persistent_regions = consecutive_true_df >= min_repeats
145
- # Ignore entries which already nan in the input data
146
- persistent_regions[data.isna()] = False
134
+ consecutive_true_df = count_consecutive_persistent_values(data, max_diff)
135
+ persistent_regions = consecutive_true_df >= min_repeats
136
+ for i in range(1, min_repeats):
137
+ persistent_regions |= persistent_regions.shift(-1, fill_value=False)
138
+ # Ignore entries which already nan in the input data
139
+ persistent_regions[data.isna()] = False
147
140
  return persistent_regions
148
141
 
149
142
 
@@ -2,7 +2,7 @@ import numpy as np
2
2
  import pandas
3
3
  import xarray
4
4
 
5
- from pypromice.utilities.dependency_graph import DependencyGraph
5
+ from pypromice.core.dependency_graph import DependencyGraph
6
6
 
7
7
 
8
8
  def clip_values(
@@ -24,11 +24,11 @@ def clip_values(
24
24
  ds : `xarray.Dataset`
25
25
  Dataset with clipped data
26
26
  """
27
- cols = ["lo", "hi", "OOL"]
27
+ cols = ["lo", "hi", "dependent_variables"]
28
28
  assert set(cols) <= set(var_configurations.columns)
29
29
 
30
30
  variable_limits = var_configurations[cols].assign(
31
- dependents=lambda df: df.OOL.fillna("").str.split(),
31
+ dependents=lambda df: df.dependent_variables.fillna("").str.split(),
32
32
  # Find the closure of dependents using the DependencyGraph class
33
33
  dependents_closure=lambda df: DependencyGraph.from_child_mapping(
34
34
  df.dependents
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,64 @@
1
+ __all__=["clip_and_interpolate", "get_cloud_coefficients"]
2
+
3
+ import pandas as pd
4
+ import xarray as xr
5
+
6
+ T_0=273.15 # degrees Celsius to Kelvin conversion
7
+ eps_overcast = 1.0 # Clouds overcast default coefficient
8
+ eps_clear = 9.36508e-6 # Clouds clear default coefficient
9
+
10
+ def clip_and_interpolate(temp : xr.DataArray,
11
+ lo : float,
12
+ hi : float,
13
+ max_interp : pd.Timedelta = pd.Timedelta(12,'h')
14
+ ) -> xr.DataArray:
15
+ """Clip and interpolate temperature dataset for use in
16
+ corrections
17
+
18
+ Parameters
19
+ ----------
20
+ temp : `xr.DataArray`
21
+ Array of temperature data
22
+ lo : float
23
+ Minimum threshold value for clipping
24
+ hi : float
25
+ Maximum threshold value for clipping
26
+ max_interp : `pd.Timedelta`
27
+ Maximum time steps to interpolate across.
28
+ The default is 12 hours.
29
+
30
+ Returns
31
+ -------
32
+ temp_interp : `xr.DataArray`
33
+ Array of interpolated temperature data
34
+ """
35
+ # Clip values to high and low threshold values
36
+ temp = temp.where((temp >= lo) & (temp <= hi))
37
+
38
+ # Drop duplicates and interpolate across NaN values
39
+ temp_interp = temp.interpolate_na(dim='time',
40
+ max_gap=max_interp)
41
+
42
+ return temp_interp
43
+
44
+
45
+ def get_cloud_coefficients(temp: xr.DataArray
46
+ ) -> tuple[xr.DataArray, xr.DataArray]:
47
+ """Get overcast and clear cloud longwave coefficients using
48
+ air temperature, based on assumptions from Swinbank (1963)
49
+
50
+ Parameters
51
+ ----------
52
+ temp : xr.DataArray
53
+ Air temperature
54
+
55
+ Returns
56
+ -------
57
+ LR_overcast : xr.DataArray
58
+ Overcast cloud coefficients, using overcast cloud assumption from Swinbank (1963)
59
+ LR_clear : xr.DataArray
60
+ Clear cloud coefficients, using clear cloud assumption, from Swinbank (1963)
61
+ """
62
+ LR_overcast = eps_overcast * 5.67e-8 * (temp + T_0) ** 4
63
+ LR_clear = eps_clear * 5.67e-8 * (temp + T_0) ** 6
64
+ return LR_overcast, LR_clear
@@ -0,0 +1,221 @@
1
+ __all__ = ["decode_and_convert", "filter",
2
+ "decode", "convert_from_degrees_and_decimal_minutes",
3
+ "convert_from_decimal_minutes"]
4
+ import re
5
+ import xarray as xr
6
+ import numpy as np
7
+ import pandas as pd
8
+ from sklearn.linear_model import LinearRegression
9
+
10
+ import logging
11
+ logger = logging.getLogger(__name__)
12
+
13
+ def decode_and_convert(gps_lat: xr.DataArray,
14
+ gps_lon: xr.DataArray,
15
+ gps_time: xr.DataArray,
16
+ latitude: float,
17
+ longitude: float
18
+ ) -> tuple[xr.DataArray,xr.DataArray,xr.DataArray]:
19
+ """Decode and convert GPS latitude, longtitude and time values."flag_decimal_minutes",
20
+ "flag_for_decoding"
21
+ Decoding is performed if values are detected as string types.
22
+ Conversion consists of transforming to decimal degrees (DD),
23
+ from either decimal minutes (mm.mmmmm) or degrees and
24
+ decimal minutes (ddmm.mmmm)
25
+
26
+ Parameters
27
+ ----------
28
+ gps_lat : `xr.DataArray`
29
+ GPS latitude
30
+ gps_lon : `xr.DataArray`
31
+ GPS longitude
32
+ gps_time : `xr.DataArray`
33
+ GPS time
34
+
35
+ Returns
36
+ -------
37
+ gps_lat : `xr.DataArray`
38
+ Decoded and converted GPS latitude
39
+ gps_lon : `xr.DataArray`
40
+ Decoded and converted GPS longitude
41
+ gps_time : `xr.DataArray`
42
+ Decoded and converted GPS time
43
+ """
44
+ # Retain GPS array attributes
45
+ lat_attrs = gps_lat.attrs
46
+ lon_attrs = gps_lon.attrs
47
+ time_attrs = gps_time.attrs
48
+
49
+ # Decode GPS information if array is an object array
50
+ if gps_lat.dtype.kind == "O":
51
+ lat, lon, time = decode(gps_lat, gps_lon, gps_time)
52
+ if lat is None:
53
+ logger.warning("GPS decoding failed, skipping this routine.")
54
+ else:
55
+ gps_lat, gps_lon, gps_time = lat, lon, time
56
+
57
+ # Reformat values to numeric
58
+ gps_lat.values = pd.to_numeric(gps_lat, errors='coerce')
59
+ gps_lon.values = pd.to_numeric(gps_lon, errors='coerce')
60
+ gps_time.values = pd.to_numeric(gps_time, errors='coerce')
61
+
62
+ # Convert GPS positions to decimal degrees
63
+ if np.any((gps_lat <= 90) & (gps_lat > 0)):
64
+ gps_lat = convert_from_decimal_minutes(gps_lat, latitude)
65
+ gps_lon = convert_from_decimal_minutes(gps_lon, longitude)
66
+ else:
67
+ gps_lat = convert_from_degrees_and_decimal_minutes(gps_lat)
68
+ gps_lon = convert_from_degrees_and_decimal_minutes(gps_lon)
69
+
70
+ # Reassign GPS array attributes
71
+ gps_lat.attrs = lat_attrs
72
+ gps_lon.attrs = lon_attrs
73
+ gps_time.attrs = time_attrs
74
+
75
+ return gps_lat, gps_lon, gps_time
76
+
77
+
78
+ def filter(gps_lat: xr.DataArray,
79
+ gps_lon: xr.DataArray,
80
+ gps_alt: xr.DataArray
81
+ ) -> tuple[xr.DataArray, xr.DataArray, xr.DataArray]:
82
+ """ Filter GPS latitude, longitude and altitude based on the difference
83
+ to a baseline elevation. The baseline elevation is a gap-filled monthly
84
+ median elevation based on the inputted GPS altitude.
85
+
86
+ Parameters
87
+ ----------
88
+ gps_lat : xr.DataArray
89
+ GPS latitude
90
+ gps_lon : xr.DataArray
91
+ GPS longitude
92
+ gps_alt : xr.DataArray
93
+ GPS altitude values with a time dimension
94
+
95
+ Returns
96
+ ----------
97
+ gps_lat_filtered : xr.DataArray
98
+ Filtered latitude values
99
+ gps_lon_filtered : xr.DataArray
100
+ Filtered longitude values
101
+ gps_alt_filtered : xr.DataArray
102
+ Filtered altitude values
103
+ """
104
+ # Get altitude monthly median (at month start)
105
+ # This will serve as baseline elevations for filtering
106
+ ser = gps_alt.to_series()
107
+ monthly_median = ser.resample("MS").median()
108
+ baseline_elevation = (
109
+ monthly_median
110
+ .reindex(ser.index, method="nearest")
111
+ .ffill()
112
+ .bfill()
113
+ )
114
+
115
+ # Produce conditional mask
116
+ mask = (np.abs(gps_alt - baseline_elevation) < 100) | gps_alt.isnull()
117
+
118
+ # Apply mask
119
+ gps_lat_filtered = gps_lat.where(mask)
120
+ gps_lon_filtered = gps_lon.where(mask)
121
+ gps_alt_filtered = gps_alt.where(mask)
122
+
123
+ return gps_lat_filtered, gps_lon_filtered, gps_alt_filtered
124
+
125
+
126
+ def convert_from_degrees_and_decimal_minutes(gps):
127
+ """Convert positions (i.e. latitude, longitude) from degrees
128
+ and decimal minutes (ddmm.mmmm) to decimal degree values (DD)"""
129
+ return np.floor(gps / 100) + (gps / 100 - np.floor(gps / 100)) * 100 / 60
130
+
131
+
132
+ def convert_from_decimal_minutes(gps: xr.DataArray, pos: float
133
+ ) -> xr.DataArray:
134
+ """Convert decimal minutes (mm.mmmmm) to decimal degree
135
+ values (DD), using a predefined position to append values to.
136
+ Needed in the case of PROMICE v1 stations, where logger
137
+ programs saved positions only in decimal minutes."""
138
+ new_gps = np.sign(pos) * (gps + 100 * np.floor(np.abs(pos)))
139
+ return convert_from_degrees_and_decimal_minutes(new_gps)
140
+
141
+
142
+ def decode(gps_lat: xr.DataArray,
143
+ gps_lon: xr.DataArray,
144
+ gps_time: xr.DataArray
145
+ ) -> tuple[xr.DataArray,xr.DataArray,xr.DataArray]:
146
+ """Decode GPS information. This should be applied if gps information
147
+ consists of strings and not float values. GPS information is returned in
148
+ decimal degrees (ddmm.mmmm) format.
149
+
150
+ Parameters
151
+ ----------
152
+ gps_lat : `xr.DataArray`
153
+ GPS latitude
154
+ gps_lon : `xr.DataArray`
155
+ GPS longitude
156
+ gps_time : `xr.DataArray`
157
+ GPS time
158
+
159
+ Returns
160
+ -------
161
+ new_lat : `xr.DataArray`
162
+ Decoded GPS latitude
163
+ new_lon : `xr.DataArray`
164
+ Decoded GPS longitude
165
+ new_time : `xr.DataArray`
166
+ Decoded GPS time
167
+ """
168
+ # Pick the first non-null sample safely and detect decoding format
169
+ non_null = gps_lat.dropna(dim='time').values
170
+ sample_value = str(non_null[0])
171
+
172
+ try:
173
+ # Object decoding
174
+ if "NH" in sample_value:
175
+ new_lat = gps_object_decoder(gps_lat)
176
+ new_lon = gps_object_decoder(gps_lon)
177
+ new_time = gps_object_decoder(gps_time)
178
+ return new_lat, new_lon, new_time
179
+
180
+ # L-string decoding
181
+ elif "L" in sample_value:
182
+ logger.info("Found 'L' in GPS string; applying decode + scaling.")
183
+ new_lat = gps_l_string_decoder(gps_lat)
184
+ new_lon = gps_l_string_decoder(gps_lon)
185
+ new_time = gps_object_decoder(gps_time)
186
+ return new_lat, new_lon, new_time
187
+
188
+ # Unknown format, attempt to decode
189
+ else:
190
+ logger.info("Unknown GPS string format; attempting generic decode.")
191
+ new_lat = gps_object_decoder(gps_lat)
192
+ new_lon = gps_object_decoder(gps_lon)
193
+ new_time = gps_object_decoder(gps_time)
194
+ return new_lat, new_lon, new_time
195
+
196
+ except Exception as e:
197
+ logger.error(f"Failed to decode GPS data: {e!r} "
198
+ f"(dtype={gps_lat.dtype})")
199
+ return None, None, None
200
+
201
+
202
+ def gps_object_decoder(gps : xr.DataArray) -> xr.DataArray:
203
+ """GPS decoder for object array formatting. For example, PROMICE v2
204
+ stations should send information as 'NH6429.01544,WH04932.86061'
205
+ original formatting (NUK_L 2022); PROMICE v3 stations should send
206
+ coordinates as '6430,4916' (NUK_Uv3); and GC-Net stations should
207
+ send coordinates as '6628.93936',04617.59187' (DY2)"""
208
+ str2nums = [re.findall(r"[-+]?\d*\.\d+|\d+", _) if isinstance(_, str) else [np.nan] for _ in gps.values]
209
+ gps[:] = pd.DataFrame(str2nums).astype(float).T.values[0]
210
+ gps = gps.astype(float)
211
+ return gps
212
+
213
+
214
+ def gps_l_string_decoder(gps : xr.DataArray) -> xr.DataArray:
215
+ """GPS L-string decoder"""
216
+ # Convert from object array
217
+ gps = gps_object_decoder(gps)
218
+
219
+ # Convert from integer-like values to degrees
220
+ gps = gps/100000
221
+ return gps
@@ -0,0 +1,111 @@
1
+ __all__ = ["adjust", "convert", "calculate_specific_humidity"]
2
+
3
+ import xarray as xr
4
+ import numpy as np
5
+
6
+ # Define constants
7
+ T_0 = 273.15 # Ice point temperature (Kelvins)
8
+ T_100 = T_0+100 # Steam point temperature (Kelvins)
9
+ ews = 1013.246 # Saturation vapour pressure at steam point temperature (normal atmosphere) (hPa)
10
+ ei0 = 6.1071 # Saturation vapour pressure at ice melting point temperature (normal atmosphere) (hPa)
11
+ eps=0.622 # Ratio of molar masses of vapor and dry air
12
+
13
+ def adjust(rh: xr.DataArray,
14
+ t: xr.DataArray
15
+ ) -> xr.DataArray:
16
+ """Correct relative humidity so that values are given with respect to
17
+ saturation over ice in subfreezing conditions, and with respect to
18
+ saturation over water (as given by the instrument) above the melting
19
+ point temperature. Saturation water vapors are calculated after
20
+ Groff & Gratch method.
21
+
22
+ Parameters
23
+ ----------
24
+ rh : xr.DataArray
25
+ Relative humidity
26
+ t : xr.DataArray
27
+ Air temperature
28
+
29
+ Returns
30
+ -------
31
+ rh_wrt_ice_or_water : xr.DataArray
32
+ CAdjusted relative humidity
33
+ """
34
+ # Convert to hPa (Groff & Gratch)
35
+ e_s_wtr = 10**(-7.90298 * (T_100 / (t + T_0) - 1)
36
+ + 5.02808 * np.log10(T_100 / (t + T_0))
37
+ - 1.3816E-7 * (10**(11.344 * (1 - (t + T_0) / T_100)) - 1)
38
+ + 8.1328E-3 * (10**(-3.49149 * (T_100/(t + T_0) - 1)) -1)
39
+ + np.log10(ews))
40
+ e_s_ice = 10**(-9.09718 * (T_0 / (t + T_0) - 1)
41
+ - 3.56654 * np.log10(T_0 / (t + T_0))
42
+ + 0.876793 * (1 - (t + T_0) / T_0)
43
+ + np.log10(ei0))
44
+
45
+ # Define freezing point. Why > -100?
46
+ nan_mask = t.notnull()
47
+ freezing = (t < 0) & (t > -100) & nan_mask
48
+
49
+ # Set to Groff & Gratch values when freezing, otherwise just rh
50
+ rh_wrt_ice_or_water = rh.where(~freezing & nan_mask,
51
+ other=rh*(e_s_wtr/e_s_ice))
52
+ return rh_wrt_ice_or_water
53
+
54
+
55
+ def calculate_specific_humidity(t, p, rh_wrt_ice_or_water):
56
+ """Calculate specific humidity.
57
+
58
+ Parameters
59
+ ----------
60
+ t : xr.DataArray
61
+ Air temperature
62
+ p : xr.DataArray
63
+ Air pressure
64
+ rh_wrt_ice_or_water : xr.DataArray
65
+ Adjusted relative humidity
66
+
67
+ Returns
68
+ -------
69
+ xr.DataArray
70
+ Specific humidity (kg/kg)
71
+ """
72
+ # Saturation vapour pressure above 0 C (hPa)
73
+ es_wtr = 10**(-7.90298 * (T_100 / (t + T_0) - 1) + 5.02808 * np.log10(T_100 / (t + T_0))
74
+ - 1.3816E-7 * (10**(11.344 * (1 - (t + T_0) / T_100)) - 1)
75
+ + 8.1328E-3 * (10**(-3.49149 * (T_100 / (t + T_0) -1)) - 1) + np.log10(ews))
76
+
77
+ # Saturation vapour pressure below 0 C (hPa)
78
+ es_ice = 10**(-9.09718 * (T_0 / (t + T_0) - 1) - 3.56654
79
+ * np.log10(T_0 / (t + T_0)) + 0.876793
80
+ * (1 - (t + T_0) / T_0)
81
+ + np.log10(ei0))
82
+
83
+ # Specific humidity at saturation (incorrect below melting point)
84
+ q_sat = eps * es_wtr / (p - (1 - eps) * es_wtr)
85
+
86
+ # Replace saturation specific humidity values below melting point
87
+ freezing = t < 0
88
+ q_sat[freezing] = eps * es_ice[freezing] / (p[freezing] - (1 - eps) * es_ice[freezing])
89
+
90
+ # Mask where temperature or pressure are null values
91
+ q_nan = np.isnan(t) | np.isnan(p)
92
+ q_sat[q_nan] = np.nan
93
+
94
+ # Convert to kg/kg
95
+ return rh_wrt_ice_or_water * q_sat / 100
96
+
97
+ def convert(qh: xr.DataArray
98
+ ) -> xr.DataArray:
99
+ """Convert specific humidity from kg/kg to g/kg units
100
+
101
+ Parameters
102
+ ----------
103
+ qh : xr.DataArray
104
+ Specific humidity (kg/kg)
105
+
106
+ Returns
107
+ -------
108
+ xr.DataArray
109
+ Specific humidity (g/kg)
110
+ """
111
+ return 1000 * qh
@@ -0,0 +1,108 @@
1
+ __all__ = ["correct_rainfall_undercatch", "get_rainfall_per_timestep", "filter_lufft_errors"]
2
+
3
+ import numpy as np
4
+ import xarray as xr
5
+
6
+
7
+ def filter_lufft_errors(
8
+ precip: xr.DataArray, t: xr.DataArray, p: xr.DataArray, rh: xr.DataArray
9
+ ) -> xr.DataArray:
10
+ """Filter precipitation measurements where air temperature, pressure, or
11
+ relative humidity measurements are null values. This assumes that
12
+ air temperature, air pressure, relative humidity and precipitation
13
+ measurements are measured using the same instrument, e.g. a
14
+ lufft instrument.
15
+
16
+ Parameters
17
+ ----------
18
+ precip : xr.DataArray
19
+ Cumulative precipitation measurements
20
+ t : xr.DataArray
21
+ Air temperature measurements
22
+ p : xr.DataArray
23
+ Air pressure measurements
24
+ rh : xr.DataArray
25
+ Relative humidity measurements
26
+
27
+ Returns
28
+ -------
29
+ xr.DataArray
30
+ Filtered precipitation values
31
+ """
32
+ mask = (t.isnull() | p.isnull() | rh.isnull()) & (precip == 0)
33
+ return precip.where(~mask)
34
+
35
+
36
+ def correct_rainfall_undercatch(
37
+ rainfall_per_timestep: xr.DataArray, wspd: xr.DataArray
38
+ ) -> xr.DataArray:
39
+ """Corrects rainfall amount per timestep for undercatch as in
40
+ Yang et al. (1999) and Box et al. (2022), based on Goodison et al. (1998).
41
+
42
+ Yang, D., Ishida, S., Goodison, B. E., and Gunther, T.: Bias correction of
43
+ daily precipitation measurements for Greenland,
44
+ https://doi.org/10.1029/1998jd200110, 1999.
45
+
46
+ Box, J., Wehrle, A., van As, D., Fausto, R., Kjeldsen, K., Dachauer, A.,
47
+ Ahlstrom, A. P., and Picard, G.: Greenland Ice Sheet rainfall, heat and
48
+ albedo feedback imapacts from the Mid-August 2021 atmospheric river,
49
+ Geophys. Res. Lett. 49 (11), e2021GL097356,
50
+ https://doi.org/10.1029/2021GL097356, 2022.
51
+
52
+ Goodison, B. E., Louie, P. Y. T., and Yang, D.: Solid Precipitation
53
+ Measurement Intercomparison, WMO, 1998
54
+
55
+ Parameters
56
+ ----------
57
+ rainfall : xr.DataArray
58
+ Uncorrected rainfall per timestep
59
+ wspd : xr.DataArray
60
+ Wind speed measurements
61
+
62
+ Returns
63
+ -------
64
+ rainfall_cor : xr.DataArray
65
+ Corrected rainfall per timestep
66
+ """
67
+
68
+ # Calculate undercatch correction factor
69
+ corr = 100 / (100.00 - 4.37 * wspd + 0.35 * wspd * wspd)
70
+
71
+ # Fix all values below 1.02 to 1.02
72
+ corr = corr.where(corr > 1.02, other=1.02)
73
+
74
+ # Apply correction to rate
75
+ rainfall_per_timestep_cor = rainfall_per_timestep * corr
76
+
77
+ return rainfall_per_timestep_cor
78
+
79
+ def get_rainfall_per_timestep(
80
+ precip: xr.DataArray,
81
+ t: xr.DataArray
82
+ ) -> xr.DataArray:
83
+ """
84
+ Derive rainfall per timestep from cumulative precipitation data.
85
+
86
+ Parameters
87
+ ----------
88
+ precip : xr.DataArray
89
+ Cumulative precipitation measurements.
90
+ t : xr.DataArray
91
+ Air temperature measurements.
92
+
93
+ Returns
94
+ -------
95
+ xr.DataArray
96
+ Rainfall per timestep with negative values removed and
97
+ cold-season precipitation (T < -2 °C) filtered out.
98
+ """
99
+ rainfall_per_timestep = precip.diff("time").reindex_like(precip)
100
+
101
+ # Removing all negative precipitation, both corrected and uncorrected
102
+ rainfall_per_timestep = rainfall_per_timestep.where(rainfall_per_timestep >= 0)
103
+
104
+ # Filtering cold season precipitation, both corrected and uncorrected
105
+ rain_in_cold = (rainfall_per_timestep > 0) & (t < -2)
106
+ rainfall_per_timestep = rainfall_per_timestep.where(~rain_in_cold)
107
+
108
+ return rainfall_per_timestep