cloudnetpy 1.65.8__py3-none-any.whl → 1.66.1__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.
Files changed (42) hide show
  1. cloudnetpy/categorize/__init__.py +0 -1
  2. cloudnetpy/categorize/atmos_utils.py +278 -59
  3. cloudnetpy/categorize/attenuation.py +31 -0
  4. cloudnetpy/categorize/attenuations/__init__.py +37 -0
  5. cloudnetpy/categorize/attenuations/gas_attenuation.py +30 -0
  6. cloudnetpy/categorize/attenuations/liquid_attenuation.py +80 -0
  7. cloudnetpy/categorize/attenuations/melting_attenuation.py +75 -0
  8. cloudnetpy/categorize/attenuations/rain_attenuation.py +84 -0
  9. cloudnetpy/categorize/categorize.py +140 -81
  10. cloudnetpy/categorize/classify.py +92 -128
  11. cloudnetpy/categorize/containers.py +45 -31
  12. cloudnetpy/categorize/droplet.py +2 -2
  13. cloudnetpy/categorize/falling.py +3 -3
  14. cloudnetpy/categorize/freezing.py +2 -2
  15. cloudnetpy/categorize/itu.py +243 -0
  16. cloudnetpy/categorize/melting.py +0 -3
  17. cloudnetpy/categorize/model.py +31 -14
  18. cloudnetpy/categorize/radar.py +28 -12
  19. cloudnetpy/constants.py +3 -6
  20. cloudnetpy/model_evaluation/file_handler.py +2 -2
  21. cloudnetpy/model_evaluation/products/observation_products.py +8 -8
  22. cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +5 -2
  23. cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +11 -11
  24. cloudnetpy/output.py +46 -26
  25. cloudnetpy/plotting/plot_meta.py +8 -2
  26. cloudnetpy/plotting/plotting.py +37 -7
  27. cloudnetpy/products/classification.py +39 -34
  28. cloudnetpy/products/der.py +15 -13
  29. cloudnetpy/products/drizzle_tools.py +22 -21
  30. cloudnetpy/products/ier.py +8 -45
  31. cloudnetpy/products/iwc.py +7 -22
  32. cloudnetpy/products/lwc.py +14 -15
  33. cloudnetpy/products/mwr_tools.py +15 -2
  34. cloudnetpy/products/product_tools.py +121 -119
  35. cloudnetpy/utils.py +4 -0
  36. cloudnetpy/version.py +2 -2
  37. {cloudnetpy-1.65.8.dist-info → cloudnetpy-1.66.1.dist-info}/METADATA +1 -1
  38. {cloudnetpy-1.65.8.dist-info → cloudnetpy-1.66.1.dist-info}/RECORD +41 -35
  39. {cloudnetpy-1.65.8.dist-info → cloudnetpy-1.66.1.dist-info}/WHEEL +1 -1
  40. cloudnetpy/categorize/atmos.py +0 -376
  41. {cloudnetpy-1.65.8.dist-info → cloudnetpy-1.66.1.dist-info}/LICENSE +0 -0
  42. {cloudnetpy-1.65.8.dist-info → cloudnetpy-1.66.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,84 @@
1
+ import numpy as np
2
+ from numpy import ma
3
+ from numpy.typing import NDArray
4
+
5
+ import cloudnetpy.constants as con
6
+ from cloudnetpy import utils
7
+ from cloudnetpy.categorize.attenuations import (
8
+ Attenuation,
9
+ calc_two_way_attenuation,
10
+ )
11
+ from cloudnetpy.categorize.containers import ClassificationResult, Observations
12
+
13
+
14
+ def calc_rain_attenuation(
15
+ data: Observations, classification: ClassificationResult
16
+ ) -> Attenuation:
17
+ affected_region, inducing_region = _find_regions(classification)
18
+ shape = affected_region.shape
19
+
20
+ if data.disdrometer is None:
21
+ return Attenuation(
22
+ amount=ma.masked_all(shape),
23
+ error=ma.masked_all(shape),
24
+ attenuated=affected_region,
25
+ uncorrected=affected_region,
26
+ )
27
+
28
+ rainfall_rate = data.disdrometer.data["rainfall_rate"][:].copy()
29
+ rainfall_rate[classification.is_rain == 0] = ma.masked
30
+ frequency = data.radar.radar_frequency
31
+
32
+ specific_attenuation_array = _calc_rain_specific_attenuation(
33
+ rainfall_rate, frequency
34
+ )
35
+
36
+ specific_attenuation = utils.transpose(specific_attenuation_array) * ma.ones(shape)
37
+
38
+ two_way_attenuation = calc_two_way_attenuation(
39
+ data.radar.height, specific_attenuation
40
+ )
41
+
42
+ two_way_attenuation[~inducing_region] = 0
43
+ two_way_attenuation = ma.array(utils.ffill(two_way_attenuation.data))
44
+ two_way_attenuation[two_way_attenuation == 0] = ma.masked
45
+
46
+ return Attenuation(
47
+ amount=two_way_attenuation,
48
+ error=two_way_attenuation * 0.2,
49
+ attenuated=affected_region,
50
+ uncorrected=np.zeros_like(affected_region, dtype=bool),
51
+ )
52
+
53
+
54
+ def _find_regions(
55
+ classification: ClassificationResult,
56
+ ) -> tuple[NDArray[np.bool_], NDArray[np.bool_]]:
57
+ """Finds regions where rain attenuation is present and can be corrected or not."""
58
+ warm_region = ~classification.category_bits.freezing
59
+ is_rain = utils.transpose(classification.is_rain).astype(bool)
60
+ affected_region = np.ones_like(warm_region, dtype=bool) * is_rain
61
+ inducing_region = warm_region * is_rain
62
+ return affected_region, inducing_region
63
+
64
+
65
+ def _calc_rain_specific_attenuation(
66
+ rainfall_rate: np.ndarray, frequency: float
67
+ ) -> np.ndarray:
68
+ """Calculates specific attenuation due to rain (dB km-1).
69
+
70
+ References:
71
+ Crane, R. (1980). Prediction of Attenuation by Rain.
72
+ IEEE Transactions on Communications, 28(9), 1717–1733.
73
+ doi:10.1109/tcom.1980.1094844
74
+ """
75
+ if frequency > 8 and frequency < 12:
76
+ alpha, beta = 0.0125, 1.18
77
+ if frequency > 34 and frequency < 37:
78
+ alpha, beta = 0.242, 1.04
79
+ elif frequency > 93 and frequency < 96:
80
+ alpha, beta = 0.95, 0.72
81
+ else:
82
+ msg = "Radar frequency not supported"
83
+ raise ValueError(msg)
84
+ return alpha * (rainfall_rate * con.M_S_TO_MM_H) ** beta
@@ -1,9 +1,15 @@
1
1
  """Module that generates Cloudnet categorize file."""
2
2
 
3
+ import dataclasses
3
4
  import logging
5
+ from dataclasses import fields
6
+
7
+ import numpy as np
8
+ from numpy.typing import NDArray
4
9
 
5
10
  from cloudnetpy import output, utils
6
- from cloudnetpy.categorize import atmos, classify
11
+ from cloudnetpy.categorize import attenuation, classify
12
+ from cloudnetpy.categorize.containers import Observations
7
13
  from cloudnetpy.categorize.disdrometer import Disdrometer
8
14
  from cloudnetpy.categorize.lidar import Lidar
9
15
  from cloudnetpy.categorize.model import Model
@@ -12,6 +18,7 @@ from cloudnetpy.categorize.radar import Radar
12
18
  from cloudnetpy.datasource import DataSource
13
19
  from cloudnetpy.exceptions import DisdrometerDataError, ValidTimeStampError
14
20
  from cloudnetpy.metadata import MetaData
21
+ from cloudnetpy.products.product_tools import CategoryBits, QualityBits
15
22
 
16
23
 
17
24
  def generate_categorize(
@@ -66,24 +73,25 @@ def generate_categorize(
66
73
  >>> generate_categorize(input_files, 'output.nc') # Use the Voodoo method
67
74
  """
68
75
 
69
- def _interpolate_to_cloudnet_grid() -> list:
70
- wl_band = utils.get_wl_band(data["radar"].radar_frequency)
71
- if data["disdrometer"] is not None:
72
- data["disdrometer"].interpolate_to_grid(time)
73
- if data["mwr"] is not None:
74
- data["mwr"].rebin_to_grid(time)
75
- data["model"].interpolate_to_common_height(wl_band)
76
- model_gap_ind = data["model"].interpolate_to_grid(time, height)
77
- radar_gap_ind = data["radar"].rebin_to_grid(time)
78
- lidar_gap_ind = data["lidar"].interpolate_to_grid(time, height)
76
+ def _interpolate_to_cloudnet_grid() -> list[int]:
77
+ if data.disdrometer is not None:
78
+ data.disdrometer.interpolate_to_grid(time)
79
+ if data.mwr is not None:
80
+ data.mwr.rebin_to_grid(time)
81
+ data.model.calc_attenuations(data.radar.radar_frequency)
82
+ data.model.interpolate_to_common_height()
83
+ model_gap_ind = data.model.interpolate_to_grid(time, height)
84
+ radar_gap_ind = data.radar.rebin_to_grid(time)
85
+ lidar_gap_ind = data.lidar.interpolate_to_grid(time, height)
79
86
  gap_indices = set(radar_gap_ind + lidar_gap_ind + model_gap_ind)
80
87
  return [ind for ind in range(len(time)) if ind not in gap_indices]
81
88
 
82
89
  def _screen_bad_time_indices(valid_indices: list) -> None:
83
90
  n_time_full = len(time)
84
- data["radar"].time = time[valid_indices]
85
- for data_key, obj in data.items():
86
- if obj is None or data_key == "lv0_files":
91
+ data.radar.time = time[valid_indices]
92
+ for field in fields(data):
93
+ obj = getattr(data, field.name)
94
+ if not hasattr(obj, "data"):
87
95
  continue
88
96
  for key, item in obj.data.items():
89
97
  if utils.isscalar(item.data):
@@ -97,79 +105,92 @@ def generate_categorize(
97
105
  else:
98
106
  continue
99
107
  obj.data[key].data = array
100
- for key, item in data["model"].data_dense.items():
101
- data["model"].data_dense[key] = item[valid_indices, :]
108
+ for key, item in data.model.data_dense.items():
109
+ data.model.data_dense[key] = item[valid_indices, :]
102
110
 
103
111
  def _prepare_output() -> dict:
104
- data["radar"].add_meta()
105
- data["model"].screen_sparse_fields()
106
- if data["disdrometer"] is not None:
107
- data["radar"].data.pop("rainfall_rate", None)
108
- data["disdrometer"].data.pop("n_particles", None)
109
- for key in ("category_bits", "insect_prob"):
110
- data["radar"].append_data(getattr(classification, key), key)
112
+ data.radar.add_meta()
113
+ data.model.screen_sparse_fields()
114
+
115
+ if data.disdrometer is not None:
116
+ data.radar.data.pop("rainfall_rate", None)
117
+ data.disdrometer.data.pop("n_particles", None)
118
+
119
+ data.radar.append_data(attenuations.gas.amount, "radar_gas_atten")
120
+ data.radar.append_data(attenuations.liquid.amount, "radar_liquid_atten")
121
+ data.radar.append_data(attenuations.rain.amount, "radar_rain_atten")
122
+ data.radar.append_data(attenuations.melting.amount, "radar_melting_atten")
123
+
124
+ data.radar.append_data(_classes_to_bits(quality), "quality_bits")
125
+
126
+ data.radar.append_data(classification.insect_prob, "insect_prob")
127
+ data.radar.append_data(classification.is_rain, "rain_detected")
128
+ data.radar.append_data(
129
+ _classes_to_bits(classification.category_bits), "category_bits"
130
+ )
131
+
111
132
  if classification.liquid_prob is not None:
112
- data["radar"].append_data(classification.liquid_prob, "liquid_prob")
113
- for key in ("radar_liquid_atten", "radar_gas_atten"):
114
- data["radar"].append_data(attenuations[key], key)
115
- data["radar"].append_data(quality["quality_bits"], "quality_bits")
116
- data["radar"].append_data(classification.is_rain, "rain_detected")
133
+ data.radar.append_data(classification.liquid_prob, "liquid_prob")
134
+
117
135
  return {
118
- **data["radar"].data,
119
- **data["lidar"].data,
120
- **data["model"].data,
121
- **data["model"].data_sparse,
122
- **(data["mwr"].data if data["mwr"] is not None else {}),
123
- **(data["disdrometer"].data if data["disdrometer"] is not None else {}),
136
+ **data.radar.data,
137
+ **data.lidar.data,
138
+ **data.model.data,
139
+ **data.model.data_sparse,
140
+ **(data.mwr.data if data.mwr is not None else {}),
141
+ **(data.disdrometer.data if data.disdrometer is not None else {}),
124
142
  }
125
143
 
126
144
  def _define_dense_grid() -> tuple:
127
- return utils.time_grid(), data["radar"].height
145
+ return utils.time_grid(), data.radar.height
128
146
 
129
147
  def _close_all() -> None:
130
- for obj in data.values():
148
+ for field in fields(data):
149
+ obj = getattr(data, field.name)
131
150
  if isinstance(obj, DataSource):
132
151
  obj.close()
133
152
 
134
153
  try:
135
- data: dict = {
136
- "radar": Radar(input_files["radar"]),
137
- "lidar": Lidar(input_files["lidar"]),
138
- "lv0_files": input_files.get("lv0_files"),
139
- "mwr": None,
140
- "disdrometer": None,
141
- }
154
+ radar = Radar(input_files["radar"])
155
+ data = Observations(
156
+ radar=radar,
157
+ lidar=Lidar(input_files["lidar"]),
158
+ model=Model(input_files["model"], radar.altitude, options),
159
+ lv0_files=input_files.get("lv0_files"),
160
+ )
142
161
  if "mwr" in input_files:
143
- data["mwr"] = Mwr(input_files["mwr"])
162
+ data.mwr = Mwr(input_files["mwr"])
144
163
  if "disdrometer" in input_files:
145
164
  try:
146
- data["disdrometer"] = Disdrometer(input_files["disdrometer"])
165
+ data.disdrometer = Disdrometer(input_files["disdrometer"])
147
166
  except DisdrometerDataError as err:
148
167
  logging.warning("Unable to use disdrometer: %s", err)
149
- data["model"] = Model(input_files["model"], data["radar"].altitude, options)
150
168
  time, height = _define_dense_grid()
151
169
  valid_ind = _interpolate_to_cloudnet_grid()
152
170
  if not valid_ind:
153
171
  msg = "No overlapping radar and lidar timestamps found"
154
172
  raise ValidTimeStampError(msg)
155
173
  _screen_bad_time_indices(valid_ind)
156
- if (
157
- "rpg" in data["radar"].source_type.lower()
158
- or "basta" in data["radar"].source_type.lower()
159
- ):
160
- data["radar"].filter_speckle_noise()
161
- data["radar"].filter_1st_gate_artifact()
174
+ if any(source in data.radar.source_type.lower() for source in ("rpg", "basta")):
175
+ data.radar.filter_speckle_noise()
176
+ data.radar.filter_1st_gate_artifact()
162
177
  for variable in ("v", "v_sigma", "ldr"):
163
- data["radar"].filter_stripes(variable)
164
- data["radar"].remove_incomplete_pixels()
165
- data["model"].calc_wet_bulb()
178
+ data.radar.filter_stripes(variable)
179
+ data.radar.remove_incomplete_pixels()
180
+ data.model.calc_wet_bulb()
181
+
166
182
  classification = classify.classify_measurements(data)
167
- attenuations = atmos.get_attenuations(data, classification)
168
- data["radar"].correct_atten(attenuations)
169
- data["radar"].calc_errors(attenuations, classification.is_clutter)
183
+
184
+ attenuations = attenuation.get_attenuations(data, classification)
185
+
186
+ data.radar.correct_atten(attenuations)
187
+ data.radar.calc_errors(attenuations, classification.is_clutter)
188
+
170
189
  quality = classify.fetch_quality(data, classification, attenuations)
190
+
171
191
  cloudnet_arrays = _prepare_output()
172
- date = data["radar"].get_date()
192
+
193
+ date = data.radar.get_date()
173
194
  attributes = output.add_time_attribute(CATEGORIZE_ATTRIBUTES, date)
174
195
  attributes = output.add_time_attribute(attributes, date, "model_time")
175
196
  attributes = output.add_source_attribute(attributes, data)
@@ -181,16 +202,16 @@ def generate_categorize(
181
202
 
182
203
  def _save_cat(
183
204
  full_path: str,
184
- data_obs: dict,
205
+ data_obs: Observations,
185
206
  cloudnet_arrays: dict,
186
207
  uuid: str | None,
187
208
  ) -> str:
188
209
  """Creates a categorize netCDF4 file and saves all data into it."""
189
210
  dims = {
190
- "time": len(data_obs["radar"].time),
191
- "height": len(data_obs["radar"].height),
192
- "model_time": len(data_obs["model"].time),
193
- "model_height": len(data_obs["model"].mean_height),
211
+ "time": len(data_obs.radar.time),
212
+ "height": len(data_obs.radar.height),
213
+ "model_time": len(data_obs.model.time),
214
+ "model_height": len(data_obs.model.mean_height),
194
215
  }
195
216
 
196
217
  file_type = "categorize"
@@ -198,12 +219,12 @@ def _save_cat(
198
219
  uuid_out = nc.file_uuid
199
220
  nc.cloudnet_file_type = file_type
200
221
  output.copy_global(
201
- data_obs["radar"].dataset,
222
+ data_obs.radar.dataset,
202
223
  nc,
203
224
  ("year", "month", "day", "location"),
204
225
  )
205
- nc.title = f"Cloud categorization products from {data_obs['radar'].location}"
206
- nc.source_file_uuids = output.get_source_uuids(*data_obs.values())
226
+ nc.title = f"Cloud categorization products from {data_obs.radar.location}"
227
+ nc.source_file_uuids = output.get_source_uuids(data_obs)
207
228
  is_voodoo = "liquid_prob" in cloudnet_arrays
208
229
  extra_references = (
209
230
  ["https://doi.org/10.5194/amt-15-5343-2022"] if is_voodoo else None
@@ -221,6 +242,14 @@ def _save_cat(
221
242
  return uuid_out
222
243
 
223
244
 
245
+ def _classes_to_bits(data: QualityBits | CategoryBits) -> NDArray[np.int_]:
246
+ shape = data.radar.shape if hasattr(data, "radar") else data.droplet.shape
247
+ quality = np.zeros(shape, dtype=np.int64)
248
+ for i, field in enumerate(dataclasses.fields(data)):
249
+ quality |= (1 << i) * getattr(data, field.name)
250
+ return quality
251
+
252
+
224
253
  COMMENTS = {
225
254
  "category_bits": (
226
255
  "This variable contains information on the nature of the targets\n"
@@ -255,14 +284,17 @@ COMMENTS = {
255
284
  "humidity, but forcing pixels containing liquid cloud to saturation with\n"
256
285
  "respect to liquid water. It has been used to correct Z."
257
286
  ),
258
- "Tw": (
259
- "This variable was derived from model temperature, pressure and relative\n"
260
- "humidity."
287
+ "radar_rain_atten": (
288
+ "This variable was calculated from the disdrometer rainfall rate."
261
289
  ),
290
+ "radar_melting_atten": (
291
+ "This variable was calculated from the disdrometer rainfall rate."
292
+ ),
293
+ "Tw": "This variable was derived from model temperature, pressure and humidity.",
262
294
  "Z_sensitivity": (
263
295
  "This variable is an estimate of the radar sensitivity, i.e. the minimum\n"
264
296
  "detectable radar reflectivity, as a function of height. It includes the\n"
265
- "effect of ground clutter and gas attenuation but not liquid attenuation."
297
+ "effect of ground clutter and gas attenuation but not other attenuations."
266
298
  ),
267
299
  "Z_error": (
268
300
  "This variable is an estimate of the one-standard-deviation random error\n"
@@ -272,18 +304,19 @@ COMMENTS = {
272
304
  " finite number of pulses\n"
273
305
  "2) 10% uncertainty in gaseous attenuation correction (mainly due to error\n"
274
306
  " in model humidity field)\n"
275
- "3) Error in liquid water path (given by the variable lwp_error) and its\n"
307
+ "3) 20% uncertainty in rain attenuation correction (mainly due to error\n"
308
+ " in disdrometer rainfall rate)\n"
309
+ "4) 10%-20% uncertainty in melting layer attenuation correction (mainly due\n"
310
+ " to error in disdrometer rainfall rate)\n"
311
+ "5) Error in liquid water path (given by the variable lwp_error) and its\n"
276
312
  " partitioning with height)."
277
313
  ),
278
314
  "Z": (
279
- "This variable has been corrected for attenuation by gaseous attenuation\n"
280
- "(using the thermodynamic variables from a forecast model; see the\n"
281
- "radar_gas_atten variable) and liquid attenuation (using liquid water path\n"
282
- "from a microwave radiometer; see the radar_liquid_atten variable) but rain\n"
283
- "and melting-layer attenuation has not been corrected.\n"
284
- "Calibration convention: in the absence of attenuation, a cloud at 273 K\n"
285
- "containing one million 100-micron droplets per cubic metre will have\n"
286
- "a reflectivity of 0 dBZ at all frequencies."
315
+ "This variable has been corrected for attenuation by gaseous attenuation,\n"
316
+ "and possibly liquid water, rain and melting layer (see quality_bits\n"
317
+ "variable). Calibration convention: in the absence of attenuation, a cloud\n"
318
+ "at 273 K containing one million 100-micron droplets per cubic metre will\n"
319
+ "have\n a reflectivity of 0 dBZ at all frequencies."
287
320
  ),
288
321
  "bias": (
289
322
  "This variable is an estimate of the one-standard-deviation\n"
@@ -329,6 +362,19 @@ DEFINITIONS = {
329
362
  liquid water path and the lidar estimation of the location of
330
363
  liquid water cloud; be aware that errors in reflectivity may
331
364
  result.""",
365
+ 6: """Rain has caused radar attenuation; if bit 7 is set then a
366
+ correction for the radar attenuation has been performed;
367
+ otherwise do not trust the absolute values of reflectivity
368
+ factor. No correction is performed for lidar attenuation.""",
369
+ 7: """Radar reflectivity has been corrected for rain attenuation
370
+ using rainfall rate from a disdrometer; be aware that errors
371
+ in reflectivity may result.""",
372
+ 8: """Melting layer has caused radar attenuation; if bit 9 is set then a
373
+ correction for the radar attenuation has been performed;
374
+ otherwise do not trust the absolute values of reflectivity
375
+ factor. No correction is performed for lidar attenuation.""",
376
+ 9: """Radar reflectivity has been corrected for melting layer
377
+ attenuation; be aware that errors in reflectivity may result.""",
332
378
  }
333
379
  ),
334
380
  }
