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.
- pypromice/__init__.py +2 -0
- pypromice/{qc → core/qc}/percentiles/compute_thresholds.py +2 -2
- pypromice/{qc → core/qc}/persistence.py +22 -29
- pypromice/{process → core/qc}/value_clipping.py +3 -3
- pypromice/core/variables/__init__.py +1 -0
- pypromice/core/variables/air_temperature.py +64 -0
- pypromice/core/variables/gps.py +221 -0
- pypromice/core/variables/humidity.py +111 -0
- pypromice/core/variables/precipitation.py +108 -0
- pypromice/core/variables/pressure_transducer_depth.py +79 -0
- pypromice/core/variables/radiation.py +422 -0
- pypromice/core/variables/station_boom_height.py +49 -0
- pypromice/core/variables/station_pose.py +375 -0
- pypromice/io/bufr/__init__.py +0 -0
- pypromice/{postprocess → io/bufr}/bufr_to_csv.py +1 -1
- pypromice/{postprocess → io/bufr}/create_bufr_files.py +2 -2
- pypromice/{postprocess → io/bufr}/get_bufr.py +6 -6
- pypromice/{postprocess → io/bufr}/real_time_utilities.py +3 -3
- pypromice/io/ingest/__init__.py +0 -0
- pypromice/{utilities → io/ingest}/git.py +1 -3
- pypromice/io/ingest/l0.py +294 -0
- pypromice/io/ingest/l0_repository.py +103 -0
- pypromice/io/ingest/toa5.py +87 -0
- pypromice/{process → io}/write.py +1 -1
- pypromice/pipeline/L0toL1.py +291 -0
- pypromice/pipeline/L1toL2.py +233 -0
- pypromice/{process → pipeline}/L2toL3.py +97 -118
- pypromice/pipeline/__init__.py +4 -0
- pypromice/{process → pipeline}/aws.py +10 -82
- pypromice/{process → pipeline}/get_l2.py +2 -2
- pypromice/{process → pipeline}/get_l2tol3.py +19 -22
- pypromice/{process → pipeline}/join_l2.py +31 -32
- pypromice/{process → pipeline}/join_l3.py +16 -14
- pypromice/{process → pipeline}/resample.py +58 -45
- pypromice/{process → pipeline}/utilities.py +0 -22
- pypromice/resources/file_attributes.csv +4 -4
- pypromice/resources/variables.csv +27 -24
- {pypromice-1.5.3.dist-info → pypromice-1.6.0.dist-info}/METADATA +1 -2
- pypromice-1.6.0.dist-info/RECORD +64 -0
- pypromice-1.6.0.dist-info/entry_points.txt +12 -0
- pypromice/get/__init__.py +0 -1
- pypromice/get/get.py +0 -211
- pypromice/get/get_promice_data.py +0 -56
- pypromice/process/L0toL1.py +0 -564
- pypromice/process/L1toL2.py +0 -824
- pypromice/process/__init__.py +0 -4
- pypromice/process/load.py +0 -161
- pypromice-1.5.3.dist-info/RECORD +0 -54
- pypromice-1.5.3.dist-info/entry_points.txt +0 -13
- /pypromice/{postprocess → core}/__init__.py +0 -0
- /pypromice/{utilities → core}/dependency_graph.py +0 -0
- /pypromice/{qc → core/qc}/__init__.py +0 -0
- /pypromice/{qc → core/qc}/github_data_issues.py +0 -0
- /pypromice/{qc → core/qc}/percentiles/__init__.py +0 -0
- /pypromice/{qc → core/qc}/percentiles/outlier_detector.py +0 -0
- /pypromice/{qc → core/qc}/percentiles/thresholds.csv +0 -0
- /pypromice/{process → core/variables}/wind.py +0 -0
- /pypromice/{utilities → io}/__init__.py +0 -0
- /pypromice/{postprocess → io/bufr}/bufr_utilities.py +0 -0
- /pypromice/{postprocess → io/bufr}/positions_seed.csv +0 -0
- /pypromice/{station_configuration.py → io/bufr/station_configuration.py} +0 -0
- /pypromice/{postprocess → io}/make_metadata_csv.py +0 -0
- {pypromice-1.5.3.dist-info → pypromice-1.6.0.dist-info}/WHEEL +0 -0
- {pypromice-1.5.3.dist-info → pypromice-1.6.0.dist-info}/licenses/LICENSE.txt +0 -0
- {pypromice-1.5.3.dist-info → pypromice-1.6.0.dist-info}/top_level.txt +0 -0
pypromice/__init__.py
CHANGED
|
@@ -3,10 +3,10 @@ from datetime import datetime
|
|
|
3
3
|
|
|
4
4
|
import pandas as pd
|
|
5
5
|
|
|
6
|
-
from pypromice.
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
|
|
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
|
|
144
|
-
persistent_regions = consecutive_true_df
|
|
145
|
-
|
|
146
|
-
|
|
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.
|
|
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", "
|
|
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.
|
|
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
|