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
cloudnetpy/categorize/atmos.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
"""
|
1
|
+
"""This module contains functions to calculate
|
2
2
|
various atmospheric parameters.
|
3
3
|
"""
|
4
4
|
from typing import Final
|
@@ -27,13 +27,16 @@ def calc_lwc_change_rate(temperature: np.ndarray, pressure: np.ndarray) -> np.nd
|
|
27
27
|
with height, given the cloud base temperature and pressure.
|
28
28
|
|
29
29
|
Args:
|
30
|
+
----
|
30
31
|
temperature: Temperature of cloud base (K).
|
31
32
|
pressure: Pressure of cloud base (Pa).
|
32
33
|
|
33
34
|
Returns:
|
35
|
+
-------
|
34
36
|
dlwc/dz (kg m-3 m-1)
|
35
37
|
|
36
38
|
References:
|
39
|
+
----------
|
37
40
|
Brenguier, 1991, https://bit.ly/2QCSJtb
|
38
41
|
|
39
42
|
"""
|
@@ -49,18 +52,19 @@ def calc_lwc_change_rate(temperature: np.ndarray, pressure: np.ndarray) -> np.nd
|
|
49
52
|
)
|
50
53
|
f3 = con.MW_RATIO * svp * pressure_difference**-2
|
51
54
|
dqs_dp = f1 * f2 * f3
|
52
|
-
|
53
|
-
return dqs_dz
|
55
|
+
return dqs_dp * air_density**2 * -scipy.constants.g
|
54
56
|
|
55
57
|
|
56
58
|
def calc_mixing_ratio(svp: np.ndarray, pressure: np.ndarray) -> np.ndarray:
|
57
59
|
"""Calculates mixing ratio from saturation vapor pressure and pressure.
|
58
60
|
|
59
61
|
Args:
|
62
|
+
----
|
60
63
|
svp: Saturation vapor pressure (Pa).
|
61
64
|
pressure: Atmospheric pressure (Pa).
|
62
65
|
|
63
66
|
Returns:
|
67
|
+
-------
|
64
68
|
Mixing ratio (kg kg-1).
|
65
69
|
|
66
70
|
"""
|
@@ -68,16 +72,20 @@ def calc_mixing_ratio(svp: np.ndarray, pressure: np.ndarray) -> np.ndarray:
|
|
68
72
|
|
69
73
|
|
70
74
|
def calc_air_density(
|
71
|
-
pressure: np.ndarray,
|
75
|
+
pressure: np.ndarray,
|
76
|
+
temperature: np.ndarray,
|
77
|
+
svp_mixing_ratio: np.ndarray,
|
72
78
|
) -> np.ndarray:
|
73
79
|
"""Calculates air density (kg m-3).
|
74
80
|
|
75
81
|
Args:
|
82
|
+
----
|
76
83
|
pressure: Pressure (Pa).
|
77
84
|
temperature: Temperature (K).
|
78
85
|
svp_mixing_ratio: Saturation vapor pressure mixing ratio (kg/kg).
|
79
86
|
|
80
87
|
Returns:
|
88
|
+
-------
|
81
89
|
Air density (kg m-3).
|
82
90
|
|
83
91
|
"""
|
@@ -88,10 +96,12 @@ def get_attenuations(data: dict, classification: ClassificationResult) -> dict:
|
|
88
96
|
"""Calculates attenuations due to atmospheric gases and liquid water.
|
89
97
|
|
90
98
|
Args:
|
99
|
+
----
|
91
100
|
data: Containing :class:`Model` and :class:`Mwr` instances.
|
92
101
|
classification: A :class:`ClassificationResult` instance.
|
93
102
|
|
94
103
|
Returns:
|
104
|
+
-------
|
95
105
|
Dictionary containing `radar_gas_atten`, `radar_liquid_atten`,
|
96
106
|
`liquid_atten_err`, `liquid_corrected` and `liquid_uncorrected` fields.
|
97
107
|
|
@@ -111,10 +121,12 @@ class Attenuation:
|
|
111
121
|
"""Base class for gas and liquid attenuations.
|
112
122
|
|
113
123
|
Args:
|
124
|
+
----
|
114
125
|
model: The :class:`Model` instance.
|
115
126
|
classification: The :class:`ClassificationResult` instance.
|
116
127
|
|
117
128
|
Attributes:
|
129
|
+
----------
|
118
130
|
classification (ClassificationResult): The :class:`ClassificationResult`
|
119
131
|
instance.
|
120
132
|
|
@@ -131,10 +143,12 @@ class GasAttenuation(Attenuation):
|
|
131
143
|
"""Radar gas attenuation class. Child of Attenuation.
|
132
144
|
|
133
145
|
Args:
|
146
|
+
----
|
134
147
|
data: Containing :class:`Model` instance.
|
135
148
|
classification: The :class:`ClassificationResult` instance.
|
136
149
|
|
137
150
|
Attributes:
|
151
|
+
----------
|
138
152
|
atten (ndarray): Gas attenuation (dB).
|
139
153
|
|
140
154
|
"""
|
@@ -146,8 +160,7 @@ class GasAttenuation(Attenuation):
|
|
146
160
|
def _calc_gas_atten(self) -> np.ndarray:
|
147
161
|
specific_atten = ma.copy(self._model["specific_gas_atten"])
|
148
162
|
specific_atten_corrected = self._fix_atten_in_liquid(specific_atten)
|
149
|
-
|
150
|
-
return gas_atten
|
163
|
+
return self._specific_to_gas_atten(specific_atten_corrected)
|
151
164
|
|
152
165
|
def _fix_atten_in_liquid(self, atten: np.ndarray) -> np.ndarray:
|
153
166
|
saturated_atten = self._model["specific_saturated_gas_atten"]
|
@@ -167,10 +180,12 @@ class LiquidAttenuation(Attenuation):
|
|
167
180
|
"""Radar liquid attenuation class. Child of Attenuation.
|
168
181
|
|
169
182
|
Args:
|
183
|
+
----
|
170
184
|
data: Containing :class:`Model` and :class:`Mwr` instances.
|
171
185
|
classification: The :class:`ClassificationResult` instance.
|
172
186
|
|
173
187
|
Attributes:
|
188
|
+
----------
|
174
189
|
atten (ndarray): Radar liquid attenuation (dB).
|
175
190
|
atten_err (ndarray): Error of radar liquid attenuation (dB).
|
176
191
|
uncorrected (ndarray): Boolean array denoting uncorrected pixels.
|
@@ -203,7 +218,8 @@ class LiquidAttenuation(Attenuation):
|
|
203
218
|
def _get_liquid_atten_err(self) -> ma.MaskedArray:
|
204
219
|
"""Finds radar liquid attenuation error."""
|
205
220
|
lwc_err_scaled = distribute_lwp_to_liquid_clouds(
|
206
|
-
self._lwc_dz_err,
|
221
|
+
self._lwc_dz_err,
|
222
|
+
self._mwr["lwp_error"][:],
|
207
223
|
)
|
208
224
|
return self._calc_attenuation(lwc_err_scaled)
|
209
225
|
|
@@ -224,9 +240,9 @@ class LiquidAttenuation(Attenuation):
|
|
224
240
|
return hard_to_correct
|
225
241
|
|
226
242
|
def _find_corrected_pixels(self) -> np.ndarray:
|
227
|
-
proper_values = self.atten > 0
|
228
|
-
|
229
|
-
return proper_values.filled(
|
243
|
+
proper_values = ma.array(self.atten > 0)
|
244
|
+
filled = False
|
245
|
+
return proper_values.filled(filled) & ~self.uncorrected
|
230
246
|
|
231
247
|
def _mask_uncorrected_attenuation(self) -> None:
|
232
248
|
self.atten[self.uncorrected] = ma.masked
|
@@ -239,10 +255,12 @@ def fill_clouds_with_lwc_dz(atmosphere: tuple, is_liquid: np.ndarray) -> np.ndar
|
|
239
255
|
"""Fills liquid clouds with lwc change rate at the cloud bases.
|
240
256
|
|
241
257
|
Args:
|
258
|
+
----
|
242
259
|
atmosphere: 2-element tuple containing temperature (K) and pressure (Pa).
|
243
260
|
is_liquid: Boolean array indicating presence of liquid clouds.
|
244
261
|
|
245
262
|
Returns:
|
263
|
+
-------
|
246
264
|
Liquid water content change rate (kg/m3/m), so that for each cloud the base
|
247
265
|
value is filled for the whole cloud.
|
248
266
|
|
@@ -254,22 +272,26 @@ def fill_clouds_with_lwc_dz(atmosphere: tuple, is_liquid: np.ndarray) -> np.ndar
|
|
254
272
|
|
255
273
|
|
256
274
|
def get_lwc_change_rate_at_bases(
|
257
|
-
atmosphere: tuple,
|
275
|
+
atmosphere: tuple,
|
276
|
+
is_liquid: np.ndarray,
|
258
277
|
) -> np.ndarray:
|
259
278
|
"""Finds LWC change rate in liquid cloud bases.
|
260
279
|
|
261
280
|
Args:
|
281
|
+
----
|
262
282
|
atmosphere: 2-element tuple containing temperature (K) and pressure (Pa).
|
263
283
|
is_liquid: Boolean array indicating presence of liquid clouds.
|
264
284
|
|
265
285
|
Returns:
|
286
|
+
-------
|
266
287
|
Liquid water content change rate at cloud bases (kg/m3/m).
|
267
288
|
|
268
289
|
"""
|
269
290
|
liquid_bases = find_cloud_bases(is_liquid)
|
270
291
|
lwc_dz = ma.zeros(liquid_bases.shape)
|
271
292
|
lwc_dz[liquid_bases] = calc_lwc_change_rate(
|
272
|
-
atmosphere[0][liquid_bases],
|
293
|
+
atmosphere[0][liquid_bases],
|
294
|
+
atmosphere[1][liquid_bases],
|
273
295
|
)
|
274
296
|
return lwc_dz
|
275
297
|
|
@@ -278,9 +300,11 @@ def find_cloud_bases(array: np.ndarray) -> np.ndarray:
|
|
278
300
|
"""Finds bases of clouds.
|
279
301
|
|
280
302
|
Args:
|
303
|
+
----
|
281
304
|
array: 2D boolean array denoting clouds or some other similar field.
|
282
305
|
|
283
306
|
Returns:
|
307
|
+
-------
|
284
308
|
Boolean array indicating bases of the individual clouds.
|
285
309
|
|
286
310
|
"""
|
@@ -293,9 +317,11 @@ def find_cloud_tops(array: np.ndarray) -> np.ndarray:
|
|
293
317
|
"""Finds tops of clouds.
|
294
318
|
|
295
319
|
Args:
|
320
|
+
----
|
296
321
|
array: 2D boolean array denoting clouds or some other similar field.
|
297
322
|
|
298
323
|
Returns:
|
324
|
+
-------
|
299
325
|
Boolean array indicating tops of the individual clouds.
|
300
326
|
|
301
327
|
"""
|
@@ -305,7 +331,8 @@ def find_cloud_tops(array: np.ndarray) -> np.ndarray:
|
|
305
331
|
|
306
332
|
|
307
333
|
def find_lowest_cloud_bases(
|
308
|
-
cloud_mask: np.ndarray,
|
334
|
+
cloud_mask: np.ndarray,
|
335
|
+
height: np.ndarray,
|
309
336
|
) -> ma.MaskedArray:
|
310
337
|
"""Finds altitudes of cloud bases."""
|
311
338
|
cloud_heights = cloud_mask * height
|
@@ -313,7 +340,8 @@ def find_lowest_cloud_bases(
|
|
313
340
|
|
314
341
|
|
315
342
|
def find_highest_cloud_tops(
|
316
|
-
cloud_mask: np.ndarray,
|
343
|
+
cloud_mask: np.ndarray,
|
344
|
+
height: np.ndarray,
|
317
345
|
) -> ma.MaskedArray:
|
318
346
|
"""Finds altitudes of cloud tops."""
|
319
347
|
cloud_heights = cloud_mask * height
|
@@ -331,11 +359,13 @@ def calc_adiabatic_lwc(lwc_change_rate: np.ndarray, dheight: float) -> np.ndarra
|
|
331
359
|
"""Calculates adiabatic liquid water content (kg/m3).
|
332
360
|
|
333
361
|
Args:
|
362
|
+
----
|
334
363
|
lwc_change_rate: Liquid water content change rate (kg/m3/m) calculated at the
|
335
364
|
base of each cloud and filled to that cloud.
|
336
365
|
dheight: Median difference of the height vector (m).
|
337
366
|
|
338
367
|
Returns:
|
368
|
+
-------
|
339
369
|
Liquid water content (kg/m3).
|
340
370
|
|
341
371
|
"""
|
@@ -352,10 +382,12 @@ def distribute_lwp_to_liquid_clouds(lwc: np.ndarray, lwp: np.ndarray) -> np.ndar
|
|
352
382
|
theoretical proportion, i.e., sum(scaled LWC) = measured LWP.
|
353
383
|
|
354
384
|
Args:
|
385
|
+
----
|
355
386
|
lwc: 2D liquid water content (kg/m3).
|
356
387
|
lwp: 1D liquid water path (kg/m2).
|
357
388
|
|
358
389
|
Returns:
|
390
|
+
-------
|
359
391
|
2D LWP-weighted, normalized LWC (kg/m2).
|
360
392
|
|
361
393
|
"""
|
@@ -17,12 +17,15 @@ def calc_wet_bulb_temperature(model_data: dict) -> np.ndarray:
|
|
17
17
|
expansion of a simple expression for the saturated vapour pressure.
|
18
18
|
|
19
19
|
Args:
|
20
|
+
----
|
20
21
|
model_data: Model variables `temperature`, `pressure`, `rh`.
|
21
22
|
|
22
23
|
Returns:
|
24
|
+
-------
|
23
25
|
Wet bulb temperature (K).
|
24
26
|
|
25
27
|
References:
|
28
|
+
----------
|
26
29
|
J. Sullivan and L. D. Sanders: Method for obtaining wet-bulb
|
27
30
|
temperatures by modifying the psychrometric formula.
|
28
31
|
|
@@ -63,9 +66,11 @@ def calc_saturation_vapor_pressure(temperature: np.ndarray) -> np.ndarray:
|
|
63
66
|
"""Goff-Gratch formula for saturation vapor pressure over water adopted by WMO.
|
64
67
|
|
65
68
|
Args:
|
69
|
+
----
|
66
70
|
temperature: Temperature (K).
|
67
71
|
|
68
72
|
Returns:
|
73
|
+
-------
|
69
74
|
Saturation vapor pressure (Pa).
|
70
75
|
|
71
76
|
"""
|
@@ -90,9 +95,11 @@ def calc_psychrometric_constant(pressure: np.ndarray) -> np.ndarray:
|
|
90
95
|
of water in air to the air temperature.
|
91
96
|
|
92
97
|
Args:
|
98
|
+
----
|
93
99
|
pressure: Atmospheric pressure (Pa).
|
94
100
|
|
95
101
|
Returns:
|
102
|
+
-------
|
96
103
|
Psychrometric constant value (Pa K-1)
|
97
104
|
|
98
105
|
"""
|
@@ -103,12 +110,15 @@ def calc_dew_point_temperature(vapor_pressure: np.ndarray) -> np.ndarray:
|
|
103
110
|
"""Returns dew point temperature.
|
104
111
|
|
105
112
|
Args:
|
113
|
+
----
|
106
114
|
vapor_pressure: Water vapor pressure (Pa).
|
107
115
|
|
108
116
|
Returns:
|
117
|
+
-------
|
109
118
|
Dew point temperature (K).
|
110
119
|
|
111
120
|
Notes:
|
121
|
+
-----
|
112
122
|
Method from Vaisala's white paper: "Humidity conversion formulas".
|
113
123
|
|
114
124
|
"""
|
@@ -130,4 +140,4 @@ def k2c(temp: np.ndarray) -> np.ndarray:
|
|
130
140
|
|
131
141
|
def mmh2ms(data: np.ndarray) -> np.ndarray:
|
132
142
|
"""Converts mm h-1 to m s-1"""
|
133
|
-
return data /
|
143
|
+
return data / con.SEC_IN_HOUR * con.MM_TO_M
|
@@ -10,7 +10,9 @@ from cloudnetpy.metadata import MetaData
|
|
10
10
|
|
11
11
|
|
12
12
|
def generate_categorize(
|
13
|
-
input_files: dict,
|
13
|
+
input_files: dict,
|
14
|
+
output_file: str,
|
15
|
+
uuid: str | None = None,
|
14
16
|
) -> str:
|
15
17
|
"""Generates Cloudnet Level 1c categorize file.
|
16
18
|
|
@@ -21,6 +23,7 @@ def generate_categorize(
|
|
21
23
|
in *ouput_file* which is a compressed netCDF4 file.
|
22
24
|
|
23
25
|
Args:
|
26
|
+
----
|
24
27
|
input_files: dict containing file names for calibrated `radar`, `lidar`,
|
25
28
|
`model` and `mwr` files. Optionally also `lv0_files`, a list of
|
26
29
|
RPG level 0 files.
|
@@ -28,12 +31,15 @@ def generate_categorize(
|
|
28
31
|
uuid: Set specific UUID for the file.
|
29
32
|
|
30
33
|
Returns:
|
34
|
+
-------
|
31
35
|
UUID of the generated file.
|
32
36
|
|
33
37
|
Raises:
|
38
|
+
------
|
34
39
|
RuntimeError: Failed to create the categorize file.
|
35
40
|
|
36
41
|
Notes:
|
42
|
+
-----
|
37
43
|
Separate mwr-file is not needed when using RPG cloud radar which
|
38
44
|
measures liquid water path. Then, the radar file can be used as
|
39
45
|
a mwr-file as well, i.e. {'mwr': 'radar.nc'}.
|
@@ -42,6 +48,7 @@ def generate_categorize(
|
|
42
48
|
to detect liquid droplets.
|
43
49
|
|
44
50
|
Examples:
|
51
|
+
--------
|
45
52
|
>>> from cloudnetpy.categorize import generate_categorize
|
46
53
|
>>> input_files = {'radar': 'radar.nc',
|
47
54
|
'lidar': 'lidar.nc',
|
@@ -56,14 +63,13 @@ def generate_categorize(
|
|
56
63
|
|
57
64
|
def _interpolate_to_cloudnet_grid() -> list:
|
58
65
|
wl_band = utils.get_wl_band(data["radar"].radar_frequency)
|
59
|
-
data["model"].interpolate_to_common_height(wl_band)
|
60
|
-
data["model"].interpolate_to_grid(time, height)
|
61
66
|
data["mwr"].rebin_to_grid(time)
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
+
data["model"].interpolate_to_common_height(wl_band)
|
68
|
+
model_gap_ind = data["model"].interpolate_to_grid(time, height)
|
69
|
+
radar_gap_ind = data["radar"].rebin_to_grid(time)
|
70
|
+
lidar_gap_ind = data["lidar"].interpolate_to_grid(time, height)
|
71
|
+
gap_indices = set(radar_gap_ind + lidar_gap_ind + model_gap_ind)
|
72
|
+
return [ind for ind in range(len(time)) if ind not in gap_indices]
|
67
73
|
|
68
74
|
def _screen_bad_time_indices(valid_indices: list) -> None:
|
69
75
|
n_time_full = len(time)
|
@@ -102,10 +108,10 @@ def generate_categorize(
|
|
102
108
|
**data["mwr"].data,
|
103
109
|
}
|
104
110
|
|
105
|
-
def _define_dense_grid():
|
111
|
+
def _define_dense_grid() -> tuple:
|
106
112
|
return utils.time_grid(), data["radar"].height
|
107
113
|
|
108
|
-
def _close_all():
|
114
|
+
def _close_all() -> None:
|
109
115
|
for obj in data.values():
|
110
116
|
if isinstance(obj, Radar | Lidar | Mwr | Model):
|
111
117
|
obj.close()
|
@@ -117,14 +123,20 @@ def generate_categorize(
|
|
117
123
|
"mwr": Mwr(input_files["mwr"]),
|
118
124
|
"lv0_files": input_files.get("lv0_files", None),
|
119
125
|
}
|
120
|
-
|
126
|
+
if data["radar"].altitude is None:
|
127
|
+
msg = "Radar altitude not defined"
|
128
|
+
raise RuntimeError(msg)
|
121
129
|
data["model"] = Model(input_files["model"], data["radar"].altitude)
|
122
130
|
time, height = _define_dense_grid()
|
123
131
|
valid_ind = _interpolate_to_cloudnet_grid()
|
124
132
|
if not valid_ind:
|
125
|
-
|
133
|
+
msg = "No overlapping radar and lidar timestamps found"
|
134
|
+
raise ValidTimeStampError(msg)
|
126
135
|
_screen_bad_time_indices(valid_ind)
|
127
|
-
if
|
136
|
+
if (
|
137
|
+
"rpg" in data["radar"].source_type.lower()
|
138
|
+
or "basta" in data["radar"].source_type.lower()
|
139
|
+
):
|
128
140
|
data["radar"].filter_speckle_noise()
|
129
141
|
data["radar"].filter_1st_gate_artifact()
|
130
142
|
for variable in ("v", "v_sigma", "ldr"):
|
@@ -142,17 +154,18 @@ def generate_categorize(
|
|
142
154
|
attributes = output.add_time_attribute(attributes, date, "model_time")
|
143
155
|
attributes = output.add_source_attribute(attributes, data)
|
144
156
|
output.update_attributes(cloudnet_arrays, attributes)
|
145
|
-
|
146
|
-
return uuid
|
157
|
+
return _save_cat(output_file, data, cloudnet_arrays, uuid)
|
147
158
|
finally:
|
148
159
|
_close_all()
|
149
160
|
|
150
161
|
|
151
162
|
def _save_cat(
|
152
|
-
full_path: str,
|
163
|
+
full_path: str,
|
164
|
+
data_obs: dict,
|
165
|
+
cloudnet_arrays: dict,
|
166
|
+
uuid: str | None,
|
153
167
|
) -> str:
|
154
168
|
"""Creates a categorize netCDF4 file and saves all data into it."""
|
155
|
-
|
156
169
|
dims = {
|
157
170
|
"time": len(data_obs["radar"].time),
|
158
171
|
"height": len(data_obs["radar"].height),
|
@@ -165,7 +178,9 @@ def _save_cat(
|
|
165
178
|
uuid_out = nc.file_uuid
|
166
179
|
nc.cloudnet_file_type = file_type
|
167
180
|
output.copy_global(
|
168
|
-
data_obs["radar"].dataset,
|
181
|
+
data_obs["radar"].dataset,
|
182
|
+
nc,
|
183
|
+
("year", "month", "day", "location"),
|
169
184
|
)
|
170
185
|
nc.title = f"Cloud categorization products from {data_obs['radar'].location}"
|
171
186
|
nc.source_file_uuids = output.get_source_uuids(*data_obs.values())
|
@@ -174,10 +189,11 @@ def _save_cat(
|
|
174
189
|
["https://doi.org/10.5194/amt-15-5343-2022"] if is_voodoo else None
|
175
190
|
)
|
176
191
|
nc.references = output.get_references(
|
177
|
-
identifier=file_type,
|
192
|
+
identifier=file_type,
|
193
|
+
extra=extra_references,
|
178
194
|
)
|
179
195
|
if is_voodoo:
|
180
|
-
import voodoonet.version
|
196
|
+
import voodoonet.version
|
181
197
|
|
182
198
|
nc.voodoonet_version = voodoonet.version.__version__
|
183
199
|
output.add_source_instruments(nc, data_obs)
|
@@ -316,7 +332,8 @@ CATEGORIZE_ATTRIBUTES = {
|
|
316
332
|
comment=COMMENTS["Z_sensitivity"],
|
317
333
|
),
|
318
334
|
"v_sigma": MetaData(
|
319
|
-
long_name="Standard deviation of mean Doppler velocity",
|
335
|
+
long_name="Standard deviation of mean Doppler velocity",
|
336
|
+
units="m s-1",
|
320
337
|
),
|
321
338
|
# Lidar variables
|
322
339
|
"beta": MetaData(
|
@@ -19,18 +19,22 @@ def classify_measurements(data: dict) -> ClassificationResult:
|
|
19
19
|
time / height grid before calling this function.
|
20
20
|
|
21
21
|
Args:
|
22
|
+
----
|
22
23
|
data: Containing :class:`Radar`, :class:`Lidar`, :class:`Model`
|
23
24
|
and :class:`Mwr` instances.
|
24
25
|
|
25
26
|
Returns:
|
27
|
+
-------
|
26
28
|
A :class:`ClassificationResult` instance.
|
27
29
|
|
28
30
|
References:
|
31
|
+
----------
|
29
32
|
The Cloudnet classification scheme is based on methodology proposed by
|
30
33
|
Hogan R. and O'Connor E., 2004, https://bit.ly/2Yjz9DZ and its
|
31
34
|
proprietary Matlab implementation.
|
32
35
|
|
33
36
|
Notes:
|
37
|
+
-----
|
34
38
|
Some individual classification methods are changed in this Python
|
35
39
|
implementation compared to the original Cloudnet methodology.
|
36
40
|
Especially methods classifying insects, melting layer and liquid droplets.
|
@@ -42,13 +46,14 @@ def classify_measurements(data: dict) -> ClassificationResult:
|
|
42
46
|
bits[2] = freezing.find_freezing_region(obs, bits[3])
|
43
47
|
liquid_from_lidar = droplet.find_liquid(obs)
|
44
48
|
if obs.lv0_files is not None and len(obs.lv0_files) > 0:
|
45
|
-
import voodoonet
|
49
|
+
import voodoonet
|
46
50
|
|
47
51
|
target_time = voodoonet.utils.decimal_hour2unix(obs.date, obs.time)
|
48
52
|
liquid_prob = voodoonet.infer(obs.lv0_files, target_time=target_time)
|
49
53
|
liquid_from_radar = liquid_prob > 0.55
|
50
54
|
liquid_from_radar = _remove_false_radar_liquid(
|
51
|
-
liquid_from_radar,
|
55
|
+
liquid_from_radar,
|
56
|
+
liquid_from_lidar,
|
52
57
|
)
|
53
58
|
bits[0] = liquid_from_radar | liquid_from_lidar
|
54
59
|
else:
|
@@ -73,27 +78,32 @@ def classify_measurements(data: dict) -> ClassificationResult:
|
|
73
78
|
|
74
79
|
|
75
80
|
def _remove_false_radar_liquid(
|
76
|
-
liquid_from_radar: np.ndarray,
|
81
|
+
liquid_from_radar: np.ndarray,
|
82
|
+
liquid_from_lidar: np.ndarray,
|
77
83
|
) -> np.ndarray:
|
78
84
|
"""Removes radar-liquid below lidar-detected liquid bases."""
|
79
85
|
lidar_liquid_bases = cloudnetpy.categorize.atmos.find_cloud_bases(liquid_from_lidar)
|
80
|
-
for prof, base in zip(*np.where(lidar_liquid_bases)):
|
86
|
+
for prof, base in zip(*np.where(lidar_liquid_bases), strict=True):
|
81
87
|
liquid_from_radar[prof, 0:base] = 0
|
82
88
|
return liquid_from_radar
|
83
89
|
|
84
90
|
|
85
91
|
def fetch_quality(
|
86
|
-
data: dict,
|
92
|
+
data: dict,
|
93
|
+
classification: ClassificationResult,
|
94
|
+
attenuations: dict,
|
87
95
|
) -> dict:
|
88
96
|
"""Returns Cloudnet quality bits.
|
89
97
|
|
90
98
|
Args:
|
99
|
+
----
|
91
100
|
data: Containing :class:`Radar` and :class:`Lidar` instances.
|
92
101
|
classification: A :class:`ClassificationResult` instance.
|
93
102
|
attenuations: Dictionary containing keys `liquid_corrected`,
|
94
103
|
`liquid_uncorrected`.
|
95
104
|
|
96
105
|
Returns:
|
106
|
+
-------
|
97
107
|
Dictionary containing `quality_bits`, an integer array with the bits:
|
98
108
|
|
99
109
|
- bit 0: Pixel contains radar data
|
@@ -117,18 +127,22 @@ def fetch_quality(
|
|
117
127
|
|
118
128
|
|
119
129
|
def _find_aerosols(
|
120
|
-
obs: ClassData,
|
130
|
+
obs: ClassData,
|
131
|
+
is_falling: np.ndarray,
|
132
|
+
is_liquid: np.ndarray,
|
121
133
|
) -> np.ndarray:
|
122
134
|
"""Estimates aerosols from lidar backscattering.
|
123
135
|
|
124
136
|
Aerosols are lidar signals that are: a) not falling, b) not liquid droplets.
|
125
137
|
|
126
138
|
Args:
|
139
|
+
----
|
127
140
|
obs: A :class:`ClassData` instance.
|
128
141
|
is_falling: 2-D boolean array of falling hydrometeors.
|
129
142
|
is_liquid: 2-D boolean array of liquid droplets.
|
130
143
|
|
131
144
|
Returns:
|
145
|
+
-------
|
132
146
|
2-D boolean array containing aerosols.
|
133
147
|
|
134
148
|
"""
|
@@ -145,16 +159,20 @@ def _fix_undetected_melting_layer(bits: list) -> np.ndarray:
|
|
145
159
|
|
146
160
|
|
147
161
|
def _find_drizzle_and_falling(
|
148
|
-
is_liquid: np.ndarray,
|
162
|
+
is_liquid: np.ndarray,
|
163
|
+
is_falling: np.ndarray,
|
164
|
+
is_freezing: np.ndarray,
|
149
165
|
) -> np.ndarray:
|
150
166
|
"""Classifies pixels as falling, drizzle and others.
|
151
167
|
|
152
168
|
Args:
|
169
|
+
----
|
153
170
|
is_liquid: 2D boolean array denoting liquid layers.
|
154
171
|
is_falling: 2D boolean array denoting falling pixels.
|
155
172
|
is_freezing: 2D boolean array denoting subzero temperatures.
|
156
173
|
|
157
174
|
Returns:
|
175
|
+
-------
|
158
176
|
2D array where values are 1 (falling, drizzle, supercooled liquids),
|
159
177
|
2 (drizzle), and masked (all others).
|
160
178
|
|
@@ -173,10 +191,12 @@ def _bits_to_integer(bits: list) -> np.ndarray:
|
|
173
191
|
"""Creates array of integers from individual boolean arrays.
|
174
192
|
|
175
193
|
Args:
|
194
|
+
----
|
176
195
|
bits: List of bit fields (of similar sizes) to be saved in the resulting
|
177
196
|
array of integers. bits[0] is saved as bit 0, bits[1] as bit 1, etc.
|
178
197
|
|
179
198
|
Returns:
|
199
|
+
-------
|
180
200
|
Array of integers containing the information of the individual boolean arrays.
|
181
201
|
|
182
202
|
"""
|
@@ -201,7 +221,7 @@ def _filter_insects(bits: list) -> list:
|
|
201
221
|
# remove around melting layer:
|
202
222
|
original_insects = np.copy(is_insects)
|
203
223
|
n_gates = 5
|
204
|
-
for x, y in zip(*np.where(is_melting_layer)):
|
224
|
+
for x, y in zip(*np.where(is_melting_layer), strict=True):
|
205
225
|
try:
|
206
226
|
# change insects to drizzle below melting layer pixel
|
207
227
|
ind1 = np.arange(y - n_gates, y)
|
@@ -229,7 +249,9 @@ def _filter_falling(bits: list) -> tuple:
|
|
229
249
|
is_freezing = bits[2]
|
230
250
|
is_falling = bits[1]
|
231
251
|
is_falling_filtered = skimage.morphology.remove_small_objects(
|
232
|
-
is_falling,
|
252
|
+
is_falling,
|
253
|
+
10,
|
254
|
+
connectivity=1,
|
233
255
|
)
|
234
256
|
is_filtered = is_falling & ~np.array(is_falling_filtered)
|
235
257
|
ice_ind = np.where(is_freezing & is_filtered)
|