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.
Files changed (95) hide show
  1. cloudnetpy/categorize/atmos.py +46 -14
  2. cloudnetpy/categorize/atmos_utils.py +11 -1
  3. cloudnetpy/categorize/categorize.py +38 -21
  4. cloudnetpy/categorize/classify.py +31 -9
  5. cloudnetpy/categorize/containers.py +19 -7
  6. cloudnetpy/categorize/droplet.py +24 -8
  7. cloudnetpy/categorize/falling.py +17 -7
  8. cloudnetpy/categorize/freezing.py +19 -5
  9. cloudnetpy/categorize/insects.py +27 -14
  10. cloudnetpy/categorize/lidar.py +38 -36
  11. cloudnetpy/categorize/melting.py +19 -9
  12. cloudnetpy/categorize/model.py +28 -9
  13. cloudnetpy/categorize/mwr.py +4 -2
  14. cloudnetpy/categorize/radar.py +58 -22
  15. cloudnetpy/cloudnetarray.py +15 -6
  16. cloudnetpy/concat_lib.py +39 -16
  17. cloudnetpy/constants.py +7 -0
  18. cloudnetpy/datasource.py +39 -19
  19. cloudnetpy/instruments/basta.py +6 -2
  20. cloudnetpy/instruments/campbell_scientific.py +33 -16
  21. cloudnetpy/instruments/ceilo.py +30 -13
  22. cloudnetpy/instruments/ceilometer.py +76 -37
  23. cloudnetpy/instruments/cl61d.py +8 -3
  24. cloudnetpy/instruments/cloudnet_instrument.py +2 -1
  25. cloudnetpy/instruments/copernicus.py +27 -14
  26. cloudnetpy/instruments/disdrometer/common.py +51 -32
  27. cloudnetpy/instruments/disdrometer/parsivel.py +79 -48
  28. cloudnetpy/instruments/disdrometer/thies.py +10 -6
  29. cloudnetpy/instruments/galileo.py +23 -12
  30. cloudnetpy/instruments/hatpro.py +27 -11
  31. cloudnetpy/instruments/instruments.py +4 -1
  32. cloudnetpy/instruments/lufft.py +20 -11
  33. cloudnetpy/instruments/mira.py +60 -49
  34. cloudnetpy/instruments/mrr.py +31 -20
  35. cloudnetpy/instruments/nc_lidar.py +15 -6
  36. cloudnetpy/instruments/nc_radar.py +31 -22
  37. cloudnetpy/instruments/pollyxt.py +36 -21
  38. cloudnetpy/instruments/radiometrics.py +32 -18
  39. cloudnetpy/instruments/rpg.py +48 -22
  40. cloudnetpy/instruments/rpg_reader.py +39 -30
  41. cloudnetpy/instruments/vaisala.py +39 -27
  42. cloudnetpy/instruments/weather_station.py +15 -11
  43. cloudnetpy/metadata.py +3 -1
  44. cloudnetpy/model_evaluation/file_handler.py +31 -21
  45. cloudnetpy/model_evaluation/metadata.py +3 -1
  46. cloudnetpy/model_evaluation/model_metadata.py +1 -1
  47. cloudnetpy/model_evaluation/plotting/plot_tools.py +20 -15
  48. cloudnetpy/model_evaluation/plotting/plotting.py +114 -64
  49. cloudnetpy/model_evaluation/products/advance_methods.py +48 -28
  50. cloudnetpy/model_evaluation/products/grid_methods.py +44 -19
  51. cloudnetpy/model_evaluation/products/model_products.py +22 -18
  52. cloudnetpy/model_evaluation/products/observation_products.py +15 -9
  53. cloudnetpy/model_evaluation/products/product_resampling.py +14 -4
  54. cloudnetpy/model_evaluation/products/tools.py +16 -7
  55. cloudnetpy/model_evaluation/statistics/statistical_methods.py +28 -15
  56. cloudnetpy/model_evaluation/tests/e2e/conftest.py +3 -3
  57. cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +9 -5
  58. cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +14 -13
  59. cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +9 -5
  60. cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +14 -13
  61. cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +9 -5
  62. cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +14 -13
  63. cloudnetpy/model_evaluation/tests/unit/conftest.py +11 -11
  64. cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +33 -27
  65. cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +83 -83
  66. cloudnetpy/model_evaluation/tests/unit/test_model_products.py +23 -21
  67. cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +24 -25
  68. cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +40 -39
  69. cloudnetpy/model_evaluation/tests/unit/test_plotting.py +12 -11
  70. cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +30 -30
  71. cloudnetpy/model_evaluation/tests/unit/test_tools.py +18 -17
  72. cloudnetpy/model_evaluation/utils.py +3 -2
  73. cloudnetpy/output.py +45 -19
  74. cloudnetpy/plotting/plot_meta.py +35 -11
  75. cloudnetpy/plotting/plotting.py +172 -104
  76. cloudnetpy/products/classification.py +20 -8
  77. cloudnetpy/products/der.py +25 -10
  78. cloudnetpy/products/drizzle.py +41 -26
  79. cloudnetpy/products/drizzle_error.py +10 -5
  80. cloudnetpy/products/drizzle_tools.py +43 -24
  81. cloudnetpy/products/ier.py +10 -5
  82. cloudnetpy/products/iwc.py +16 -9
  83. cloudnetpy/products/lwc.py +34 -12
  84. cloudnetpy/products/mwr_multi.py +4 -1
  85. cloudnetpy/products/mwr_single.py +4 -1
  86. cloudnetpy/products/product_tools.py +33 -10
  87. cloudnetpy/utils.py +175 -74
  88. cloudnetpy/version.py +1 -1
  89. {cloudnetpy-1.55.20.dist-info → cloudnetpy-1.55.22.dist-info}/METADATA +11 -10
  90. cloudnetpy-1.55.22.dist-info/RECORD +114 -0
  91. docs/source/conf.py +2 -2
  92. cloudnetpy-1.55.20.dist-info/RECORD +0 -114
  93. {cloudnetpy-1.55.20.dist-info → cloudnetpy-1.55.22.dist-info}/LICENSE +0 -0
  94. {cloudnetpy-1.55.20.dist-info → cloudnetpy-1.55.22.dist-info}/WHEEL +0 -0
  95. {cloudnetpy-1.55.20.dist-info → cloudnetpy-1.55.22.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- """ This module contains functions to calculate
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
- dqs_dz = dqs_dp * air_density**2 * -scipy.constants.g
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, temperature: np.ndarray, svp_mixing_ratio: 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
- gas_atten = self._specific_to_gas_atten(specific_atten_corrected)
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, self._mwr["lwp_error"][:]
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
- assert isinstance(proper_values, ma.MaskedArray)
229
- return proper_values.filled(False) & ~self.uncorrected
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, is_liquid: np.ndarray
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], atmosphere[1][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, height: 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, height: 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 / 3600_000
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, output_file: str, uuid: str | None = None
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
- radar_data_gap_indices = data["radar"].rebin_to_grid(time)
63
- lidar_data_gap_indices = data["lidar"].interpolate_to_grid(time, height)
64
- bad_time_indices = list(set(radar_data_gap_indices + lidar_data_gap_indices))
65
- valid_ind = [ind for ind in range(len(time)) if ind not in bad_time_indices]
66
- return valid_ind
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
- assert data["radar"].altitude is not None
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
- raise ValidTimeStampError("No overlapping radar and lidar timestamps found")
133
+ msg = "No overlapping radar and lidar timestamps found"
134
+ raise ValidTimeStampError(msg)
126
135
  _screen_bad_time_indices(valid_ind)
127
- if "rpg" in data["radar"].type.lower() or "basta" in data["radar"].type.lower():
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
- uuid = _save_cat(output_file, data, cloudnet_arrays, uuid)
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, data_obs: dict, cloudnet_arrays: dict, uuid: str | None
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, nc, ("year", "month", "day", "location")
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, extra=extra_references
192
+ identifier=file_type,
193
+ extra=extra_references,
178
194
  )
179
195
  if is_voodoo:
180
- import voodoonet.version # pylint: disable=import-outside-toplevel,import-error
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", units="m s-1"
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 # pylint: disable=import-outside-toplevel,import-error
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, liquid_from_lidar
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, liquid_from_lidar: 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, classification: ClassificationResult, attenuations: 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, is_falling: np.ndarray, is_liquid: np.ndarray
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, is_falling: np.ndarray, is_freezing: 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, 10, connectivity=1
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)