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.
Files changed (83) hide show
  1. cloudnetpy/categorize/__init__.py +1 -1
  2. cloudnetpy/categorize/atmos_utils.py +31 -27
  3. cloudnetpy/categorize/attenuations/__init__.py +4 -4
  4. cloudnetpy/categorize/attenuations/liquid_attenuation.py +7 -5
  5. cloudnetpy/categorize/attenuations/melting_attenuation.py +3 -3
  6. cloudnetpy/categorize/attenuations/rain_attenuation.py +4 -4
  7. cloudnetpy/categorize/categorize.py +25 -11
  8. cloudnetpy/categorize/classify.py +9 -8
  9. cloudnetpy/categorize/containers.py +13 -10
  10. cloudnetpy/categorize/disdrometer.py +5 -3
  11. cloudnetpy/categorize/droplet.py +12 -9
  12. cloudnetpy/categorize/falling.py +9 -8
  13. cloudnetpy/categorize/freezing.py +10 -7
  14. cloudnetpy/categorize/insects.py +18 -17
  15. cloudnetpy/categorize/lidar.py +7 -3
  16. cloudnetpy/categorize/melting.py +16 -15
  17. cloudnetpy/categorize/model.py +17 -10
  18. cloudnetpy/categorize/mwr.py +5 -3
  19. cloudnetpy/categorize/radar.py +15 -13
  20. cloudnetpy/cli.py +10 -8
  21. cloudnetpy/cloudnetarray.py +8 -7
  22. cloudnetpy/concat_lib.py +29 -20
  23. cloudnetpy/datasource.py +26 -21
  24. cloudnetpy/exceptions.py +12 -10
  25. cloudnetpy/instruments/basta.py +19 -9
  26. cloudnetpy/instruments/bowtie.py +18 -11
  27. cloudnetpy/instruments/ceilo.py +22 -10
  28. cloudnetpy/instruments/ceilometer.py +33 -34
  29. cloudnetpy/instruments/cl61d.py +5 -3
  30. cloudnetpy/instruments/cloudnet_instrument.py +7 -7
  31. cloudnetpy/instruments/copernicus.py +16 -7
  32. cloudnetpy/instruments/disdrometer/common.py +5 -4
  33. cloudnetpy/instruments/disdrometer/parsivel.py +14 -9
  34. cloudnetpy/instruments/disdrometer/thies.py +11 -7
  35. cloudnetpy/instruments/fd12p.py +7 -6
  36. cloudnetpy/instruments/galileo.py +16 -7
  37. cloudnetpy/instruments/hatpro.py +33 -24
  38. cloudnetpy/instruments/lufft.py +6 -4
  39. cloudnetpy/instruments/mira.py +33 -19
  40. cloudnetpy/instruments/mrr.py +12 -12
  41. cloudnetpy/instruments/nc_lidar.py +1 -1
  42. cloudnetpy/instruments/nc_radar.py +8 -8
  43. cloudnetpy/instruments/pollyxt.py +19 -12
  44. cloudnetpy/instruments/radiometrics.py +17 -10
  45. cloudnetpy/instruments/rain_e_h3.py +9 -5
  46. cloudnetpy/instruments/rpg.py +32 -21
  47. cloudnetpy/instruments/rpg_reader.py +15 -12
  48. cloudnetpy/instruments/vaisala.py +32 -24
  49. cloudnetpy/instruments/weather_station.py +22 -19
  50. cloudnetpy/model_evaluation/file_handler.py +27 -29
  51. cloudnetpy/model_evaluation/plotting/plot_tools.py +7 -5
  52. cloudnetpy/model_evaluation/plotting/plotting.py +41 -32
  53. cloudnetpy/model_evaluation/products/advance_methods.py +38 -34
  54. cloudnetpy/model_evaluation/products/grid_methods.py +10 -9
  55. cloudnetpy/model_evaluation/products/model_products.py +15 -9
  56. cloudnetpy/model_evaluation/products/observation_products.py +12 -10
  57. cloudnetpy/model_evaluation/products/product_resampling.py +11 -7
  58. cloudnetpy/model_evaluation/products/tools.py +18 -14
  59. cloudnetpy/model_evaluation/statistics/statistical_methods.py +6 -5
  60. cloudnetpy/model_evaluation/tests/unit/test_plotting.py +18 -25
  61. cloudnetpy/model_evaluation/utils.py +3 -3
  62. cloudnetpy/output.py +15 -32
  63. cloudnetpy/plotting/plotting.py +22 -12
  64. cloudnetpy/products/classification.py +15 -9
  65. cloudnetpy/products/der.py +24 -19
  66. cloudnetpy/products/drizzle.py +21 -13
  67. cloudnetpy/products/drizzle_error.py +8 -7
  68. cloudnetpy/products/drizzle_tools.py +27 -23
  69. cloudnetpy/products/epsilon.py +6 -5
  70. cloudnetpy/products/ier.py +11 -5
  71. cloudnetpy/products/iwc.py +18 -9
  72. cloudnetpy/products/lwc.py +41 -31
  73. cloudnetpy/products/mwr_tools.py +30 -19
  74. cloudnetpy/products/product_tools.py +23 -19
  75. cloudnetpy/utils.py +84 -98
  76. cloudnetpy/version.py +2 -2
  77. {cloudnetpy-1.80.8.dist-info → cloudnetpy-1.81.0.dist-info}/METADATA +2 -1
  78. cloudnetpy-1.81.0.dist-info/RECORD +126 -0
  79. cloudnetpy-1.80.8.dist-info/RECORD +0 -126
  80. {cloudnetpy-1.80.8.dist-info → cloudnetpy-1.81.0.dist-info}/WHEEL +0 -0
  81. {cloudnetpy-1.80.8.dist-info → cloudnetpy-1.81.0.dist-info}/entry_points.txt +0 -0
  82. {cloudnetpy-1.80.8.dist-info → cloudnetpy-1.81.0.dist-info}/licenses/LICENSE +0 -0
  83. {cloudnetpy-1.80.8.dist-info → cloudnetpy-1.81.0.dist-info}/top_level.txt +0 -0
