cloudnetpy 1.55.20__py3-none-any.whl → 1.55.22__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.
- cloudnetpy/categorize/atmos.py +46 -14
- cloudnetpy/categorize/atmos_utils.py +11 -1
- cloudnetpy/categorize/categorize.py +38 -21
- cloudnetpy/categorize/classify.py +31 -9
- cloudnetpy/categorize/containers.py +19 -7
- cloudnetpy/categorize/droplet.py +24 -8
- cloudnetpy/categorize/falling.py +17 -7
- cloudnetpy/categorize/freezing.py +19 -5
- cloudnetpy/categorize/insects.py +27 -14
- cloudnetpy/categorize/lidar.py +38 -36
- cloudnetpy/categorize/melting.py +19 -9
- cloudnetpy/categorize/model.py +28 -9
- cloudnetpy/categorize/mwr.py +4 -2
- cloudnetpy/categorize/radar.py +58 -22
- cloudnetpy/cloudnetarray.py +15 -6
- cloudnetpy/concat_lib.py +39 -16
- cloudnetpy/constants.py +7 -0
- cloudnetpy/datasource.py +39 -19
- cloudnetpy/instruments/basta.py +6 -2
- cloudnetpy/instruments/campbell_scientific.py +33 -16
- cloudnetpy/instruments/ceilo.py +30 -13
- cloudnetpy/instruments/ceilometer.py +76 -37
- cloudnetpy/instruments/cl61d.py +8 -3
- cloudnetpy/instruments/cloudnet_instrument.py +2 -1
- cloudnetpy/instruments/copernicus.py +27 -14
- cloudnetpy/instruments/disdrometer/common.py +51 -32
- cloudnetpy/instruments/disdrometer/parsivel.py +79 -48
- cloudnetpy/instruments/disdrometer/thies.py +10 -6
- cloudnetpy/instruments/galileo.py +23 -12
- cloudnetpy/instruments/hatpro.py +27 -11
- cloudnetpy/instruments/instruments.py +4 -1
- cloudnetpy/instruments/lufft.py +20 -11
- cloudnetpy/instruments/mira.py +60 -49
- cloudnetpy/instruments/mrr.py +31 -20
- cloudnetpy/instruments/nc_lidar.py +15 -6
- cloudnetpy/instruments/nc_radar.py +31 -22
- cloudnetpy/instruments/pollyxt.py +36 -21
- cloudnetpy/instruments/radiometrics.py +32 -18
- cloudnetpy/instruments/rpg.py +48 -22
- cloudnetpy/instruments/rpg_reader.py +39 -30
- cloudnetpy/instruments/vaisala.py +39 -27
- cloudnetpy/instruments/weather_station.py +15 -11
- cloudnetpy/metadata.py +3 -1
- cloudnetpy/model_evaluation/file_handler.py +31 -21
- cloudnetpy/model_evaluation/metadata.py +3 -1
- cloudnetpy/model_evaluation/model_metadata.py +1 -1
- cloudnetpy/model_evaluation/plotting/plot_tools.py +20 -15
- cloudnetpy/model_evaluation/plotting/plotting.py +114 -64
- cloudnetpy/model_evaluation/products/advance_methods.py +48 -28
- cloudnetpy/model_evaluation/products/grid_methods.py +44 -19
- cloudnetpy/model_evaluation/products/model_products.py +22 -18
- cloudnetpy/model_evaluation/products/observation_products.py +15 -9
- cloudnetpy/model_evaluation/products/product_resampling.py +14 -4
- cloudnetpy/model_evaluation/products/tools.py +16 -7
- cloudnetpy/model_evaluation/statistics/statistical_methods.py +28 -15
- cloudnetpy/model_evaluation/tests/e2e/conftest.py +3 -3
- cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +9 -5
- cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +14 -13
- cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +9 -5
- cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +14 -13
- cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +9 -5
- cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +14 -13
- cloudnetpy/model_evaluation/tests/unit/conftest.py +11 -11
- cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +33 -27
- cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +83 -83
- cloudnetpy/model_evaluation/tests/unit/test_model_products.py +23 -21
- cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +24 -25
- cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +40 -39
- cloudnetpy/model_evaluation/tests/unit/test_plotting.py +12 -11
- cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +30 -30
- cloudnetpy/model_evaluation/tests/unit/test_tools.py +18 -17
- cloudnetpy/model_evaluation/utils.py +3 -2
- cloudnetpy/output.py +45 -19
- cloudnetpy/plotting/plot_meta.py +35 -11
- cloudnetpy/plotting/plotting.py +172 -104
- cloudnetpy/products/classification.py +20 -8
- cloudnetpy/products/der.py +25 -10
- cloudnetpy/products/drizzle.py +41 -26
- cloudnetpy/products/drizzle_error.py +10 -5
- cloudnetpy/products/drizzle_tools.py +43 -24
- cloudnetpy/products/ier.py +10 -5
- cloudnetpy/products/iwc.py +16 -9
- cloudnetpy/products/lwc.py +34 -12
- cloudnetpy/products/mwr_multi.py +4 -1
- cloudnetpy/products/mwr_single.py +4 -1
- cloudnetpy/products/product_tools.py +33 -10
- cloudnetpy/utils.py +175 -74
- cloudnetpy/version.py +1 -1
- {cloudnetpy-1.55.20.dist-info → cloudnetpy-1.55.22.dist-info}/METADATA +11 -10
- cloudnetpy-1.55.22.dist-info/RECORD +114 -0
- docs/source/conf.py +2 -2
- cloudnetpy-1.55.20.dist-info/RECORD +0 -114
- {cloudnetpy-1.55.20.dist-info → cloudnetpy-1.55.22.dist-info}/LICENSE +0 -0
- {cloudnetpy-1.55.20.dist-info → cloudnetpy-1.55.22.dist-info}/WHEEL +0 -0
- {cloudnetpy-1.55.20.dist-info → cloudnetpy-1.55.22.dist-info}/top_level.txt +0 -0
@@ -24,10 +24,12 @@ class ClassData:
|
|
24
24
|
"""Container for observations that are used in the classification.
|
25
25
|
|
26
26
|
Args:
|
27
|
+
----
|
27
28
|
data: Containing :class:`Radar`, :class:`Lidar`, :class:`Model`
|
28
29
|
and :class:`Mwr` instances.
|
29
30
|
|
30
31
|
Attributes:
|
32
|
+
----------
|
31
33
|
z (ndarray): 2D radar echo.
|
32
34
|
ldr (ndarray): 2D linear depolarization ratio.
|
33
35
|
v (ndarray): 2D radar velocity.
|
@@ -52,13 +54,13 @@ class ClassData:
|
|
52
54
|
self.v = data["radar"].data["v"][:]
|
53
55
|
self.v_sigma = data["radar"].data["v_sigma"][:]
|
54
56
|
for key in ("width", "ldr", "sldr"):
|
55
|
-
if key in data["radar"].data
|
57
|
+
if key in data["radar"].data:
|
56
58
|
setattr(self, key, data["radar"].data[key][:])
|
57
59
|
self.time = data["radar"].time
|
58
60
|
self.height = data["radar"].height
|
59
|
-
self.radar_type = data["radar"].
|
61
|
+
self.radar_type = data["radar"].source_type
|
60
62
|
self.tw = data["model"].data["Tw"][:]
|
61
|
-
self.model_type = data["model"].
|
63
|
+
self.model_type = data["model"].source_type
|
62
64
|
self.beta = data["lidar"].data["beta"][:]
|
63
65
|
self.lwp = data["mwr"].data["lwp"][:]
|
64
66
|
self.is_rain = _find_rain_from_radar_echo(self.z, self.time)
|
@@ -70,7 +72,9 @@ class ClassData:
|
|
70
72
|
|
71
73
|
|
72
74
|
def _find_rain_from_radar_echo(
|
73
|
-
z: np.ndarray,
|
75
|
+
z: np.ndarray,
|
76
|
+
time: np.ndarray,
|
77
|
+
time_buffer: int = 5,
|
74
78
|
) -> np.ndarray:
|
75
79
|
"""Find profiles affected by rain.
|
76
80
|
|
@@ -80,17 +84,22 @@ def _find_rain_from_radar_echo(
|
|
80
84
|
detections as raining.
|
81
85
|
|
82
86
|
Args:
|
87
|
+
----
|
83
88
|
z: Radar echo.
|
84
89
|
time: Time vector.
|
85
90
|
time_buffer: Time in minutes.
|
86
91
|
|
87
92
|
Returns:
|
93
|
+
-------
|
88
94
|
1D Boolean array denoting profiles with rain.
|
89
95
|
|
90
96
|
"""
|
91
|
-
|
97
|
+
filled = False
|
98
|
+
is_rain = ma.array(z[:, 3] > 0, dtype=bool).filled(filled)
|
92
99
|
is_rain = skimage.morphology.remove_small_objects(
|
93
|
-
is_rain,
|
100
|
+
is_rain,
|
101
|
+
2,
|
102
|
+
connectivity=1,
|
94
103
|
) # Filter hot pixels
|
95
104
|
n_profiles = len(time)
|
96
105
|
n_steps = utils.n_elements(time, time_buffer, "time")
|
@@ -122,16 +131,19 @@ def _find_clutter(
|
|
122
131
|
"""Estimates clutter from doppler velocity.
|
123
132
|
|
124
133
|
Args:
|
134
|
+
----
|
125
135
|
n_gates: Number of range gates from the ground where clutter is expected
|
126
136
|
to be found. Default is 10.
|
127
137
|
v_lim: Velocity threshold. Smaller values are classified as clutter.
|
128
138
|
Default is 0.05 (m/s).
|
129
139
|
|
130
140
|
Returns:
|
141
|
+
-------
|
131
142
|
2-D boolean array denoting pixels contaminated by clutter.
|
132
143
|
|
133
144
|
"""
|
134
145
|
is_clutter = np.zeros(v.shape, dtype=bool)
|
135
|
-
|
146
|
+
filled = False
|
147
|
+
tiny_velocity = (np.abs(v[:, :n_gates]) < v_lim).filled(filled)
|
136
148
|
is_clutter[:, :n_gates] = tiny_velocity * utils.transpose(~is_rain)
|
137
149
|
return is_clutter
|
cloudnetpy/categorize/droplet.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
"""
|
1
|
+
"""This module has functions for liquid layer detection.
|
2
2
|
"""
|
3
3
|
import numpy as np
|
4
4
|
import scipy.signal
|
@@ -18,6 +18,7 @@ def correct_liquid_top(
|
|
18
18
|
"""Corrects lidar detected liquid cloud top using radar data.
|
19
19
|
|
20
20
|
Args:
|
21
|
+
----
|
21
22
|
obs: The :class:`ClassData` instance.
|
22
23
|
is_liquid: 2-D boolean array denoting liquid clouds from lidar data.
|
23
24
|
is_freezing: 2-D boolean array of sub-zero temperature, derived from the model
|
@@ -25,16 +26,18 @@ def correct_liquid_top(
|
|
25
26
|
limit: The maximum correction distance (m) above liquid cloud top.
|
26
27
|
|
27
28
|
Returns:
|
29
|
+
-------
|
28
30
|
Corrected liquid cloud array.
|
29
31
|
|
30
32
|
References:
|
33
|
+
----------
|
31
34
|
Hogan R. and O'Connor E., 2004, https://bit.ly/2Yjz9DZ.
|
32
35
|
|
33
36
|
"""
|
34
37
|
is_liquid_corrected = np.copy(is_liquid)
|
35
38
|
liquid_tops = cloudnetpy.categorize.atmos.find_cloud_tops(is_liquid)
|
36
39
|
top_above = utils.n_elements(obs.height, limit)
|
37
|
-
for prof, top in zip(*np.where(liquid_tops)):
|
40
|
+
for prof, top in zip(*np.where(liquid_tops), strict=True):
|
38
41
|
ind = _find_ind_above_top(is_freezing[prof, top:], top_above)
|
39
42
|
rad = obs.z[prof, top : top + ind + 1]
|
40
43
|
if not (rad.mask.all() or ~rad.mask.any()):
|
@@ -61,6 +64,7 @@ def find_liquid(
|
|
61
64
|
"""Estimate liquid layers from SNR-screened attenuated backscatter.
|
62
65
|
|
63
66
|
Args:
|
67
|
+
----
|
64
68
|
obs: The :class:`ClassData` instance.
|
65
69
|
peak_amp: Minimum value of peak. Default is 1e-6.
|
66
70
|
max_width: Maximum width of peak. Default is 300 (m).
|
@@ -72,15 +76,17 @@ def find_liquid(
|
|
72
76
|
min_alt: Minimum altitude of the peak from the ground. Default is 100 (m).
|
73
77
|
|
74
78
|
Returns:
|
79
|
+
-------
|
75
80
|
2-D boolean array denoting liquid layers.
|
76
81
|
|
77
82
|
References:
|
83
|
+
----------
|
78
84
|
The method is based on Tuononen, M. et.al, 2019,
|
79
85
|
https://acp.copernicus.org/articles/19/1985/2019/.
|
80
86
|
|
81
87
|
"""
|
82
88
|
|
83
|
-
def _is_proper_peak():
|
89
|
+
def _is_proper_peak() -> bool:
|
84
90
|
conditions = (
|
85
91
|
npoints >= min_points,
|
86
92
|
peak_width < max_width,
|
@@ -97,13 +103,12 @@ def find_liquid(
|
|
97
103
|
is_liquid = np.zeros(beta.shape, dtype=bool)
|
98
104
|
base_below_peak = utils.n_elements(height, 200)
|
99
105
|
top_above_peak = utils.n_elements(height, 150)
|
100
|
-
difference = np.diff(beta, axis=1)
|
101
|
-
assert isinstance(difference, ma.MaskedArray)
|
106
|
+
difference = ma.array(np.diff(beta, axis=1))
|
102
107
|
beta_diff = difference.filled(0)
|
103
108
|
beta = beta.filled(0)
|
104
109
|
peak_indices = _find_strong_peaks(beta, peak_amp)
|
105
110
|
|
106
|
-
for n, peak in zip(*peak_indices):
|
111
|
+
for n, peak in zip(*peak_indices, strict=True):
|
107
112
|
lprof = beta[n, :]
|
108
113
|
dprof = beta_diff[n, :]
|
109
114
|
try:
|
@@ -129,6 +134,7 @@ def ind_base(dprof: np.ndarray, ind_peak: int, dist: int, lim: float) -> int:
|
|
129
134
|
below the peak exceed a threshold value.
|
130
135
|
|
131
136
|
Args:
|
137
|
+
----
|
132
138
|
dprof: 1-D array of 1st discrete difference. Masked values should
|
133
139
|
be 0, e.g. dprof = np.diff(masked_prof).filled(0)
|
134
140
|
ind_peak: Index of (possibly local) peak in the original profile.
|
@@ -143,13 +149,16 @@ def ind_base(dprof: np.ndarray, ind_peak: int, dist: int, lim: float) -> int:
|
|
143
149
|
in the profile.
|
144
150
|
|
145
151
|
Returns:
|
152
|
+
-------
|
146
153
|
Base index of the peak.
|
147
154
|
|
148
155
|
Raises:
|
156
|
+
------
|
149
157
|
IndexError: Can't find proper base index (probably too many masked
|
150
158
|
values in the profile).
|
151
159
|
|
152
160
|
Examples:
|
161
|
+
--------
|
153
162
|
Consider a profile
|
154
163
|
|
155
164
|
>>> x = np.array([0, 0.5, 1, -99, 4, 8, 5])
|
@@ -177,7 +186,8 @@ def ind_base(dprof: np.ndarray, ind_peak: int, dist: int, lim: float) -> int:
|
|
177
186
|
>>> ind_base(dx, 5, 4, 10)
|
178
187
|
1
|
179
188
|
|
180
|
-
See
|
189
|
+
See Also:
|
190
|
+
--------
|
181
191
|
droplet.ind_top()
|
182
192
|
|
183
193
|
"""
|
@@ -194,6 +204,7 @@ def ind_top(dprof: np.ndarray, ind_peak: int, nprof: int, dist: int, lim: float)
|
|
194
204
|
above the peak exceed a threshold value.
|
195
205
|
|
196
206
|
Args:
|
207
|
+
----
|
197
208
|
dprof: 1-D array of 1st discrete difference. Masked values should be 0, e.g.
|
198
209
|
dprof = np.diff(masked_prof).filled(0)
|
199
210
|
nprof: Length of the profile. Top index can't be higher than this.
|
@@ -207,13 +218,16 @@ def ind_top(dprof: np.ndarray, ind_peak: int, nprof: int, dist: int, lim: float)
|
|
207
218
|
likely accept some other point, higher in the profile.
|
208
219
|
|
209
220
|
Returns:
|
221
|
+
-------
|
210
222
|
Top index of the peak.
|
211
223
|
|
212
224
|
Raises:
|
225
|
+
------
|
213
226
|
IndexError: Can not find proper top index (probably too many masked
|
214
227
|
values in the profile).
|
215
228
|
|
216
|
-
See
|
229
|
+
See Also:
|
230
|
+
--------
|
217
231
|
droplet.ind_base()
|
218
232
|
|
219
233
|
"""
|
@@ -227,9 +241,11 @@ def interpolate_lwp(obs: ClassData) -> np.ndarray:
|
|
227
241
|
"""Linear interpolation of liquid water path to fill masked values.
|
228
242
|
|
229
243
|
Args:
|
244
|
+
----
|
230
245
|
obs: The :class:`ClassData` instance.
|
231
246
|
|
232
247
|
Returns:
|
248
|
+
-------
|
233
249
|
Liquid water path where the masked values are filled by interpolation.
|
234
250
|
|
235
251
|
"""
|
cloudnetpy/categorize/falling.py
CHANGED
@@ -8,7 +8,9 @@ from cloudnetpy.constants import T0
|
|
8
8
|
|
9
9
|
|
10
10
|
def find_falling_hydrometeors(
|
11
|
-
obs: ClassData,
|
11
|
+
obs: ClassData,
|
12
|
+
is_liquid: np.ndarray,
|
13
|
+
is_insects: np.ndarray,
|
12
14
|
) -> np.ndarray:
|
13
15
|
"""Finds falling hydrometeors.
|
14
16
|
|
@@ -19,21 +21,25 @@ def find_falling_hydrometeors(
|
|
19
21
|
temperatures.
|
20
22
|
|
21
23
|
Args:
|
24
|
+
----
|
22
25
|
obs: The :class:`ClassData` instance.
|
23
26
|
is_liquid: 2-D boolean array of liquid droplets.
|
24
27
|
is_insects: 2-D boolean array of insects.
|
25
28
|
|
26
29
|
Returns:
|
30
|
+
-------
|
27
31
|
2-D boolean array containing falling hydrometeors.
|
28
32
|
|
29
33
|
References:
|
34
|
+
----------
|
30
35
|
Hogan R. and O'Connor E., 2004, https://bit.ly/2Yjz9DZ.
|
31
36
|
|
32
37
|
"""
|
33
|
-
|
34
38
|
falling_from_radar = _find_falling_from_radar(obs, is_insects)
|
35
39
|
falling_from_radar_fixed = _fix_liquid_dominated_radar(
|
36
|
-
obs,
|
40
|
+
obs,
|
41
|
+
falling_from_radar,
|
42
|
+
is_liquid,
|
37
43
|
)
|
38
44
|
cold_aerosols = _find_cold_aerosols(obs, is_liquid)
|
39
45
|
return falling_from_radar_fixed | cold_aerosols
|
@@ -60,7 +66,7 @@ def _find_cold_aerosols(obs: ClassData, is_liquid: np.ndarray) -> np.ndarray:
|
|
60
66
|
cold_aerosol_min_altitude = 2000
|
61
67
|
is_beta = ~obs.beta.mask
|
62
68
|
lidar_ice_indices = np.where(
|
63
|
-
(obs.tw.data < cold_aerosol_temperature_limit) & is_beta & ~is_liquid
|
69
|
+
(obs.tw.data < cold_aerosol_temperature_limit) & is_beta & ~is_liquid,
|
64
70
|
)
|
65
71
|
cold_aerosols[lidar_ice_indices] = True
|
66
72
|
low_range_indices = np.where(lidar_range < cold_aerosol_min_altitude)
|
@@ -85,7 +91,9 @@ def _find_cold_aerosols(obs: ClassData, is_liquid: np.ndarray) -> np.ndarray:
|
|
85
91
|
|
86
92
|
|
87
93
|
def _fix_liquid_dominated_radar(
|
88
|
-
obs: ClassData,
|
94
|
+
obs: ClassData,
|
95
|
+
falling_from_radar: np.ndarray,
|
96
|
+
is_liquid: np.ndarray,
|
89
97
|
) -> np.ndarray:
|
90
98
|
"""Radar signals inside liquid clouds are NOT ice if Z is
|
91
99
|
increasing in height inside the cloud.
|
@@ -95,10 +103,12 @@ def _fix_liquid_dominated_radar(
|
|
95
103
|
base_indices = np.where(liquid_bases)
|
96
104
|
top_indices = np.where(liquid_tops)
|
97
105
|
|
98
|
-
for n, base, _, top in zip(*base_indices, *top_indices):
|
106
|
+
for n, base, _, top in zip(*base_indices, *top_indices, strict=True):
|
99
107
|
z_prof = obs.z[n, :]
|
100
108
|
if _is_z_missing_above_liquid(z_prof, top) and _is_z_increasing(
|
101
|
-
z_prof,
|
109
|
+
z_prof,
|
110
|
+
base,
|
111
|
+
top,
|
102
112
|
):
|
103
113
|
falling_from_radar[n, base : top + 1] = False
|
104
114
|
|
@@ -20,13 +20,16 @@ def find_freezing_region(obs: ClassData, melting_layer: np.ndarray) -> np.ndarra
|
|
20
20
|
interpolated for all profiles.
|
21
21
|
|
22
22
|
Args:
|
23
|
+
----
|
23
24
|
obs: The :class:`ClassData` instance.
|
24
25
|
melting_layer: 2-D boolean array denoting melting layer.
|
25
26
|
|
26
27
|
Returns:
|
28
|
+
-------
|
27
29
|
2-D boolean array denoting the sub-zero region.
|
28
30
|
|
29
31
|
Notes:
|
32
|
+
-----
|
30
33
|
It is not clear how model temperature and melting layer should be
|
31
34
|
ideally combined to determine the sub-zero region. This current
|
32
35
|
method differs slightly from the original Matlab code and should
|
@@ -59,7 +62,9 @@ def find_freezing_region(obs: ClassData, melting_layer: np.ndarray) -> np.ndarra
|
|
59
62
|
|
60
63
|
|
61
64
|
def _is_all_freezing(
|
62
|
-
mean_melting_alt: np.ndarray,
|
65
|
+
mean_melting_alt: np.ndarray,
|
66
|
+
t0_alt: np.ndarray,
|
67
|
+
height: np.ndarray,
|
63
68
|
) -> bool:
|
64
69
|
no_detected_melting = mean_melting_alt.all() is ma.masked
|
65
70
|
all_temperatures_below_freezing = (t0_alt <= height[0]).all()
|
@@ -67,7 +72,9 @@ def _is_all_freezing(
|
|
67
72
|
|
68
73
|
|
69
74
|
def _find_mean_melting_alt(obs: ClassData, melting_layer: np.ndarray) -> ma.MaskedArray:
|
70
|
-
|
75
|
+
if melting_layer.dtype != bool:
|
76
|
+
msg = "melting_layer data type should be boolean"
|
77
|
+
raise ValueError(msg)
|
71
78
|
alt_array = np.tile(obs.height, (len(obs.time), 1))
|
72
79
|
melting_alts = ma.array(alt_array, mask=~melting_layer)
|
73
80
|
return ma.median(melting_alts, axis=1)
|
@@ -77,10 +84,12 @@ def _find_t0_alt(temperature: np.ndarray, height: np.ndarray) -> np.ndarray:
|
|
77
84
|
"""Interpolates altitudes where temperature goes below freezing.
|
78
85
|
|
79
86
|
Args:
|
87
|
+
----
|
80
88
|
temperature: 2-D temperature (K).
|
81
89
|
height: 1-D altitude grid (m).
|
82
90
|
|
83
91
|
Returns:
|
92
|
+
-------
|
84
93
|
1-D array denoting altitudes where the temperature drops below 0 deg C.
|
85
94
|
|
86
95
|
"""
|
@@ -90,8 +99,13 @@ def _find_t0_alt(temperature: np.ndarray, height: np.ndarray) -> np.ndarray:
|
|
90
99
|
if ind == 0:
|
91
100
|
alt = np.append(alt, height[0])
|
92
101
|
else:
|
93
|
-
x =
|
94
|
-
|
95
|
-
|
102
|
+
x, y = zip(
|
103
|
+
*sorted(
|
104
|
+
zip(
|
105
|
+
prof[ind - 1 : ind + 1], height[ind - 1 : ind + 1], strict=True
|
106
|
+
),
|
107
|
+
),
|
108
|
+
strict=True,
|
109
|
+
)
|
96
110
|
alt = np.append(alt, np.interp(T0, x, y))
|
97
111
|
return alt
|
cloudnetpy/categorize/insects.py
CHANGED
@@ -29,6 +29,7 @@ def find_insects(
|
|
29
29
|
above melting layer.
|
30
30
|
|
31
31
|
Args:
|
32
|
+
----
|
32
33
|
obs: The :class:`ClassData` instance.
|
33
34
|
melting_layer: 2D array denoting melting layer.
|
34
35
|
liquid_layers: 2D array denoting liquid layers.
|
@@ -36,12 +37,14 @@ def find_insects(
|
|
36
37
|
Default is 0.8.
|
37
38
|
|
38
39
|
Returns:
|
40
|
+
-------
|
39
41
|
tuple: 2-element tuple containing
|
40
42
|
|
41
43
|
- 2-D boolean flag of insects presence.
|
42
44
|
- 2-D probability of pixel containing insects.
|
43
45
|
|
44
46
|
Notes:
|
47
|
+
-----
|
45
48
|
This insect detection method is novel and needs to be validated.
|
46
49
|
|
47
50
|
"""
|
@@ -65,9 +68,9 @@ def _get_probabilities(obs: ClassData) -> dict:
|
|
65
68
|
lwp_interp = droplet.interpolate_lwp(obs)
|
66
69
|
fun = utils.array_to_probability
|
67
70
|
return {
|
68
|
-
"width": fun(obs.width, 1, 0.3, True) if hasattr(obs, "width") else 1,
|
69
|
-
"z_strong": fun(obs.z, 0, 8, True),
|
70
|
-
"z_weak": fun(obs.z, -20, 8, True),
|
71
|
+
"width": fun(obs.width, 1, 0.3, invert=True) if hasattr(obs, "width") else 1,
|
72
|
+
"z_strong": fun(obs.z, 0, 8, invert=True),
|
73
|
+
"z_weak": fun(obs.z, -20, 8, invert=True),
|
71
74
|
"ldr": fun(obs.ldr, -25, 5) if hasattr(obs, "ldr") else None,
|
72
75
|
"sldr": fun(obs.sldr, -25, 5) if hasattr(obs, "sldr") else None,
|
73
76
|
"temp_loose": fun(obs.tw, 268, 2),
|
@@ -79,11 +82,11 @@ def _get_probabilities(obs: ClassData) -> dict:
|
|
79
82
|
|
80
83
|
|
81
84
|
def _get_smoothed_v(
|
82
|
-
obs: ClassData,
|
85
|
+
obs: ClassData,
|
86
|
+
sigma: tuple[float, float] = (5, 5),
|
83
87
|
) -> ma.MaskedArray:
|
84
88
|
smoothed_v = gaussian_filter(obs.v, sigma)
|
85
|
-
|
86
|
-
return smoothed_v
|
89
|
+
return ma.masked_where(obs.v.mask, smoothed_v)
|
87
90
|
|
88
91
|
|
89
92
|
def _calc_prob_from_ldr(prob: dict) -> np.ndarray:
|
@@ -102,12 +105,15 @@ def _calc_prob_from_ldr(prob: dict) -> np.ndarray:
|
|
102
105
|
def _calc_prob_from_all(prob: dict) -> np.ndarray:
|
103
106
|
"""This can be tried when LDR is not available. To detect insects without LDR
|
104
107
|
unambiguously is difficult and might result in many false positives and/or false
|
105
|
-
negatives.
|
108
|
+
negatives.
|
109
|
+
"""
|
106
110
|
return prob["z_weak"] * prob["temp_strict"] * prob["width"] * prob["v"]
|
107
111
|
|
108
112
|
|
109
113
|
def _adjust_for_radar(
|
110
|
-
obs: ClassData,
|
114
|
+
obs: ClassData,
|
115
|
+
prob: dict,
|
116
|
+
prob_from_others: np.ndarray,
|
111
117
|
) -> np.ndarray:
|
112
118
|
"""Adds radar-specific weighting to insect probabilities."""
|
113
119
|
if "mira" in obs.radar_type.lower():
|
@@ -116,7 +122,8 @@ def _adjust_for_radar(
|
|
116
122
|
|
117
123
|
|
118
124
|
def _fill_missing_pixels(
|
119
|
-
prob_from_ldr: np.ndarray,
|
125
|
+
prob_from_ldr: np.ndarray,
|
126
|
+
prob_from_others: np.ndarray,
|
120
127
|
) -> np.ndarray:
|
121
128
|
prob_combined = np.copy(prob_from_ldr)
|
122
129
|
no_ldr = np.where(prob_from_ldr == 0)
|
@@ -124,19 +131,25 @@ def _fill_missing_pixels(
|
|
124
131
|
return prob_combined
|
125
132
|
|
126
133
|
|
127
|
-
def _screen_insects(
|
128
|
-
|
134
|
+
def _screen_insects(
|
135
|
+
insect_prob,
|
136
|
+
insect_prob_no_ldr,
|
137
|
+
melting_layer,
|
138
|
+
liquid_layers,
|
139
|
+
obs,
|
140
|
+
) -> np.ndarray:
|
141
|
+
def _screen_liquid_layers() -> None:
|
129
142
|
prob[liquid_layers == 1] = 0
|
130
143
|
|
131
|
-
def _screen_above_melting():
|
144
|
+
def _screen_above_melting() -> None:
|
132
145
|
above_melting = utils.ffill(melting_layer)
|
133
146
|
prob[above_melting == 1] = 0
|
134
147
|
|
135
|
-
def _screen_above_liquid():
|
148
|
+
def _screen_above_liquid() -> None:
|
136
149
|
above_liquid = utils.ffill(liquid_layers)
|
137
150
|
prob[(above_liquid == 1) & (insect_prob_no_ldr > 0)] = 0
|
138
151
|
|
139
|
-
def _screen_rainy_profiles():
|
152
|
+
def _screen_rainy_profiles() -> None:
|
140
153
|
prob[obs.is_rain == 1, :] = 0
|
141
154
|
|
142
155
|
prob = np.copy(insect_prob)
|
cloudnetpy/categorize/lidar.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
"""Lidar module, containing the :class:`Lidar` class."""
|
2
2
|
import logging
|
3
|
+
from typing import Literal
|
3
4
|
|
4
5
|
import numpy as np
|
5
6
|
from numpy import ma
|
@@ -12,6 +13,7 @@ class Lidar(DataSource):
|
|
12
13
|
"""Lidar class, child of DataSource.
|
13
14
|
|
14
15
|
Args:
|
16
|
+
----
|
15
17
|
full_path: Cloudnet Level 1 lidar netCDF file.
|
16
18
|
|
17
19
|
"""
|
@@ -21,42 +23,46 @@ class Lidar(DataSource):
|
|
21
23
|
self.append_data(self.getvar("beta"), "beta")
|
22
24
|
self._add_meta()
|
23
25
|
|
24
|
-
def interpolate_to_grid(
|
26
|
+
def interpolate_to_grid(
|
27
|
+
self, time_new: np.ndarray, height_new: np.ndarray
|
28
|
+
) -> list[int]:
|
25
29
|
"""Interpolate beta using nearest neighbor."""
|
26
|
-
max_height = 100
|
27
|
-
max_time = 1
|
30
|
+
max_height = 100 # m
|
31
|
+
max_time = 1 / 60 # min -> fraction hour
|
28
32
|
|
29
|
-
|
33
|
+
if self.height is None:
|
34
|
+
msg = "Unable to interpolate lidar: no height information"
|
35
|
+
raise RuntimeError(msg)
|
36
|
+
|
37
|
+
# Interpolate beta to new grid but ignore profiles that are completely masked
|
30
38
|
beta = self.data["beta"][:]
|
31
|
-
indices = []
|
32
|
-
|
33
|
-
if not ma.all(b) is ma.masked:
|
34
|
-
indices.append(ind)
|
35
|
-
assert self.height is not None
|
36
|
-
beta_interpolated = interpolate_2d_nearest(
|
39
|
+
indices = [ind for ind, b in enumerate(beta) if ma.all(b) is not ma.masked]
|
40
|
+
beta_interp = interpolate_2d_nearest(
|
37
41
|
self.time[indices],
|
38
42
|
self.height,
|
39
43
|
beta[indices, :],
|
40
44
|
time_new,
|
41
45
|
height_new,
|
42
46
|
)
|
47
|
+
# Mask data points that are too far from the original grid
|
48
|
+
time_gap_ind = _get_gap_ind(self.time[indices], time_new, max_time)
|
49
|
+
height_gap_ind = _get_gap_ind(self.height, height_new, max_height)
|
50
|
+
self._mask_profiles(beta_interp, time_gap_ind, "time")
|
51
|
+
self._mask_profiles(beta_interp, height_gap_ind, "height")
|
52
|
+
self.data["beta"].data = beta_interp
|
53
|
+
return time_gap_ind
|
43
54
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
f"Unable to interpolate lidar for {len(bad_height_indices)} altitudes"
|
56
|
-
)
|
57
|
-
beta_interpolated[:, bad_height_indices] = ma.masked
|
58
|
-
self.data["beta"].data = beta_interpolated
|
59
|
-
return bad_time_indices
|
55
|
+
@staticmethod
|
56
|
+
def _mask_profiles(
|
57
|
+
data: ma.MaskedArray, ind: list[int], dim: Literal["time", "height"]
|
58
|
+
) -> None:
|
59
|
+
prefix = f"Unable to interpolate lidar for {len(ind)}"
|
60
|
+
if dim == "time" and ind:
|
61
|
+
logging.warning("%s time steps", prefix)
|
62
|
+
data[ind, :] = ma.masked
|
63
|
+
elif dim == "height" and ind:
|
64
|
+
logging.warning("%s altitudes", prefix)
|
65
|
+
data[:, ind] = ma.masked
|
60
66
|
|
61
67
|
def _add_meta(self) -> None:
|
62
68
|
self.append_data(float(self.getvar("wavelength")), "lidar_wavelength")
|
@@ -64,13 +70,9 @@ class Lidar(DataSource):
|
|
64
70
|
self.append_data(3.0, "beta_bias")
|
65
71
|
|
66
72
|
|
67
|
-
def
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
distance = diffu[diffu.argmin()]
|
74
|
-
if distance > threshold:
|
75
|
-
indices.append(ind)
|
76
|
-
return indices
|
73
|
+
def _get_gap_ind(grid: np.ndarray, new_grid: np.ndarray, threshold: float) -> list[int]:
|
74
|
+
return [
|
75
|
+
ind
|
76
|
+
for ind, value in enumerate(new_grid)
|
77
|
+
if np.min(np.abs(grid - value)) > threshold
|
78
|
+
]
|
cloudnetpy/categorize/melting.py
CHANGED
@@ -9,7 +9,7 @@ from cloudnetpy.categorize.containers import ClassData
|
|
9
9
|
from cloudnetpy.constants import T0
|
10
10
|
|
11
11
|
|
12
|
-
def find_melting_layer(obs: ClassData, smooth: bool = True) -> np.ndarray:
|
12
|
+
def find_melting_layer(obs: ClassData, *, smooth: bool = True) -> np.ndarray:
|
13
13
|
"""Finds melting layer from model temperature, ldr, and velocity.
|
14
14
|
|
15
15
|
Melting layer is detected using linear depolarization ratio, *ldr*,
|
@@ -32,17 +32,20 @@ def find_melting_layer(obs: ClassData, smooth: bool = True) -> np.ndarray:
|
|
32
32
|
the rest -8..+6.
|
33
33
|
|
34
34
|
Notes:
|
35
|
+
-----
|
35
36
|
This melting layer detection method is novel and needs to be validated.
|
36
37
|
Also note that there might be some detection problems with strong
|
37
38
|
updrafts of air. In these cases the absolute values for speed do not
|
38
39
|
make sense (rain drops can even move upwards instead of down).
|
39
40
|
|
40
41
|
Args:
|
42
|
+
----
|
41
43
|
obs: The :class:`ClassData` instance.
|
42
44
|
smooth: If True, apply a small Gaussian smoother to the
|
43
45
|
melting layer. Default is True.
|
44
46
|
|
45
47
|
Returns:
|
48
|
+
-------
|
46
49
|
2-D boolean array denoting the melting layer.
|
47
50
|
|
48
51
|
"""
|
@@ -55,8 +58,7 @@ def find_melting_layer(obs: ClassData, smooth: bool = True) -> np.ndarray:
|
|
55
58
|
|
56
59
|
if hasattr(obs, "ldr"):
|
57
60
|
# Required for peak detection
|
58
|
-
diffu = np.diff(obs.ldr, axis=1)
|
59
|
-
assert isinstance(diffu, ma.MaskedArray)
|
61
|
+
diffu = ma.array(np.diff(obs.ldr, axis=1))
|
60
62
|
ldr_diff = diffu.filled(0)
|
61
63
|
|
62
64
|
t_range = _find_model_temperature_range(obs.model_type)
|
@@ -69,15 +71,22 @@ def find_melting_layer(obs: ClassData, smooth: bool = True) -> np.ndarray:
|
|
69
71
|
v_prof = obs.v[ind, temp_indices]
|
70
72
|
|
71
73
|
if ldr_diff is not None:
|
72
|
-
|
74
|
+
if not hasattr(obs, "ldr"):
|
75
|
+
msg = "ldr_diff is not None but obs.ldr does not exist"
|
76
|
+
raise RuntimeError(msg)
|
73
77
|
ldr_prof = obs.ldr[ind, temp_indices]
|
74
78
|
ldr_dprof = ldr_diff[ind, temp_indices]
|
75
79
|
|
76
80
|
if ma.count(ldr_prof) > 3 or ma.count(v_prof) > 3:
|
77
81
|
try:
|
78
|
-
|
82
|
+
if ldr_prof is None or ldr_dprof is None:
|
83
|
+
msg = "ldr_prof or ldr_dprof is None"
|
84
|
+
raise AssertionError(msg) # noqa: TRY301
|
79
85
|
indices = _find_melting_layer_from_ldr(
|
80
|
-
ldr_prof,
|
86
|
+
ldr_prof,
|
87
|
+
ldr_dprof,
|
88
|
+
v_prof,
|
89
|
+
z_prof,
|
81
90
|
)
|
82
91
|
except (ValueError, IndexError, AssertionError):
|
83
92
|
height = obs.height[temp_indices]
|
@@ -114,13 +123,14 @@ def _find_melting_layer_from_ldr(
|
|
114
123
|
|
115
124
|
if all(conditions):
|
116
125
|
base = int(np.floor(base + (peak - base) / 2))
|
117
|
-
|
118
|
-
return indices
|
126
|
+
return np.arange(base, top)
|
119
127
|
return None
|
120
128
|
|
121
129
|
|
122
130
|
def _find_melting_layer_from_v(
|
123
|
-
v_prof: np.ndarray,
|
131
|
+
v_prof: np.ndarray,
|
132
|
+
width_prof: np.ndarray | None,
|
133
|
+
height: np.ndarray,
|
124
134
|
) -> np.ndarray | None:
|
125
135
|
v = np.copy(v_prof[:-1])
|
126
136
|
v_diff = np.diff(v_prof)
|