cloudnetpy 1.80.8__py3-none-any.whl → 1.81.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.
- cloudnetpy/categorize/__init__.py +1 -1
- cloudnetpy/categorize/atmos_utils.py +31 -27
- cloudnetpy/categorize/attenuations/__init__.py +4 -4
- cloudnetpy/categorize/attenuations/liquid_attenuation.py +7 -5
- cloudnetpy/categorize/attenuations/melting_attenuation.py +3 -3
- cloudnetpy/categorize/attenuations/rain_attenuation.py +4 -4
- cloudnetpy/categorize/categorize.py +25 -11
- cloudnetpy/categorize/classify.py +9 -8
- cloudnetpy/categorize/containers.py +13 -10
- cloudnetpy/categorize/disdrometer.py +5 -3
- cloudnetpy/categorize/droplet.py +12 -9
- cloudnetpy/categorize/falling.py +9 -8
- cloudnetpy/categorize/freezing.py +10 -7
- cloudnetpy/categorize/insects.py +18 -17
- cloudnetpy/categorize/lidar.py +7 -3
- cloudnetpy/categorize/melting.py +16 -15
- cloudnetpy/categorize/model.py +17 -10
- cloudnetpy/categorize/mwr.py +5 -3
- cloudnetpy/categorize/radar.py +15 -13
- cloudnetpy/cli.py +10 -8
- cloudnetpy/cloudnetarray.py +8 -7
- cloudnetpy/concat_lib.py +29 -20
- cloudnetpy/datasource.py +26 -21
- cloudnetpy/exceptions.py +12 -10
- cloudnetpy/instruments/basta.py +19 -9
- cloudnetpy/instruments/bowtie.py +18 -11
- cloudnetpy/instruments/ceilo.py +22 -10
- cloudnetpy/instruments/ceilometer.py +33 -34
- cloudnetpy/instruments/cl61d.py +5 -3
- cloudnetpy/instruments/cloudnet_instrument.py +7 -7
- cloudnetpy/instruments/copernicus.py +16 -7
- cloudnetpy/instruments/disdrometer/common.py +5 -4
- cloudnetpy/instruments/disdrometer/parsivel.py +14 -9
- cloudnetpy/instruments/disdrometer/thies.py +11 -7
- cloudnetpy/instruments/fd12p.py +7 -6
- cloudnetpy/instruments/galileo.py +16 -7
- cloudnetpy/instruments/hatpro.py +33 -24
- cloudnetpy/instruments/lufft.py +6 -4
- cloudnetpy/instruments/mira.py +33 -19
- cloudnetpy/instruments/mrr.py +12 -12
- cloudnetpy/instruments/nc_lidar.py +1 -1
- cloudnetpy/instruments/nc_radar.py +8 -8
- cloudnetpy/instruments/pollyxt.py +19 -12
- cloudnetpy/instruments/radiometrics.py +17 -10
- cloudnetpy/instruments/rain_e_h3.py +9 -5
- cloudnetpy/instruments/rpg.py +32 -21
- cloudnetpy/instruments/rpg_reader.py +15 -12
- cloudnetpy/instruments/vaisala.py +32 -24
- cloudnetpy/instruments/weather_station.py +22 -19
- cloudnetpy/model_evaluation/file_handler.py +27 -29
- cloudnetpy/model_evaluation/plotting/plot_tools.py +7 -5
- cloudnetpy/model_evaluation/plotting/plotting.py +41 -32
- cloudnetpy/model_evaluation/products/advance_methods.py +38 -34
- cloudnetpy/model_evaluation/products/grid_methods.py +10 -9
- cloudnetpy/model_evaluation/products/model_products.py +15 -9
- cloudnetpy/model_evaluation/products/observation_products.py +12 -10
- cloudnetpy/model_evaluation/products/product_resampling.py +11 -7
- cloudnetpy/model_evaluation/products/tools.py +18 -14
- cloudnetpy/model_evaluation/statistics/statistical_methods.py +6 -5
- cloudnetpy/model_evaluation/tests/unit/test_plotting.py +18 -25
- cloudnetpy/model_evaluation/utils.py +3 -3
- cloudnetpy/output.py +15 -32
- cloudnetpy/plotting/plotting.py +22 -12
- cloudnetpy/products/classification.py +15 -9
- cloudnetpy/products/der.py +24 -19
- cloudnetpy/products/drizzle.py +21 -13
- cloudnetpy/products/drizzle_error.py +8 -7
- cloudnetpy/products/drizzle_tools.py +27 -23
- cloudnetpy/products/epsilon.py +6 -5
- cloudnetpy/products/ier.py +11 -5
- cloudnetpy/products/iwc.py +18 -9
- cloudnetpy/products/lwc.py +41 -31
- cloudnetpy/products/mwr_tools.py +30 -19
- cloudnetpy/products/product_tools.py +23 -19
- cloudnetpy/utils.py +84 -98
- cloudnetpy/version.py +2 -2
- {cloudnetpy-1.80.8.dist-info → cloudnetpy-1.81.0.dist-info}/METADATA +2 -1
- cloudnetpy-1.81.0.dist-info/RECORD +126 -0
- cloudnetpy-1.80.8.dist-info/RECORD +0 -126
- {cloudnetpy-1.80.8.dist-info → cloudnetpy-1.81.0.dist-info}/WHEEL +0 -0
- {cloudnetpy-1.80.8.dist-info → cloudnetpy-1.81.0.dist-info}/entry_points.txt +0 -0
- {cloudnetpy-1.80.8.dist-info → cloudnetpy-1.81.0.dist-info}/licenses/LICENSE +0 -0
- {cloudnetpy-1.80.8.dist-info → cloudnetpy-1.81.0.dist-info}/top_level.txt +0 -0
cloudnetpy/products/lwc.py
CHANGED
@@ -2,7 +2,11 @@
|
|
2
2
|
method.
|
3
3
|
"""
|
4
4
|
|
5
|
+
from os import PathLike
|
6
|
+
from uuid import UUID
|
7
|
+
|
5
8
|
import numpy as np
|
9
|
+
import numpy.typing as npt
|
6
10
|
from numpy import ma
|
7
11
|
|
8
12
|
from cloudnetpy import output, utils
|
@@ -15,10 +19,10 @@ from cloudnetpy.products.product_tools import CategorizeBits, get_is_rain
|
|
15
19
|
|
16
20
|
|
17
21
|
def generate_lwc(
|
18
|
-
categorize_file: str,
|
19
|
-
output_file: str,
|
20
|
-
uuid: str | None = None,
|
21
|
-
) ->
|
22
|
+
categorize_file: str | PathLike,
|
23
|
+
output_file: str | PathLike,
|
24
|
+
uuid: str | UUID | None = None,
|
25
|
+
) -> UUID:
|
22
26
|
"""Generates Cloudnet liquid water content product.
|
23
27
|
|
24
28
|
This function calculates cloud liquid water content using the so-called
|
@@ -47,6 +51,7 @@ def generate_lwc(
|
|
47
51
|
Bull. Amer. Meteor. Soc., 88, 883–898, https://doi.org/10.1175/BAMS-88-6-883
|
48
52
|
|
49
53
|
"""
|
54
|
+
uuid = utils.get_uuid(uuid)
|
50
55
|
with LwcSource(categorize_file) as lwc_source:
|
51
56
|
lwc = Lwc(lwc_source)
|
52
57
|
clouds = CloudAdjustor(lwc_source, lwc)
|
@@ -55,7 +60,7 @@ def generate_lwc(
|
|
55
60
|
date = lwc_source.get_date()
|
56
61
|
attributes = output.add_time_attribute(LWC_ATTRIBUTES, date)
|
57
62
|
output.update_attributes(lwc_source.data, attributes)
|
58
|
-
|
63
|
+
output.save_product_file(
|
59
64
|
"lwc",
|
60
65
|
lwc_source,
|
61
66
|
output_file,
|
@@ -65,6 +70,7 @@ def generate_lwc(
|
|
65
70
|
"lwp_error",
|
66
71
|
),
|
67
72
|
)
|
73
|
+
return uuid
|
68
74
|
|
69
75
|
|
70
76
|
class LwcSource(DataSource):
|
@@ -87,7 +93,7 @@ class LwcSource(DataSource):
|
|
87
93
|
|
88
94
|
"""
|
89
95
|
|
90
|
-
def __init__(self, categorize_file: str):
|
96
|
+
def __init__(self, categorize_file: str | PathLike) -> None:
|
91
97
|
super().__init__(categorize_file)
|
92
98
|
if "lwp" not in self.dataset.variables:
|
93
99
|
msg = "Liquid water path missing from the categorize file."
|
@@ -102,16 +108,18 @@ class LwcSource(DataSource):
|
|
102
108
|
|
103
109
|
def append_results(
|
104
110
|
self,
|
105
|
-
lwc:
|
106
|
-
status:
|
107
|
-
error:
|
111
|
+
lwc: npt.NDArray,
|
112
|
+
status: npt.NDArray,
|
113
|
+
error: npt.NDArray,
|
108
114
|
) -> None:
|
109
115
|
self.append_data(lwc, "lwc", units="kg m-3")
|
110
116
|
self.append_data(status, "lwc_retrieval_status")
|
111
117
|
self.append_data(error, "lwc_error", units="dB")
|
112
118
|
|
113
119
|
@staticmethod
|
114
|
-
def _get_atmosphere(
|
120
|
+
def _get_atmosphere(
|
121
|
+
categorize_file: str | PathLike,
|
122
|
+
) -> tuple[npt.NDArray, npt.NDArray]:
|
115
123
|
fields = ["temperature", "pressure"]
|
116
124
|
atmosphere = p_tools.interpolate_model(categorize_file, fields)
|
117
125
|
return atmosphere["temperature"], atmosphere["pressure"]
|
@@ -131,7 +139,7 @@ class Lwc:
|
|
131
139
|
|
132
140
|
"""
|
133
141
|
|
134
|
-
def __init__(self, lwc_source: LwcSource):
|
142
|
+
def __init__(self, lwc_source: LwcSource) -> None:
|
135
143
|
self.lwc_source = lwc_source
|
136
144
|
self.height = lwc_source.getvar("height")
|
137
145
|
self.is_liquid = self._get_liquid()
|
@@ -139,11 +147,11 @@ class Lwc:
|
|
139
147
|
self.lwc = self._adiabatic_lwc_to_lwc()
|
140
148
|
self._mask_rain()
|
141
149
|
|
142
|
-
def _get_liquid(self) ->
|
150
|
+
def _get_liquid(self) -> npt.NDArray:
|
143
151
|
category_bits = self.lwc_source.categorize_bits.category_bits
|
144
152
|
return category_bits.droplet
|
145
153
|
|
146
|
-
def _init_lwc_adiabatic(self) ->
|
154
|
+
def _init_lwc_adiabatic(self) -> npt.NDArray:
|
147
155
|
"""Returns theoretical adiabatic lwc in liquid clouds (kg/m3)."""
|
148
156
|
lwc_dz = atmos_utils.fill_clouds_with_lwc_dz(
|
149
157
|
*self.lwc_source.atmosphere,
|
@@ -151,7 +159,7 @@ class Lwc:
|
|
151
159
|
)
|
152
160
|
return atmos_utils.calc_adiabatic_lwc(lwc_dz, self.height)
|
153
161
|
|
154
|
-
def _adiabatic_lwc_to_lwc(self) ->
|
162
|
+
def _adiabatic_lwc_to_lwc(self) -> npt.NDArray:
|
155
163
|
"""Initialises liquid water content (kg/m3).
|
156
164
|
|
157
165
|
Calculates LWC for ALL profiles (rain, lwp > theoretical, etc.),
|
@@ -184,7 +192,7 @@ class CloudAdjustor:
|
|
184
192
|
|
185
193
|
"""
|
186
194
|
|
187
|
-
def __init__(self, lwc_source: LwcSource, lwc: Lwc):
|
195
|
+
def __init__(self, lwc_source: LwcSource, lwc: Lwc) -> None:
|
188
196
|
self.lwc_source = lwc_source
|
189
197
|
self.lwc = lwc.lwc
|
190
198
|
self.is_liquid = lwc.is_liquid
|
@@ -204,7 +212,7 @@ class CloudAdjustor:
|
|
204
212
|
status[self.is_liquid] = 1
|
205
213
|
return status
|
206
214
|
|
207
|
-
def _adjust_cloud_tops(self, adjustable_clouds:
|
215
|
+
def _adjust_cloud_tops(self, adjustable_clouds: npt.NDArray) -> None:
|
208
216
|
"""Adjusts cloud top index so that measured lwc corresponds to theoretical
|
209
217
|
value.
|
210
218
|
"""
|
@@ -213,7 +221,7 @@ class CloudAdjustor:
|
|
213
221
|
self._update_status(time_index)
|
214
222
|
self._adjust_lwc(time_index, base_index)
|
215
223
|
|
216
|
-
def _update_status(self, time_ind:
|
224
|
+
def _update_status(self, time_ind: npt.NDArray) -> None:
|
217
225
|
alt_indices = np.where(self.is_liquid[time_ind, :])[0]
|
218
226
|
self.status[time_ind, alt_indices] = 2
|
219
227
|
|
@@ -237,7 +245,7 @@ class CloudAdjustor:
|
|
237
245
|
def _out_of_bound(self, ind: int) -> bool:
|
238
246
|
return ind >= self.lwc.shape[1] - 1
|
239
247
|
|
240
|
-
def _find_adjustable_clouds(self) ->
|
248
|
+
def _find_adjustable_clouds(self) -> npt.NDArray:
|
241
249
|
top_clouds = self._find_topmost_clouds()
|
242
250
|
detection_type = self._find_echo_combinations_in_liquid()
|
243
251
|
detection_type[~top_clouds] = 0
|
@@ -245,7 +253,7 @@ class CloudAdjustor:
|
|
245
253
|
top_clouds[~lidar_only_clouds, :] = 0
|
246
254
|
return self._remove_good_profiles(top_clouds)
|
247
255
|
|
248
|
-
def _find_topmost_clouds(self) ->
|
256
|
+
def _find_topmost_clouds(self) -> npt.NDArray:
|
249
257
|
top_clouds = np.copy(self.is_liquid)
|
250
258
|
cloud_edges = top_clouds[:, :-1][:, ::-1] < top_clouds[:, 1:][:, ::-1]
|
251
259
|
topmost_bases = self.is_liquid.shape[1] - 1 - np.argmax(cloud_edges, axis=1)
|
@@ -253,14 +261,14 @@ class CloudAdjustor:
|
|
253
261
|
top_clouds[n, :base] = 0
|
254
262
|
return top_clouds
|
255
263
|
|
256
|
-
def _find_echo_combinations_in_liquid(self) ->
|
264
|
+
def _find_echo_combinations_in_liquid(self) -> npt.NDArray:
|
257
265
|
"""Classifies liquid clouds by detection type: 1=lidar, 2=radar, 3=both."""
|
258
266
|
lidar_detected = (self.is_liquid & self.echo["lidar"]).astype(int)
|
259
267
|
radar_detected = (self.is_liquid & self.echo["radar"]).astype(int) * 2
|
260
268
|
return lidar_detected + radar_detected
|
261
269
|
|
262
270
|
@staticmethod
|
263
|
-
def _find_lidar_only_clouds(detection:
|
271
|
+
def _find_lidar_only_clouds(detection: npt.NDArray) -> npt.NDArray:
|
264
272
|
"""Finds top clouds that contain only lidar-detected pixels.
|
265
273
|
|
266
274
|
Args:
|
@@ -274,14 +282,14 @@ class CloudAdjustor:
|
|
274
282
|
sum_of_detection_type = ma.sum(detection, axis=1)
|
275
283
|
return sum_of_cloud_pixels / sum_of_detection_type == 1
|
276
284
|
|
277
|
-
def _remove_good_profiles(self, top_clouds:
|
285
|
+
def _remove_good_profiles(self, top_clouds: npt.NDArray) -> npt.NDArray:
|
278
286
|
no_rain = ~self.lwc_source.is_rain.astype(bool)
|
279
287
|
lwp_difference = self._find_lwp_difference()
|
280
288
|
dubious_profiles = (lwp_difference < 0) & no_rain
|
281
289
|
top_clouds[~dubious_profiles, :] = 0
|
282
290
|
return top_clouds
|
283
291
|
|
284
|
-
def _find_lwp_difference(self) ->
|
292
|
+
def _find_lwp_difference(self) -> npt.NDArray:
|
285
293
|
"""Returns difference of theoretical LWP and measured LWP (g/m2).
|
286
294
|
|
287
295
|
In theory, this difference should be always positive. Negative values
|
@@ -313,13 +321,13 @@ class LwcError:
|
|
313
321
|
|
314
322
|
"""
|
315
323
|
|
316
|
-
def __init__(self, lwc_source: LwcSource, lwc: Lwc):
|
324
|
+
def __init__(self, lwc_source: LwcSource, lwc: Lwc) -> None:
|
317
325
|
self.lwc = lwc.lwc
|
318
326
|
self.lwc_source = lwc_source
|
319
327
|
self.error = self._calculate_lwc_error()
|
320
328
|
self._mask_rain()
|
321
329
|
|
322
|
-
def _calculate_lwc_error(self) ->
|
330
|
+
def _calculate_lwc_error(self) -> npt.NDArray:
|
323
331
|
lwc_relative_error = self._calc_lwc_relative_error()
|
324
332
|
lwp_relative_error = self._calc_lwp_relative_error()
|
325
333
|
combined_error = self._calc_combined_error(
|
@@ -328,34 +336,36 @@ class LwcError:
|
|
328
336
|
)
|
329
337
|
return self._fill_error_array(combined_error)
|
330
338
|
|
331
|
-
def _calc_lwc_relative_error(self) ->
|
339
|
+
def _calc_lwc_relative_error(self) -> npt.NDArray:
|
332
340
|
lwc_gradient = self._calc_lwc_gradient()
|
333
341
|
error = lwc_gradient / self.lwc / 2
|
334
342
|
return self._limit_error(error, 5)
|
335
343
|
|
336
|
-
def _calc_lwc_gradient(self) ->
|
344
|
+
def _calc_lwc_gradient(self) -> npt.NDArray:
|
337
345
|
if not isinstance(self.lwc, ma.MaskedArray):
|
338
346
|
self.lwc = ma.masked_array(self.lwc)
|
339
347
|
gradient_elements = np.gradient(self.lwc.filled(0))
|
340
348
|
return utils.l2norm(*gradient_elements)
|
341
349
|
|
342
|
-
def _calc_lwp_relative_error(self) ->
|
350
|
+
def _calc_lwp_relative_error(self) -> npt.NDArray:
|
343
351
|
err = self.lwc_source.lwp_error
|
344
352
|
value = self.lwc_source.lwp
|
345
353
|
error = np.divide(err, value, out=np.zeros_like(err), where=value != 0)
|
346
354
|
return self._limit_error(error, 10)
|
347
355
|
|
348
356
|
@staticmethod
|
349
|
-
def _limit_error(error:
|
357
|
+
def _limit_error(error: npt.NDArray, max_value: float) -> npt.NDArray:
|
350
358
|
error[error > max_value] = max_value
|
351
359
|
return error
|
352
360
|
|
353
361
|
@staticmethod
|
354
|
-
def _calc_combined_error(
|
362
|
+
def _calc_combined_error(
|
363
|
+
error_2d: npt.NDArray, error_1d: npt.NDArray
|
364
|
+
) -> npt.NDArray:
|
355
365
|
error_1d_transposed = utils.transpose(error_1d)
|
356
366
|
return utils.l2norm(error_2d, error_1d_transposed)
|
357
367
|
|
358
|
-
def _fill_error_array(self, error_in:
|
368
|
+
def _fill_error_array(self, error_in: npt.NDArray) -> ma.MaskedArray:
|
359
369
|
lwc_error = ma.masked_all(self.lwc.shape)
|
360
370
|
ind = ma.where(self.lwc)
|
361
371
|
lwc_error[ind] = error_in[ind]
|
cloudnetpy/products/mwr_tools.py
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
import os
|
2
2
|
import tempfile
|
3
|
+
from os import PathLike
|
3
4
|
from typing import TYPE_CHECKING, Literal
|
5
|
+
from uuid import UUID
|
4
6
|
|
5
7
|
import netCDF4
|
6
8
|
import numpy as np
|
@@ -19,8 +21,10 @@ if TYPE_CHECKING:
|
|
19
21
|
|
20
22
|
|
21
23
|
def generate_mwr_single(
|
22
|
-
mwr_l1c_file: str
|
23
|
-
|
24
|
+
mwr_l1c_file: str | PathLike,
|
25
|
+
output_file: str | PathLike,
|
26
|
+
uuid: str | UUID | None = None,
|
27
|
+
) -> UUID:
|
24
28
|
"""Generates MWR single-pointing product including liquid water path, integrated
|
25
29
|
water vapor, etc. from zenith measurements.
|
26
30
|
|
@@ -39,8 +43,10 @@ def generate_mwr_single(
|
|
39
43
|
|
40
44
|
|
41
45
|
def generate_mwr_lhumpro(
|
42
|
-
mwr_l1c_file: str
|
43
|
-
|
46
|
+
mwr_l1c_file: str | PathLike,
|
47
|
+
output_file: str | PathLike,
|
48
|
+
uuid: str | UUID | None = None,
|
49
|
+
) -> UUID:
|
44
50
|
"""Generates LHUMPRO single-pointing product including liquid water path, integrated
|
45
51
|
water vapor, etc. from zenith measurements.
|
46
52
|
|
@@ -59,8 +65,10 @@ def generate_mwr_lhumpro(
|
|
59
65
|
|
60
66
|
|
61
67
|
def generate_mwr_multi(
|
62
|
-
mwr_l1c_file: str
|
63
|
-
|
68
|
+
mwr_l1c_file: str | PathLike,
|
69
|
+
output_file: str | PathLike,
|
70
|
+
uuid: str | UUID | None = None,
|
71
|
+
) -> UUID:
|
64
72
|
"""Generates MWR multiple-pointing product, including relative humidity profiles,
|
65
73
|
etc. from scanning measurements.
|
66
74
|
|
@@ -77,11 +85,12 @@ def generate_mwr_multi(
|
|
77
85
|
|
78
86
|
|
79
87
|
def _generate_product(
|
80
|
-
mwr_l1c_file: str,
|
81
|
-
output_file: str,
|
82
|
-
uuid: str | None,
|
88
|
+
mwr_l1c_file: str | PathLike,
|
89
|
+
output_file: str | PathLike,
|
90
|
+
uuid: str | UUID | None,
|
83
91
|
product: Literal["multi", "single", "lhumpro"],
|
84
|
-
) ->
|
92
|
+
) -> UUID:
|
93
|
+
uuid = utils.get_uuid(uuid)
|
85
94
|
fun: Callable
|
86
95
|
if product == "multi":
|
87
96
|
fun = gen_multi
|
@@ -115,28 +124,28 @@ def _generate_product(
|
|
115
124
|
|
116
125
|
class Mwr:
|
117
126
|
def __init__(
|
118
|
-
self, nc_l1c: netCDF4.Dataset, nc_l2: netCDF4.Dataset, uuid:
|
119
|
-
):
|
127
|
+
self, nc_l1c: netCDF4.Dataset, nc_l2: netCDF4.Dataset, uuid: UUID
|
128
|
+
) -> None:
|
120
129
|
self.nc_l1c = nc_l1c
|
121
130
|
self.nc_l2 = nc_l2
|
122
|
-
self.uuid = uuid
|
131
|
+
self.uuid = uuid
|
123
132
|
|
124
|
-
def harmonize(self, product: Literal["multi", "single"]) ->
|
133
|
+
def harmonize(self, product: Literal["multi", "single"]) -> UUID:
|
125
134
|
self._truncate_global_attributes()
|
126
135
|
self._copy_global_attributes()
|
127
136
|
self._fix_variable_attributes()
|
128
137
|
self._write_missing_global_attributes(product)
|
129
138
|
return self.uuid
|
130
139
|
|
131
|
-
def _truncate_global_attributes(self):
|
140
|
+
def _truncate_global_attributes(self) -> None:
|
132
141
|
for attr in self.nc_l2.ncattrs():
|
133
142
|
delattr(self.nc_l2, attr)
|
134
143
|
|
135
|
-
def _copy_global_attributes(self):
|
144
|
+
def _copy_global_attributes(self) -> None:
|
136
145
|
keys = ("year", "month", "day", "location", "source")
|
137
146
|
output.copy_global(self.nc_l1c, self.nc_l2, keys)
|
138
147
|
|
139
|
-
def _fix_variable_attributes(self):
|
148
|
+
def _fix_variable_attributes(self) -> None:
|
140
149
|
output.replace_attribute_with_standard_value(
|
141
150
|
self.nc_l2,
|
142
151
|
(
|
@@ -151,7 +160,9 @@ class Mwr:
|
|
151
160
|
("units", "long_name", "standard_name"),
|
152
161
|
)
|
153
162
|
|
154
|
-
def _write_missing_global_attributes(
|
163
|
+
def _write_missing_global_attributes(
|
164
|
+
self, product: Literal["multi", "single"]
|
165
|
+
) -> None:
|
155
166
|
output.add_standard_global_attributes(self.nc_l2, self.uuid)
|
156
167
|
product_type = "multiple-pointing" if product == "multi" else "single-pointing"
|
157
168
|
self.nc_l2.title = f"MWR {product_type} from {self.nc_l1c.location}"
|
@@ -166,7 +177,7 @@ class Mwr:
|
|
166
177
|
self.nc_l2.instrument_pid = self.nc_l1c.instrument_pid
|
167
178
|
|
168
179
|
|
169
|
-
def _read_mwrpy_coeffs(mwr_l1c_file, folder: str) -> list:
|
180
|
+
def _read_mwrpy_coeffs(mwr_l1c_file: str | PathLike, folder: str) -> list[str]:
|
170
181
|
with netCDF4.Dataset(mwr_l1c_file) as nc:
|
171
182
|
links = nc.mwrpy_coefficients.split(", ")
|
172
183
|
coeffs = []
|
@@ -1,10 +1,12 @@
|
|
1
1
|
"""General helper classes and functions for all products."""
|
2
2
|
|
3
3
|
from dataclasses import dataclass
|
4
|
+
from os import PathLike
|
4
5
|
from typing import NamedTuple
|
5
6
|
|
6
7
|
import netCDF4
|
7
8
|
import numpy as np
|
9
|
+
import numpy.typing as npt
|
8
10
|
from numpy import ma
|
9
11
|
from numpy.typing import NDArray
|
10
12
|
|
@@ -48,7 +50,7 @@ class QualityBits:
|
|
48
50
|
|
49
51
|
|
50
52
|
class CategorizeBits:
|
51
|
-
def __init__(self, categorize_file: str):
|
53
|
+
def __init__(self, categorize_file: str | PathLike) -> None:
|
52
54
|
self._categorize_file = categorize_file
|
53
55
|
self.category_bits = self._read_category_bits()
|
54
56
|
self.quality_bits = self._read_quality_bits()
|
@@ -94,7 +96,7 @@ class ProductClassification(CategorizeBits):
|
|
94
96
|
|
95
97
|
"""
|
96
98
|
|
97
|
-
def __init__(self, categorize_file: str):
|
99
|
+
def __init__(self, categorize_file: str | PathLike) -> None:
|
98
100
|
super().__init__(categorize_file)
|
99
101
|
self.is_rain = get_is_rain(categorize_file)
|
100
102
|
|
@@ -104,7 +106,7 @@ class IceClassification(ProductClassification):
|
|
104
106
|
Child of ProductClassification().
|
105
107
|
"""
|
106
108
|
|
107
|
-
def __init__(self, categorize_file: str):
|
109
|
+
def __init__(self, categorize_file: str | PathLike) -> None:
|
108
110
|
super().__init__(categorize_file)
|
109
111
|
self._is_attenuated = self._find_attenuated()
|
110
112
|
self._is_corrected = self._find_corrected()
|
@@ -115,28 +117,28 @@ class IceClassification(ProductClassification):
|
|
115
117
|
self.ice_above_rain = self._find_ice_above_rain()
|
116
118
|
self.clear_above_rain = self._find_clear_above_rain()
|
117
119
|
|
118
|
-
def _find_clear_above_rain(self) ->
|
120
|
+
def _find_clear_above_rain(self) -> npt.NDArray:
|
119
121
|
return (
|
120
122
|
utils.transpose(self.is_rain) * ~self.is_ice
|
121
123
|
& self.category_bits.freezing
|
122
124
|
& ~self.category_bits.melting
|
123
125
|
)
|
124
126
|
|
125
|
-
def _find_attenuated(self) ->
|
127
|
+
def _find_attenuated(self) -> npt.NDArray:
|
126
128
|
return (
|
127
129
|
self.quality_bits.attenuated_liquid
|
128
130
|
| self.quality_bits.attenuated_rain
|
129
131
|
| self.quality_bits.attenuated_melting
|
130
132
|
)
|
131
133
|
|
132
|
-
def _find_corrected(self) ->
|
134
|
+
def _find_corrected(self) -> npt.NDArray:
|
133
135
|
return (
|
134
136
|
self.quality_bits.corrected_liquid
|
135
137
|
| self.quality_bits.corrected_rain
|
136
138
|
| self.quality_bits.corrected_melting
|
137
139
|
)
|
138
140
|
|
139
|
-
def _find_ice(self) ->
|
141
|
+
def _find_ice(self) -> npt.NDArray:
|
140
142
|
return (
|
141
143
|
self.category_bits.falling
|
142
144
|
& self.category_bits.freezing
|
@@ -144,7 +146,7 @@ class IceClassification(ProductClassification):
|
|
144
146
|
& ~self.category_bits.insect
|
145
147
|
)
|
146
148
|
|
147
|
-
def _find_would_be_ice(self) ->
|
149
|
+
def _find_would_be_ice(self) -> npt.NDArray:
|
148
150
|
warm_falling = (
|
149
151
|
self.category_bits.falling
|
150
152
|
& ~self.category_bits.freezing
|
@@ -152,10 +154,10 @@ class IceClassification(ProductClassification):
|
|
152
154
|
)
|
153
155
|
return warm_falling | self.category_bits.melting
|
154
156
|
|
155
|
-
def _find_corrected_ice(self) ->
|
157
|
+
def _find_corrected_ice(self) -> npt.NDArray:
|
156
158
|
return self.is_ice & self._is_attenuated & self._is_corrected
|
157
159
|
|
158
|
-
def _find_uncorrected_ice(self) ->
|
160
|
+
def _find_uncorrected_ice(self) -> npt.NDArray:
|
159
161
|
uncorrected_melting = (
|
160
162
|
self.quality_bits.attenuated_melting & ~self.quality_bits.corrected_melting
|
161
163
|
)
|
@@ -171,7 +173,7 @@ class IceClassification(ProductClassification):
|
|
171
173
|
& (uncorrected_melting | uncorrected_rain | uncorrected_liquid)
|
172
174
|
)
|
173
175
|
|
174
|
-
def _find_ice_above_rain(self) ->
|
176
|
+
def _find_ice_above_rain(self) -> npt.NDArray:
|
175
177
|
is_rain = utils.transpose(self.is_rain)
|
176
178
|
return (self.is_ice * is_rain) == 1
|
177
179
|
|
@@ -179,7 +181,7 @@ class IceClassification(ProductClassification):
|
|
179
181
|
class IceSource(DataSource):
|
180
182
|
"""Base class for different ice products."""
|
181
183
|
|
182
|
-
def __init__(self, categorize_file: str, product: str):
|
184
|
+
def __init__(self, categorize_file: str | PathLike, product: str) -> None:
|
183
185
|
super().__init__(categorize_file)
|
184
186
|
self.radar_frequency = float(self.getvar("radar_frequency"))
|
185
187
|
self.wl_band = utils.get_wl_band(self.radar_frequency)
|
@@ -230,7 +232,7 @@ class IceSource(DataSource):
|
|
230
232
|
return IceCoefficients(0.669, 0.000580, -0.00706, 0.0923, -0.992)
|
231
233
|
raise ValueError(msg)
|
232
234
|
|
233
|
-
def _convert_z(self, z_variable: str = "Z") ->
|
235
|
+
def _convert_z(self, z_variable: str = "Z") -> npt.NDArray:
|
234
236
|
"""Calculates temperature weighted z, i.e. ice effective radius [m]."""
|
235
237
|
if self.product not in ("iwc", "ier"):
|
236
238
|
msg = f"Invalid product: {self.product}"
|
@@ -264,7 +266,7 @@ class IceSource(DataSource):
|
|
264
266
|
return float(utils.lin2db(k2))
|
265
267
|
|
266
268
|
|
267
|
-
def get_is_rain(filename: str) ->
|
269
|
+
def get_is_rain(filename: str | PathLike) -> npt.NDArray:
|
268
270
|
# TODO: Check that this is correct
|
269
271
|
with netCDF4.Dataset(filename) as nc:
|
270
272
|
for name in ["rain_detected", "rainfall_rate", "rain_rate"]:
|
@@ -277,12 +279,14 @@ def get_is_rain(filename: str) -> np.ndarray:
|
|
277
279
|
raise ValueError(msg)
|
278
280
|
|
279
281
|
|
280
|
-
def read_nc_field(nc_file: str, name: str) -> ma.MaskedArray:
|
282
|
+
def read_nc_field(nc_file: str | PathLike, name: str) -> ma.MaskedArray:
|
281
283
|
with netCDF4.Dataset(nc_file) as nc:
|
282
284
|
return nc.variables[name][:]
|
283
285
|
|
284
286
|
|
285
|
-
def interpolate_model(
|
287
|
+
def interpolate_model(
|
288
|
+
cat_file: str | PathLike, names: str | list
|
289
|
+
) -> dict[str, npt.NDArray]:
|
286
290
|
"""Interpolates 2D model field into dense Cloudnet grid.
|
287
291
|
|
288
292
|
Args:
|
@@ -295,7 +299,7 @@ def interpolate_model(cat_file: str, names: str | list) -> dict[str, np.ndarray]
|
|
295
299
|
|
296
300
|
"""
|
297
301
|
|
298
|
-
def _interp_field(var_name: str) ->
|
302
|
+
def _interp_field(var_name: str) -> npt.NDArray:
|
299
303
|
values = _read_nc_fields(
|
300
304
|
cat_file,
|
301
305
|
["model_time", "model_height", var_name, "time", "height"],
|
@@ -306,12 +310,12 @@ def interpolate_model(cat_file: str, names: str | list) -> dict[str, np.ndarray]
|
|
306
310
|
return {name: _interp_field(name) for name in names}
|
307
311
|
|
308
312
|
|
309
|
-
def _read_nc_fields(nc_file: str, names: list[str]) -> list[ma.MaskedArray]:
|
313
|
+
def _read_nc_fields(nc_file: str | PathLike, names: list[str]) -> list[ma.MaskedArray]:
|
310
314
|
with netCDF4.Dataset(nc_file) as nc:
|
311
315
|
return [nc.variables[name][:] for name in names]
|
312
316
|
|
313
317
|
|
314
|
-
def _get_temperature(categorize_file: str) ->
|
318
|
+
def _get_temperature(categorize_file: str | PathLike) -> npt.NDArray:
|
315
319
|
"""Returns interpolated temperatures in Celsius."""
|
316
320
|
atmosphere = interpolate_model(categorize_file, "temperature")
|
317
321
|
return atmos_utils.k2c(atmosphere["temperature"])
|