@@ -416,12 +462,25 @@ CATEGORIZE_ATTRIBUTES = {
416
462
  long_name="Two-way radar attenuation due to liquid water",
417
463
  units="dB",
418
464
  comment=COMMENTS["radar_liquid_atten"],
465
+ references="ITU-R P.840-9",
466
+ ),
467
+ "radar_rain_atten": MetaData(
468
+ long_name="Two-way radar attenuation due to rain",
469
+ units="dB",
470
+ references="Crane, R. (1980)",
471
+ comment=COMMENTS["radar_rain_atten"],
472
+ ),
473
+ "radar_melting_atten": MetaData(
474
+ long_name="Two-way radar attenuation due to melting ice",
475
+ units="dB",
476
+ references="Li, H., & Moisseev, D. (2019)",
477
+ comment=COMMENTS["radar_melting_atten"],
419
478
  ),
420
479
  "radar_gas_atten": MetaData(
421
480
  long_name="Two-way radar attenuation due to atmospheric gases",
422
481
  units="dB",
423
482
  comment=COMMENTS["radar_gas_atten"],
424
- references="Liebe (1985, Radio Sci. 20(5), 1069-1089)",
483
+ references="ITU-R P.676-13",
425
484
  ),
426
485
  "insect_prob": MetaData(
427
486
  long_name="Insect probability",