@@ -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
- ) -> str:
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
- return output.save_product_file(
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: np.ndarray,
106
- status: np.ndarray,
107
- error: np.ndarray,
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(categorize_file: str) -> tuple[np.ndarray, np.ndarray]:
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) -> np.ndarray:
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) -> np.ndarray:
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) -> np.ndarray:
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: np.ndarray) -> None:
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: np.ndarray) -> None:
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) -> np.ndarray:
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) -> np.ndarray:
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) -> np.ndarray:
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: np.ndarray) -> np.ndarray:
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: np.ndarray) -> np.ndarray:
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) -> np.ndarray:
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) -> np.ndarray:
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) -> np.ndarray:
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) -> np.ndarray:
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) -> np.ndarray:
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: np.ndarray, max_value: float) -> np.ndarray:
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(error_2d: np.ndarray, error_1d: np.ndarray) -> np.ndarray:
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: np.ndarray) -> ma.MaskedArray:
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]
@@ -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, output_file: str, uuid: str | None = None
23
- ) -> str:
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, output_file: str, uuid: str | None = None
43
- ) -> str:
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, output_file: str, uuid: str | None = None
63
- ) -> str:
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
- ) -> str:
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: str | None
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 if uuid is not None else utils.get_uuid()
131
+ self.uuid = uuid
123
132
 
124
- def harmonize(self, product: Literal["multi", "single"]) -> str:
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(self, product: Literal["multi", "single"]):
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) -> np.ndarray:
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) -> np.ndarray:
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) -> np.ndarray:
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) -> np.ndarray:
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) -> np.ndarray:
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) -> np.ndarray:
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) -> np.ndarray:
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) -> np.ndarray:
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") -> np.ndarray:
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) -> np.ndarray:
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(cat_file: str, names: str | list) -> dict[str, np.ndarray]:
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) -> np.ndarray:
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) -> np.ndarray:
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"])