cloudnetpy 1.57.0__py3-none-any.whl → 1.58.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.
@@ -219,7 +219,7 @@ class LiquidAttenuation(Attenuation):
219
219
  def _find_pixels_hard_to_correct(self) -> np.ndarray:
220
220
  melting_layer = utils.isbit(self.classification.category_bits, 3)
221
221
  hard_to_correct = np.cumsum(melting_layer, axis=1) >= 1
222
- hard_to_correct[self.classification.is_rain, :] = True
222
+ hard_to_correct[self.classification.is_rain == 1, :] = True
223
223
  attenuated = self._find_attenuated_part_of_atmosphere()
224
224
  hard_to_correct[attenuated & self.atten.mask] = True
225
225
  return hard_to_correct
@@ -1,10 +1,12 @@
1
1
  """Module that generates Cloudnet categorize file."""
2
2
  from cloudnetpy import output, utils
3
3
  from cloudnetpy.categorize import atmos, classify
4
+ from cloudnetpy.categorize.disdrometer import Disdrometer
4
5
  from cloudnetpy.categorize.lidar import Lidar
5
6
  from cloudnetpy.categorize.model import Model
6
7
  from cloudnetpy.categorize.mwr import Mwr
7
8
  from cloudnetpy.categorize.radar import Radar
9
+ from cloudnetpy.datasource import DataSource
8
10
  from cloudnetpy.exceptions import ValidTimeStampError
9
11
  from cloudnetpy.metadata import MetaData
10
12
 
@@ -59,6 +61,8 @@ def generate_categorize(
59
61
  def _interpolate_to_cloudnet_grid() -> list:
60
62
  wl_band = utils.get_wl_band(data["radar"].radar_frequency)
61
63
  data["mwr"].rebin_to_grid(time)
64
+ if is_disdrometer:
65
+ data["disdrometer"].interpolate_to_grid(time)
62
66
  data["model"].interpolate_to_common_height(wl_band)
63
67
  model_gap_ind = data["model"].interpolate_to_grid(time, height)
64
68
  radar_gap_ind = data["radar"].rebin_to_grid(time)
@@ -69,8 +73,10 @@ def generate_categorize(
69
73
  def _screen_bad_time_indices(valid_indices: list) -> None:
70
74
  n_time_full = len(time)
71
75
  data["radar"].time = time[valid_indices]
72
- for var in ("radar", "lidar", "mwr", "model"):
73
- for key, item in data[var].data.items():
76
+ for data_key, obj in data.items():
77
+ if obj is None or data_key == "lv0_files":
78
+ continue
79
+ for key, item in obj.data.items():
74
80
  if utils.isscalar(item.data):
75
81
  continue
76
82
  array = item[:]
@@ -81,26 +87,31 @@ def generate_categorize(
81
87
  array = array[valid_indices, :]
82
88
  else:
83
89
  continue
84
- data[var].data[key].data = array
90
+ obj.data[key].data = array
85
91
  for key, item in data["model"].data_dense.items():
86
92
  data["model"].data_dense[key] = item[valid_indices, :]
87
93
 
88
94
  def _prepare_output() -> dict:
89
95
  data["radar"].add_meta()
90
96
  data["model"].screen_sparse_fields()
91
- for key in ("category_bits", "rainfall_rate", "insect_prob"):
97
+ if is_disdrometer:
98
+ data["radar"].data.pop("rainfall_rate", None)
99
+ data["disdrometer"].data.pop("n_particles", None)
100
+ for key in ("category_bits", "insect_prob"):
92
101
  data["radar"].append_data(getattr(classification, key), key)
93
102
  if classification.liquid_prob is not None:
94
103
  data["radar"].append_data(classification.liquid_prob, "liquid_prob")
95
104
  for key in ("radar_liquid_atten", "radar_gas_atten"):
96
105
  data["radar"].append_data(attenuations[key], key)
97
106
  data["radar"].append_data(quality["quality_bits"], "quality_bits")
107
+ data["radar"].append_data(classification.is_rain, "rain_detected")
98
108
  return {
99
109
  **data["radar"].data,
100
110
  **data["lidar"].data,
101
111
  **data["model"].data,
102
112
  **data["model"].data_sparse,
103
113
  **data["mwr"].data,
114
+ **(data["disdrometer"].data if is_disdrometer else {}),
104
115
  }
105
116
 
106
117
  def _define_dense_grid() -> tuple:
@@ -108,19 +119,20 @@ def generate_categorize(
108
119
 
109
120
  def _close_all() -> None:
110
121
  for obj in data.values():
111
- if isinstance(obj, Radar | Lidar | Mwr | Model):
122
+ if isinstance(obj, DataSource):
112
123
  obj.close()
113
124
 
114
125
  try:
126
+ is_disdrometer = "disdrometer" in input_files
115
127
  data = {
116
128
  "radar": Radar(input_files["radar"]),
117
129
  "lidar": Lidar(input_files["lidar"]),
118
130
  "mwr": Mwr(input_files["mwr"]),
119
131
  "lv0_files": input_files.get("lv0_files", None),
132
+ "disdrometer": Disdrometer(input_files["disdrometer"])
133
+ if is_disdrometer
134
+ else None,
120
135
  }
121
- if data["radar"].altitude is None:
122
- msg = "Radar altitude not defined"
123
- raise RuntimeError(msg)
124
136
  data["model"] = Model(input_files["model"], data["radar"].altitude)
125
137
  time, height = _define_dense_grid()
126
138
  valid_ind = _interpolate_to_cloudnet_grid()
@@ -141,7 +153,7 @@ def generate_categorize(
141
153
  classification = classify.classify_measurements(data)
142
154
  attenuations = atmos.get_attenuations(data, classification)
143
155
  data["radar"].correct_atten(attenuations)
144
- data["radar"].calc_errors(attenuations, classification)
156
+ data["radar"].calc_errors(attenuations, classification.is_clutter)
145
157
  quality = classify.fetch_quality(data, classification, attenuations)
146
158
  cloudnet_arrays = _prepare_output()
147
159
  date = data["radar"].get_date()
@@ -382,12 +394,6 @@ CATEGORIZE_ATTRIBUTES = {
382
394
  definition=DEFINITIONS["quality_bits"],
383
395
  units="1",
384
396
  ),
385
- "rainfall_rate": MetaData(
386
- long_name="Rainfall rate",
387
- standard_name="rainfall_rate",
388
- units="m s-1",
389
- comment="Fill values denote rain with undefined intensity.",
390
- ),
391
397
  "radar_liquid_atten": MetaData(
392
398
  long_name="Two-way radar attenuation due to liquid water",
393
399
  units="dB",
@@ -409,4 +415,9 @@ CATEGORIZE_ATTRIBUTES = {
409
415
  units="1",
410
416
  comment=COMMENTS["liquid_prob"],
411
417
  ),
418
+ "rain_detected": MetaData(
419
+ long_name="Rain detected",
420
+ units="1",
421
+ comment="1 = rain detected, 0 = no rain detected",
422
+ ),
412
423
  }
@@ -66,12 +66,11 @@ def classify_measurements(data: dict) -> ClassificationResult:
66
66
  bits[4] = _find_aerosols(obs, bits[1], bits[0])
67
67
  bits[4][filtered_ice] = False
68
68
  return ClassificationResult(
69
- _bits_to_integer(bits),
70
- obs.is_rain,
71
- obs.is_clutter,
72
- obs.rainfall_rate,
73
- insect_prob,
74
- liquid_prob,
69
+ category_bits=_bits_to_integer(bits),
70
+ is_rain=obs.is_rain,
71
+ is_clutter=obs.is_clutter,
72
+ insect_prob=insect_prob,
73
+ liquid_prob=liquid_prob,
75
74
  )
76
75
 
77
76
 
@@ -1,11 +1,10 @@
1
- import logging
2
1
  from dataclasses import dataclass
3
2
 
4
3
  import numpy as np
5
- import skimage
6
4
  from numpy import ma
7
5
 
8
6
  from cloudnetpy import utils
7
+ from cloudnetpy.constants import MM_H_TO_M_S
9
8
 
10
9
 
11
10
  @dataclass
@@ -15,7 +14,6 @@ class ClassificationResult:
15
14
  category_bits: np.ndarray
16
15
  is_rain: np.ndarray
17
16
  is_clutter: np.ndarray
18
- rainfall_rate: np.ndarray
19
17
  insect_prob: np.ndarray
20
18
  liquid_prob: np.ndarray | None
21
19
 
@@ -42,12 +40,12 @@ class ClassData:
42
40
  radar_type (str): Radar identifier.
43
41
  is_rain (ndarray): 2D boolean array denoting rain.
44
42
  is_clutter (ndarray): 2D boolean array denoting clutter.
45
- rainfall_rate: 1D rain rate.
46
43
  altitude: site altitude.
47
44
 
48
45
  """
49
46
 
50
47
  def __init__(self, data: dict):
48
+ self.data = data
51
49
  self.z = data["radar"].data["Z"][:]
52
50
  self.v = data["radar"].data["v"][:]
53
51
  self.v_sigma = data["radar"].data["v_sigma"][:]
@@ -61,61 +59,39 @@ class ClassData:
61
59
  self.model_type = data["model"].source_type
62
60
  self.beta = data["lidar"].data["beta"][:]
63
61
  self.lwp = data["mwr"].data["lwp"][:]
64
- self.is_rain = _find_rain_from_radar_echo(self.z, self.time)
65
- self.rainfall_rate = _find_rainfall_rate(self.is_rain, data["radar"])
62
+ self.is_rain = self._find_profiles_with_rain()
66
63
  self.is_clutter = _find_clutter(self.v, self.is_rain)
67
64
  self.altitude = data["radar"].altitude
68
65
  self.lv0_files = data["lv0_files"]
69
66
  self.date = data["radar"].get_date()
70
67
 
71
-
72
- def _find_rain_from_radar_echo(
73
- z: np.ndarray,
74
- time: np.ndarray,
75
- time_buffer: int = 5,
76
- ) -> np.ndarray:
77
- """Find profiles affected by rain.
78
-
79
- Rain is present in such profiles where the radar echo in
80
- the third range gate is > 0 dB. To make sure we do not include any
81
- rainy profiles, we also flag a few profiles before and after
82
- detections as raining.
83
-
84
- Args:
85
- z: Radar echo.
86
- time: Time vector.
87
- time_buffer: Time in minutes.
88
-
89
- Returns:
90
- 1D Boolean array denoting profiles with rain.
91
-
92
- """
93
- filled = False
94
- is_rain = ma.array(z[:, 3] > 0, dtype=bool).filled(filled)
95
- is_rain = skimage.morphology.remove_small_objects(
96
- is_rain,
97
- 2,
98
- connectivity=1,
99
- ) # Filter hot pixels
100
- n_profiles = len(time)
101
- n_steps = utils.n_elements(time, time_buffer, "time")
102
- for ind in np.where(is_rain)[0]:
103
- ind1 = max(0, ind - n_steps)
104
- ind2 = min(ind + n_steps, n_profiles)
105
- is_rain[ind1 : ind2 + 1] = True
106
- return is_rain
107
-
108
-
109
- def _find_rainfall_rate(is_rain: np.ndarray, radar) -> np.ndarray:
110
- rainfall_rate = ma.zeros(len(is_rain))
111
- rainfall_rate[is_rain] = ma.masked
112
- if "rainfall_rate" in radar.data:
113
- radar_rainfall_rate = radar.data["rainfall_rate"].data
114
- ind = np.where(~radar_rainfall_rate.mask)
115
- rainfall_rate[ind] = radar_rainfall_rate[ind]
116
- else:
117
- logging.info("No measured rain rate available")
118
- return rainfall_rate
68
+ def _find_profiles_with_rain(self) -> np.ndarray:
69
+ is_rain = self._find_rain_from_radar_echo()
70
+ rain_from_disdrometer = self._find_rain_from_disdrometer()
71
+ ind = ~rain_from_disdrometer.mask
72
+ is_rain[ind] = rain_from_disdrometer[ind]
73
+ return is_rain
74
+
75
+ def _find_rain_from_radar_echo(self) -> np.ndarray:
76
+ gate_number = 3
77
+ threshold = 0
78
+ z = self.z[:, gate_number]
79
+ return np.where((~ma.getmaskarray(z)) & (z > threshold), 1, 0)
80
+
81
+ def _find_rain_from_disdrometer(self) -> ma.MaskedArray:
82
+ threshold_mm_h = 0.25 # Standard threshold for drizzle -> rain
83
+ threshold_particles = 10 # This is arbitrary and should be better tested
84
+ threshold_rate = threshold_mm_h * MM_H_TO_M_S
85
+ try:
86
+ rainfall_rate = self.data["disdrometer"].data["rainfall_rate"].data
87
+ n_particles = self.data["disdrometer"].data["n_particles"].data
88
+ is_rain = ma.array(
89
+ (rainfall_rate > threshold_rate) & (n_particles > threshold_particles),
90
+ dtype=int,
91
+ )
92
+ except (AttributeError, KeyError):
93
+ is_rain = ma.masked_all(self.time.shape, dtype=int)
94
+ return is_rain
119
95
 
120
96
 
121
97
  def _find_clutter(
@@ -0,0 +1,53 @@
1
+ """Mwr module, containing the :class:`Mwr` class."""
2
+ import logging
3
+
4
+ import numpy as np
5
+ from numpy import ma
6
+ from scipy.interpolate import interp1d
7
+
8
+ from cloudnetpy.categorize.lidar import get_gap_ind
9
+ from cloudnetpy.datasource import DataSource
10
+
11
+
12
+ class Disdrometer(DataSource):
13
+ """Disdrometer class, child of DataSource.
14
+
15
+ Args:
16
+ ----
17
+ full_path: Cloudnet Level 1b disdrometer file.
18
+
19
+ """
20
+
21
+ def __init__(self, full_path: str):
22
+ super().__init__(full_path)
23
+ self._init_rainfall_rate()
24
+
25
+ def interpolate_to_grid(self, time_grid: np.ndarray) -> None:
26
+ for key, array in self.data.items():
27
+ self.data[key].data = self._interpolate(array.data, time_grid)
28
+
29
+ def _init_rainfall_rate(self) -> None:
30
+ keys = ("rainfall_rate", "n_particles")
31
+ for key in keys:
32
+ self.append_data(self.dataset.variables[key][:], key)
33
+
34
+ def _interpolate(self, y: ma.MaskedArray, x_new: np.ndarray) -> np.ndarray:
35
+ if ma.getmask(y) is ma.nomask:
36
+ non_masked_indices = np.arange(len(y))
37
+ elif y.mask.all():
38
+ return ma.masked_all(x_new.shape)
39
+ else:
40
+ non_masked_indices = np.where(~y.mask)[0]
41
+ non_masked_values = y[non_masked_indices]
42
+ non_masked_time = self.time[non_masked_indices]
43
+ fun = interp1d(non_masked_time, non_masked_values, fill_value="extrapolate")
44
+ interpolated_array = ma.array(fun(x_new))
45
+ max_time = 1 / 60 # min -> fraction hour
46
+ mask_ind = get_gap_ind(non_masked_time, x_new, max_time)
47
+
48
+ if len(mask_ind) > 0:
49
+ msg = f"Unable to interpolate disdrometer for {len(mask_ind)} time steps"
50
+ logging.warning(msg)
51
+ interpolated_array[mask_ind] = ma.masked
52
+
53
+ return interpolated_array
@@ -147,7 +147,8 @@ def _screen_insects(
147
147
  prob[(above_liquid == 1) & (insect_prob_no_ldr > 0)] = 0
148
148
 
149
149
  def _screen_rainy_profiles() -> None:
150
- prob[obs.is_rain == 1, :] = 0
150
+ rain_smoothed = _smooth_rain(obs.time, obs.is_rain)
151
+ prob[rain_smoothed == 1, :] = 0
151
152
 
152
153
  prob = np.copy(insect_prob)
153
154
  _screen_liquid_layers()
@@ -155,3 +156,15 @@ def _screen_insects(
155
156
  _screen_above_liquid()
156
157
  _screen_rainy_profiles()
157
158
  return prob
159
+
160
+
161
+ def _smooth_rain(time: np.ndarray, is_rain: np.ndarray) -> np.ndarray:
162
+ is_rain_smoothed = np.copy(is_rain)
163
+ time_buffer = 5 # minutes
164
+ n_profiles = len(is_rain)
165
+ n_steps = utils.n_elements(time, time_buffer, "time")
166
+ for rain_idx in np.where(is_rain)[0]:
167
+ idx_start = max(0, rain_idx - n_steps)
168
+ idx_end = min(rain_idx + n_steps, n_profiles)
169
+ is_rain_smoothed[idx_start : idx_end + 1] = True
170
+ return is_rain_smoothed
@@ -44,8 +44,8 @@ class Lidar(DataSource):
44
44
  height_new,
45
45
  )
46
46
  # Mask data points that are too far from the original grid
47
- time_gap_ind = _get_gap_ind(self.time[indices], time_new, max_time)
48
- height_gap_ind = _get_gap_ind(self.height, height_new, max_height)
47
+ time_gap_ind = get_gap_ind(self.time[indices], time_new, max_time)
48
+ height_gap_ind = get_gap_ind(self.height, height_new, max_height)
49
49
  self._mask_profiles(beta_interp, time_gap_ind, "time")
50
50
  self._mask_profiles(beta_interp, height_gap_ind, "height")
51
51
  self.data["beta"].data = beta_interp
@@ -69,7 +69,7 @@ class Lidar(DataSource):
69
69
  self.append_data(3.0, "beta_bias")
70
70
 
71
71
 
72
- def _get_gap_ind(grid: np.ndarray, new_grid: np.ndarray, threshold: float) -> list[int]:
72
+ def get_gap_ind(grid: np.ndarray, new_grid: np.ndarray, threshold: float) -> list[int]:
73
73
  return [
74
74
  ind
75
75
  for ind, value in enumerate(new_grid)
@@ -7,8 +7,7 @@ from numpy import ma
7
7
  from scipy import constants
8
8
 
9
9
  from cloudnetpy import utils
10
- from cloudnetpy.categorize.classify import ClassificationResult
11
- from cloudnetpy.constants import SEC_IN_HOUR
10
+ from cloudnetpy.constants import GHZ_TO_HZ, SEC_IN_HOUR, SPEED_OF_LIGHT
12
11
  from cloudnetpy.datasource import DataSource
13
12
 
14
13
 
@@ -69,8 +68,10 @@ class Radar(DataSource):
69
68
  )
70
69
  case "v_sigma":
71
70
  array.calc_linear_std(self.time, time_new)
72
- case "width" | "rainfall_rate":
71
+ case "width":
73
72
  array.rebin_data(self.time, time_new)
73
+ case "rainfall_rate":
74
+ array.rebin_data(self.time, time_new, mask_zeros=False)
74
75
  case _:
75
76
  continue
76
77
  return bad_time_indices
@@ -214,7 +215,7 @@ class Radar(DataSource):
214
215
  def calc_errors(
215
216
  self,
216
217
  attenuations: dict,
217
- classification: ClassificationResult,
218
+ is_clutter: np.ndarray,
218
219
  ) -> None:
219
220
  """Calculates uncertainties of radar echo.
220
221
 
@@ -223,7 +224,7 @@ class Radar(DataSource):
223
224
 
224
225
  Args:
225
226
  attenuations: 2-D attenuations due to atmospheric gases.
226
- classification: The :class:`ClassificationResult` instance.
227
+ is_clutter: 2-D boolean array denoting pixels contaminated by clutter.
227
228
 
228
229
  References:
229
230
  The method is based on Hogan R. and O'Connor E., 2004,
@@ -235,17 +236,24 @@ class Radar(DataSource):
235
236
  """Returns sensitivity of radar as function of altitude."""
236
237
  mean_gas_atten = ma.mean(attenuations["radar_gas_atten"], axis=0)
237
238
  z_sensitivity = z_power_min + log_range + mean_gas_atten
238
- zc = ma.median(ma.array(z, mask=~classification.is_clutter), axis=0)
239
+ zc = ma.median(ma.array(z, mask=~is_clutter), axis=0)
239
240
  valid_values = np.logical_not(zc.mask)
240
241
  z_sensitivity[valid_values] = zc[valid_values]
241
242
  return z_sensitivity
242
243
 
243
244
  def _calc_error() -> np.ndarray | float:
244
- if "width" not in self.data:
245
- return 0.3
246
- z_precision = 4.343 * (
247
- 1 / np.sqrt(_number_of_independent_pulses())
248
- + utils.db2lin(z_power_min - z_power) / 3
245
+ """Returns error of radar as function of altitude.
246
+
247
+ References:
248
+ Hogan, R. J., 1998: Dual-wavelength radar studies of clouds.
249
+ PhD Thesis, University of Reading, UK.
250
+
251
+ """
252
+ noise_threshold = 3
253
+ n_pulses = _number_of_independent_pulses()
254
+ ln_to_log10 = 10 / np.log(10)
255
+ z_precision = (ln_to_log10 / np.sqrt(n_pulses)) * (
256
+ 1 + (utils.db2lin(z_power_min - z_power) / noise_threshold)
249
257
  )
250
258
  gas_error = attenuations["radar_gas_atten"] * 0.1
251
259
  liq_error = attenuations["liquid_atten_err"].filled(0)
@@ -254,16 +262,22 @@ class Radar(DataSource):
254
262
  return z_error
255
263
 
256
264
  def _number_of_independent_pulses() -> float:
265
+ """Returns number of independent pulses.
266
+
267
+ References:
268
+ Atlas, D., 1964: Advances in radar meteorology.
269
+ Advances in Geophys., 10, 318-478.
270
+
271
+ """
272
+ if "width" not in self.data:
273
+ default_width = 0.3
274
+ width = np.zeros_like(self.data["Z"][:])
275
+ width[~width.mask] = default_width
276
+ else:
277
+ width = self.data["width"][:]
257
278
  dwell_time = utils.mdiff(self.time) * SEC_IN_HOUR
258
- return (
259
- dwell_time
260
- * self.radar_frequency
261
- * 1e9
262
- * 4
263
- * np.sqrt(math.pi)
264
- * self.data["width"][:]
265
- / 3e8
266
- )
279
+ wl = SPEED_OF_LIGHT / (self.radar_frequency * GHZ_TO_HZ)
280
+ return 4 * np.sqrt(math.pi) * dwell_time * width / wl
267
281
 
268
282
  def _calc_z_power_min() -> float:
269
283
  if ma.all(z_power.mask):
@@ -54,7 +54,9 @@ class CloudnetArray:
54
54
  """Masks data from given indices."""
55
55
  self.data[ind] = ma.masked
56
56
 
57
- def rebin_data(self, time: np.ndarray, time_new: np.ndarray) -> list:
57
+ def rebin_data(
58
+ self, time: np.ndarray, time_new: np.ndarray, *, mask_zeros: bool = True
59
+ ) -> list:
58
60
  """Rebins `data` in time.
59
61
 
60
62
  Args:
@@ -66,12 +68,14 @@ class CloudnetArray:
66
68
 
67
69
  """
68
70
  if self.data.ndim == 1:
69
- self.data = utils.rebin_1d(time, self.data, time_new)
71
+ self.data = utils.rebin_1d(time, self.data, time_new, mask_zeros=mask_zeros)
70
72
  bad_indices = list(np.where(self.data == ma.masked)[0])
71
73
  else:
72
74
  if not isinstance(self.data, ma.MaskedArray):
73
75
  self.data = ma.masked_array(self.data)
74
- self.data, bad_indices = utils.rebin_2d(time, self.data, time_new)
76
+ self.data, bad_indices = utils.rebin_2d(
77
+ time, self.data, time_new, mask_zeros=mask_zeros
78
+ )
75
79
  return bad_indices
76
80
 
77
81
  def fetch_attributes(self) -> list:
cloudnetpy/constants.py CHANGED
@@ -20,8 +20,12 @@ RS: Final = 287.058
20
20
  RHO_ICE: Final = 917
21
21
 
22
22
  # other
23
+ SPEED_OF_LIGHT: Final = 3.0e8
23
24
  SEC_IN_MINUTE: Final = 60
24
25
  SEC_IN_HOUR: Final = 3600
25
26
  SEC_IN_DAY: Final = 86400
26
27
  MM_TO_M: Final = 1e-3
27
28
  G_TO_KG: Final = 1e-3
29
+ M_S_TO_MM_H: Final = SEC_IN_HOUR / MM_TO_M
30
+ MM_H_TO_M_S: Final = 1 / M_S_TO_MM_H
31
+ GHZ_TO_HZ: Final = 1e9
@@ -372,7 +372,10 @@ def _parse_spectrum(tokens: Iterator[str]) -> np.ndarray:
372
372
  return np.array(values, dtype="i2").reshape((32, 32))
373
373
 
374
374
 
375
- PARSERS: dict[str, Callable[[Iterator[str]], Any]] = {
375
+ ParserType = Callable[[Iterator[str]], Any]
376
+
377
+
378
+ PARSERS: dict[str, ParserType] = {
376
379
  "I_heating": _parse_float,
377
380
  "T_sensor": _parse_int,
378
381
  "_T_pcb": _parse_int,
@@ -401,6 +404,16 @@ PARSERS: dict[str, Callable[[Iterator[str]], Any]] = {
401
404
  "visibility": _parse_int,
402
405
  }
403
406
 
407
+ EMPTY_VALUES: dict[ParserType, Any] = {
408
+ _parse_int: 0,
409
+ _parse_float: 0.0,
410
+ _parse_date: datetime.date(2000, 1, 1),
411
+ _parse_time: datetime.time(12, 0, 0),
412
+ _parse_datetime: datetime.datetime(2000, 1, 1),
413
+ _parse_vector: np.zeros(32, dtype=float),
414
+ _parse_spectrum: np.zeros((32, 32), dtype="i2"),
415
+ }
416
+
404
417
 
405
418
  def _parse_headers(line: str) -> list[str]:
406
419
  return [CSV_HEADERS[header.strip()] for header in line.split(";")]
@@ -508,7 +521,7 @@ def _read_toa5(filename: str | PathLike) -> dict[str, list]:
508
521
  return data
509
522
 
510
523
 
511
- def _read_typ_op4a(lines: list[str]) -> dict[str, list]:
524
+ def _read_typ_op4a(lines: list[str]) -> dict[str, Any]:
512
525
  """Read output of "CS/PA" command. The output starts with line "TYP OP4A"
513
526
  followed by one line per measured variable in format: <number>:<value>.
514
527
  Output ends with characters: <ETX><CR><LF><NUL>. Lines are separated by
@@ -527,7 +540,7 @@ def _read_typ_op4a(lines: list[str]) -> dict[str, list]:
527
540
  continue
528
541
  parser = PARSERS.get(varname, next)
529
542
  tokens = value.split(";")
530
- data[varname] = [parser(iter(tokens))]
543
+ data[varname] = parser(iter(tokens))
531
544
  return data
532
545
 
533
546
 
@@ -538,15 +551,26 @@ def _read_fmi(content: str):
538
551
  - output of "CS/PA" command without non-printable characters at the end
539
552
  - "]\n"
540
553
  """
541
- output: dict[str, Any] = defaultdict(list)
554
+ output: dict[str, list] = {"_datetime": []}
542
555
  for m in re.finditer(
543
556
  r"\[(?P<year>\d+)-(?P<month>\d+)-(?P<day>\d+) "
544
557
  r"(?P<hour>\d+):(?P<minute>\d+):(?P<second>\d+)"
545
558
  r"(?P<output>[^\]]*)\]",
546
559
  content,
547
560
  ):
548
- for key, value in _read_typ_op4a(m["output"].splitlines()).items():
561
+ try:
562
+ record = _read_typ_op4a(m["output"].splitlines())
563
+ except ValueError:
564
+ continue
565
+
566
+ for key, value in record.items():
567
+ if key not in output:
568
+ output[key] = [None] * len(output["_datetime"])
549
569
  output[key].append(value)
570
+ for key in output:
571
+ if key not in record and key != "_datetime":
572
+ output[key].append(None)
573
+
550
574
  output["_datetime"].append(
551
575
  datetime.datetime(
552
576
  int(m["year"]),
@@ -577,6 +601,7 @@ def _read_parsivel(
577
601
  data = _read_toa5(filename)
578
602
  elif "TYP OP4A" in lines[0]:
579
603
  data = _read_typ_op4a(lines)
604
+ data = {key: [value] for key, value in data.items()}
580
605
  elif "Date" in lines[0]:
581
606
  headers = _parse_headers(lines[0])
582
607
  data = _read_rows(headers, lines[1:])
@@ -597,6 +622,17 @@ def _read_parsivel(
597
622
  combined_data[key].extend(values)
598
623
  if timestamps is not None:
599
624
  combined_data["_datetime"] = list(timestamps)
600
- result = {key: np.array(value) for key, value in combined_data.items()}
625
+ result = {}
626
+ for key, value in combined_data.items():
627
+ array = np.array(
628
+ [
629
+ x
630
+ if x is not None
631
+ else (EMPTY_VALUES[PARSERS[key]] if key in PARSERS else "")
632
+ for x in value
633
+ ]
634
+ )
635
+ mask = [np.full(array.shape[1:], x is None) for x in value]
636
+ result[key] = ma.array(array, mask=mask)
601
637
  result["time"] = result["_datetime"].astype("datetime64[s]")
602
638
  return result
cloudnetpy/output.py CHANGED
@@ -317,7 +317,7 @@ def add_time_attribute(
317
317
 
318
318
 
319
319
  def add_source_attribute(attributes: dict, data: dict) -> dict:
320
- """Adds source attribute."""
320
+ """Adds source attribute to variables."""
321
321
  variables = {
322
322
  "radar": (
323
323
  "v",
@@ -334,8 +334,11 @@ def add_source_attribute(attributes: dict, data: dict) -> dict:
334
334
  "lidar": ("beta", "lidar_wavelength"),
335
335
  "mwr": ("lwp",),
336
336
  "model": ("uwind", "vwind", "Tw", "q", "pressure", "temperature"),
337
+ "disdrometer": ("rainfall_rate",),
337
338
  }
338
339
  for instrument, keys in variables.items():
340
+ if data[instrument] is None:
341
+ continue
339
342
  source = data[instrument].dataset.source
340
343
  for key in keys:
341
344
  if key in attributes:
@@ -514,6 +514,9 @@ class Plot2D(Plot):
514
514
  if figure_data.height is None:
515
515
  msg = "No height information in the file."
516
516
  raise ValueError(msg)
517
+ if self._data.ndim < 2:
518
+ msg = "Data has to be 2D."
519
+ raise PlottingError(msg)
517
520
  alt = figure_data.height
518
521
  if figure_data.options.max_y is None:
519
522
  return alt
@@ -214,7 +214,7 @@ class DerSource(DataSource):
214
214
  is_retrieved = ~self.data["der"][:].mask
215
215
  is_mixed = droplet_classification.is_mixed
216
216
  is_ice = droplet_classification.is_ice
217
- is_rain = np.tile(self.is_rain, (is_retrieved.shape[1], 1)).T
217
+ is_rain = np.tile(self.is_rain == 1, (is_retrieved.shape[1], 1)).T
218
218
 
219
219
  retrieval_status = np.zeros(is_retrieved.shape, dtype=int)
220
220
  retrieval_status[is_ice] = 4
@@ -4,6 +4,7 @@ from bisect import bisect_left
4
4
 
5
5
  import netCDF4
6
6
  import numpy as np
7
+ from numpy import ma
7
8
  from scipy.special import gamma
8
9
 
9
10
  from cloudnetpy import utils
@@ -101,7 +102,7 @@ class DrizzleClassification(ProductClassification):
101
102
 
102
103
  @staticmethod
103
104
  def _find_v_sigma(cat_file: str) -> np.ndarray:
104
- v_sigma = product_tools.read_nc_fields(cat_file, "v_sigma")
105
+ v_sigma = product_tools.read_nc_field(cat_file, "v_sigma")
105
106
  return np.isfinite(v_sigma)
106
107
 
107
108
  def _find_warm_liquid(self) -> np.ndarray:
@@ -160,11 +161,11 @@ class SpectralWidth:
160
161
  self.width_ht = self._calculate_spectral_width()
161
162
 
162
163
  def _calculate_spectral_width(self) -> np.ndarray:
163
- v_sigma = product_tools.read_nc_fields(self.cat_file, "v_sigma")
164
+ v_sigma = product_tools.read_nc_field(self.cat_file, "v_sigma")
164
165
  try:
165
- width = product_tools.read_nc_fields(self.cat_file, "width")
166
+ width = product_tools.read_nc_field(self.cat_file, "width")
166
167
  except KeyError:
167
- width = [0]
168
+ width = ma.array([0])
168
169
  logging.warning("No spectral width, assuming width = %s", width[0])
169
170
  sigma_factor = self._calc_v_sigma_factor()
170
171
  return width - sigma_factor * v_sigma
@@ -178,7 +179,7 @@ class SpectralWidth:
178
179
 
179
180
  def _calc_beam_divergence(self) -> np.ndarray:
180
181
  beam_width = 0.5
181
- height = product_tools.read_nc_fields(self.cat_file, "height")
182
+ height = product_tools.read_nc_field(self.cat_file, "height")
182
183
  return height * np.deg2rad(beam_width)
183
184
 
184
185
  def _calc_horizontal_wind(self) -> np.ndarray:
@@ -232,32 +232,35 @@ class IceSource(DataSource):
232
232
 
233
233
  def get_is_rain(filename: str) -> np.ndarray:
234
234
  try:
235
- rainfall_rate = read_nc_fields(filename, "rainfall_rate")
235
+ is_rain = read_nc_field(filename, "rain_detected")
236
236
  except KeyError:
237
- rainfall_rate = read_nc_fields(filename, "rain_rate")
238
- is_rain = rainfall_rate != 0
239
- if not isinstance(is_rain, ma.MaskedArray):
240
- is_rain = ma.array(is_rain)
241
- is_rain[is_rain.mask] = True
237
+ try:
238
+ rainfall_rate = read_nc_field(filename, "rainfall_rate")
239
+ except KeyError:
240
+ rainfall_rate = read_nc_field(filename, "rain_rate")
241
+ is_rain = rainfall_rate != 0
242
+ is_rain[is_rain.mask] = True
242
243
  return np.array(is_rain)
243
244
 
244
245
 
245
- def read_nc_fields(nc_file: str, names: str | list) -> ma.MaskedArray | list:
246
+ def read_nc_field(nc_file: str, name: str) -> ma.MaskedArray:
247
+ with netCDF4.Dataset(nc_file) as nc:
248
+ return nc.variables[name][:]
249
+
250
+
251
+ def read_nc_fields(nc_file: str, names: list[str]) -> list[ma.MaskedArray]:
246
252
  """Reads selected variables from a netCDF file.
247
253
 
248
254
  Args:
249
255
  nc_file: netCDF file name.
250
- names: Variables to be read, e.g. 'temperature' or ['ldr', 'lwp'].
256
+ names: Variables to be read, e.g. ['ldr', 'lwp'].
251
257
 
252
258
  Returns:
253
- ndarray/list: Array in case of one variable passed as a string.
254
- List of arrays otherwise.
259
+ List of numpy arrays.
255
260
 
256
261
  """
257
- names = [names] if isinstance(names, str) else names
258
262
  with netCDF4.Dataset(nc_file) as nc:
259
- data = [nc.variables[name][:] for name in names]
260
- return data[0] if len(data) == 1 else data
263
+ return [nc.variables[name][:] for name in names]
261
264
 
262
265
 
263
266
  def interpolate_model(cat_file: str, names: str | list) -> dict[str, np.ndarray]:
cloudnetpy/utils.py CHANGED
@@ -140,6 +140,8 @@ def rebin_2d(
140
140
  x_new: np.ndarray,
141
141
  statistic: str = "mean",
142
142
  n_min: int = 1,
143
+ *,
144
+ mask_zeros: bool = True,
143
145
  ) -> tuple[ma.MaskedArray, list]:
144
146
  """Rebins 2-D data in one dimension.
145
147
 
@@ -171,7 +173,10 @@ def rebin_2d(
171
173
  bins=edges,
172
174
  )
173
175
  result[~np.isfinite(result)] = 0
174
- masked_result = ma.masked_equal(result, 0)
176
+ if mask_zeros is True:
177
+ masked_result = ma.masked_equal(result, 0)
178
+ else:
179
+ masked_result = ma.array(result)
175
180
 
176
181
  # Fill bins with not enough profiles
177
182
  empty_indices = []
@@ -191,6 +196,8 @@ def rebin_1d(
191
196
  array: np.ndarray | ma.MaskedArray,
192
197
  x_new: np.ndarray,
193
198
  statistic: str = "mean",
199
+ *,
200
+ mask_zeros: bool = True,
194
201
  ) -> ma.MaskedArray:
195
202
  """Rebins 1D array.
196
203
 
@@ -217,7 +224,9 @@ def rebin_1d(
217
224
  bins=edges,
218
225
  )
219
226
  result[~np.isfinite(result)] = 0
220
- return ma.masked_equal(result, 0)
227
+ if mask_zeros:
228
+ return ma.masked_equal(result, 0)
229
+ return ma.array(result)
221
230
 
222
231
 
223
232
  def filter_isolated_pixels(array: np.ndarray) -> np.ndarray:
cloudnetpy/version.py CHANGED
@@ -1,4 +1,4 @@
1
1
  MAJOR = 1
2
- MINOR = 57
3
- PATCH = 0
2
+ MINOR = 58
3
+ PATCH = 1
4
4
  __version__ = f"{MAJOR}.{MINOR}.{PATCH}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cloudnetpy
3
- Version: 1.57.0
3
+ Version: 1.58.1
4
4
  Summary: Python package for Cloudnet processing
5
5
  Author: Simo Tukiainen
6
6
  License: MIT License
@@ -1,29 +1,30 @@
1
1
  cloudnetpy/__init__.py,sha256=X_FqY-4yg5GUj5Edo14SToLEos6JIsC3fN-v1FUgQoA,43
2
- cloudnetpy/cloudnetarray.py,sha256=vSI6hqozhS1ntNIgY2kqTt7N6BF_xJtUii_vsuyxTno,6869
2
+ cloudnetpy/cloudnetarray.py,sha256=HT6bLtjnimOVbGrdjQBqD0F8GW0KWkn2qhaIGFMKLAY,6987
3
3
  cloudnetpy/concat_lib.py,sha256=YK5ho5msqwNxpPtPT8f2OewIJ8hTrbhCkaxHaBKLTI0,9809
4
- cloudnetpy/constants.py,sha256=OMp3pKHCZmdKyRvfO35E7vE3FrS_DHIs_GJuexJLCQk,600
4
+ cloudnetpy/constants.py,sha256=l7_ohQgLEQ6XEG9AMBarTPKp9OM8B1ElJ6fSN0ScdmM,733
5
5
  cloudnetpy/datasource.py,sha256=-6oLC5bsn9sIoaN0glV88owFyTeGRsW1ZVJSV8rM5rE,7813
6
6
  cloudnetpy/exceptions.py,sha256=nA6YieylwKSp5KQOh3DhhcTmUBsd0tBZnedgmlWck-w,1415
7
7
  cloudnetpy/metadata.py,sha256=Bcu1a9UyUq61jomuZ0_6hYIOzf61e5qCXeiwLm46ikw,5040
8
- cloudnetpy/output.py,sha256=jD1pfBb4OQhVOrlhPEk-8FAi4bUW7zjAL468r6BPkJg,14586
8
+ cloudnetpy/output.py,sha256=WoVTNuxni0DUr163vZ-_mDr1brXhY15XSlGMrq9Aoqw,14700
9
9
  cloudnetpy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- cloudnetpy/utils.py,sha256=yY5a5HLuAks2uzA4XbbqsGFEmXoyqECn_TjD3sMa0lI,27193
11
- cloudnetpy/version.py,sha256=qC35wCh1KxhBnDTG1irZ-STVg6O7ebRlvaGi3fZqxjA,72
10
+ cloudnetpy/utils.py,sha256=0TlHm71YtSrKXBsRKctitnhQrvZPE-ulEVeAQW-oK58,27398
11
+ cloudnetpy/version.py,sha256=ACE_5AEhiiFIDukxdKYlkIvBmsdfhExmwRoHxF3tRPs,72
12
12
  cloudnetpy/categorize/__init__.py,sha256=gP5q3Vis1y9u9OWgA_idlbjfWXYN_S0IBSWdwBhL_uU,69
13
- cloudnetpy/categorize/atmos.py,sha256=cax3iRmvr7S-VkUZqz0JCfAN3WEsUVbGfH4zSHy1APo,12384
13
+ cloudnetpy/categorize/atmos.py,sha256=fWW8ye_8HZASRAiYwURFKWzcGOYIA2RKeVxCq0lVOuM,12389
14
14
  cloudnetpy/categorize/atmos_utils.py,sha256=wndpwJxc2-QnNTkV8tc8I11Vs_WkNz9sVMX1fuGgUC4,3777
15
- cloudnetpy/categorize/categorize.py,sha256=88gHVRHDB0eyapWUrKwo_T6ep-vtAT1_NS1yy3f59lo,16970
16
- cloudnetpy/categorize/classify.py,sha256=NwJXt44bynlNOuYAlt0i3rOgbuU4zVMQDnYxG92jOx0,8875
17
- cloudnetpy/categorize/containers.py,sha256=bt6q7vUGn9V8mpNcB44mWNZj92rYmaIh-SDlRIrQY_g,4608
15
+ cloudnetpy/categorize/categorize.py,sha256=_chLFT0l9ll78y3oaxFwOTBbv2raxxjghCz-_KGdImQ,17476
16
+ cloudnetpy/categorize/classify.py,sha256=vIR7Ztu41kQbRaA2xIy_mm08KxVgQczYXpNSo0vz2Yg,8905
17
+ cloudnetpy/categorize/containers.py,sha256=j6oSKPeZcq9vFthYaocAw1m6yReRNNPYUQF5UTDq4YM,4232
18
+ cloudnetpy/categorize/disdrometer.py,sha256=0Z0nvUtoZKDxiUfBZzoYZxUFOVjq-thmYfaCkskeECs,1799
18
19
  cloudnetpy/categorize/droplet.py,sha256=pUmB-gN0t9sVgsGLof6X9N0nuEb4EBtEUswwpoQapTY,8687
19
20
  cloudnetpy/categorize/falling.py,sha256=xES5ZdYs34tbX1p4a9kzt9r3G5s25Mpvs5WeFs1KNzo,4385
20
21
  cloudnetpy/categorize/freezing.py,sha256=684q83TPQ5hHrbbHX-E36VoTlWLSOlGfOW1FC8b3wfI,3754
21
- cloudnetpy/categorize/insects.py,sha256=ubNp7-lZWXsQ3qYgyEqqjSF2LWnHdlYTInUGKYl_p_s,5242
22
- cloudnetpy/categorize/lidar.py,sha256=4dRbD3E1r36d1cEIaHBVh8BiXXlEFBdqhNIyH682K4k,2607
22
+ cloudnetpy/categorize/insects.py,sha256=7m31aQSO9nekf_d3TgXMnPpgHIP7J_xhHLShfQ9JS9E,5764
23
+ cloudnetpy/categorize/lidar.py,sha256=LYqXw30sLOYxhKRcO3k5r0uVLGRYmJ5k0KuVOMduY5A,2604
23
24
  cloudnetpy/categorize/melting.py,sha256=AOq36yLntDXYbeMw5QhZ7kMLwt0INyUbhzv-rSILLyo,6261
24
25
  cloudnetpy/categorize/model.py,sha256=xWB6XOSz9p0h4b4m6ImMmzcTImOmz54d093WmsLogdQ,5535
25
26
  cloudnetpy/categorize/mwr.py,sha256=-KMoYlch_C79bqgcEiRDCTRCcQf1ZsYxU90GQ8hzMgs,1435
26
- cloudnetpy/categorize/radar.py,sha256=gephjRnEEMMCfTGX-HVT5kklAZblvkkGwf3SPCngqe4,12981
27
+ cloudnetpy/categorize/radar.py,sha256=S3561FVK6J4NUjqPi2S6fKnjnOm0oLwbIC54J64AN4Y,13659
27
28
  cloudnetpy/instruments/__init__.py,sha256=_jejVwi_viSZehmAOkEqTNI-0-exGgAJ_bHW1IRRwTI,398
28
29
  cloudnetpy/instruments/basta.py,sha256=OeWBP_5b2xjOzPaFWEFC0irgLhSqX1YdcPkVBRbxd4c,3742
29
30
  cloudnetpy/instruments/campbell_scientific.py,sha256=2WHfBKQjtRSl0AqvtPeX7G8Hdi3Dn0WbvoAppFOMbA8,5270
@@ -48,7 +49,7 @@ cloudnetpy/instruments/vaisala.py,sha256=E6PaK26lHprqOJUCEDZPZQu83Qan9n_THudTFQM
48
49
  cloudnetpy/instruments/weather_station.py,sha256=IMJHGXfMhb4jJw_i66oGDCkeeRn3_eko8zVehu6Fte0,5970
49
50
  cloudnetpy/instruments/disdrometer/__init__.py,sha256=lyjwttWvFvuwYxEkusoAvgRcbBmglmOp5HJOpXUqLWo,93
50
51
  cloudnetpy/instruments/disdrometer/common.py,sha256=TTsvWzhHg5tansTs47WB-7uuBRCZdjbFQMAyAQtFkSU,15636
51
- cloudnetpy/instruments/disdrometer/parsivel.py,sha256=myBHJw4VDPJErPlzAgwLoTjUQOGcvXdbvvy0MEjyGBc,21293
52
+ cloudnetpy/instruments/disdrometer/parsivel.py,sha256=TWq8VgG8U75AJQfCX-V2y8qy-nO6dKMOGd2eG8-u7to,22342
52
53
  cloudnetpy/instruments/disdrometer/thies.py,sha256=h7EwZ9tn47UUMiYqDQ68vkXv4q0rEqX1ZeFXd7XJYNg,5050
53
54
  cloudnetpy/model_evaluation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
55
  cloudnetpy/model_evaluation/file_handler.py,sha256=oUGIblcEWLLv16YKUch-M5KA-dGRAcuHa-9anP3xtX4,6447
@@ -92,22 +93,22 @@ cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py,sha256=Ra3r4V
92
93
  cloudnetpy/model_evaluation/tests/unit/test_tools.py,sha256=Ia_VrLdV2NstX5gbx_3AZTOAlrgLAy_xFZ8fHYVX0xI,3817
93
94
  cloudnetpy/plotting/__init__.py,sha256=lg9Smn4BI0dVBgnDLC3JVJ4GmwoSnO-qoSd4ApvwV6Y,107
94
95
  cloudnetpy/plotting/plot_meta.py,sha256=MK_-fByZgihXkTXVJhs942qvq9SLfyzLa911y6x8cV0,14791
95
- cloudnetpy/plotting/plotting.py,sha256=LzO0UIMSGLsF0Q5Tkr5G5Xa8Ah7pi1ZzibksBIOLoEI,30755
96
+ cloudnetpy/plotting/plotting.py,sha256=TUC6kZqpBDE-EgehkFFebw4y9tZOIVP3kH6UHh41H3o,30863
96
97
  cloudnetpy/products/__init__.py,sha256=2hRb5HG9hNrxH1if5laJkLeFeaZCd5W1q3hh4ewsX0E,273
97
98
  cloudnetpy/products/classification.py,sha256=0E9OUGR3uLCsS1nORwQu0SqW0_8uX7n6LlRcVhtzKw4,7845
98
- cloudnetpy/products/der.py,sha256=HAdPvbJySEqkIwDrdZDPnli_wnN2qwm72_D1a82ZWIs,12398
99
+ cloudnetpy/products/der.py,sha256=mam6jWV7A2h8V5WC3DIeFp6ou7UD1JOw9r7h2B0su-s,12403
99
100
  cloudnetpy/products/drizzle.py,sha256=BY2HvJeWt_ps6KKCGXwUUNRTy78q0cQM8bOCCoj8TWA,10803
100
101
  cloudnetpy/products/drizzle_error.py,sha256=4GwlHRtNbk9ks7bGtXCco-wXbcDOKeAQwKmbhzut6Qk,6132
101
- cloudnetpy/products/drizzle_tools.py,sha256=wGnYVPCzQpvIRV0OJoL-5NavstEA1JuHsj3Is2uxp48,10820
102
+ cloudnetpy/products/drizzle_tools.py,sha256=UhcJbPa4tXHbuVlegIRfOl5nZ_E6ddKv20aghfP0hdg,10847
102
103
  cloudnetpy/products/ier.py,sha256=IcGPlQahbwJjp3vOOrxWSYW2FPzbSV0KQL5eYECc4kU,7777
103
104
  cloudnetpy/products/iwc.py,sha256=MUPuVKWgqOuuLRCGk3QY74uBZB_7P1qlinlP8nEvz9o,10124
104
105
  cloudnetpy/products/lwc.py,sha256=TbIR6kMwjbm63ed5orB1pkqx9ZBm8C5TF2JmT8WKdKI,18794
105
106
  cloudnetpy/products/mie_lu_tables.nc,sha256=It4fYpqJXlqOgL8jeZ-PxGzP08PMrELIDVe55y9ob58,16637951
106
107
  cloudnetpy/products/mwr_tools.py,sha256=PRm5aCULccUehU-Byk55wYhhEHseMjoAjGBu5TSyHao,4621
107
- cloudnetpy/products/product_tools.py,sha256=E8CSijBY8cr70BH2JFa0lGQ-RzI9EcHQ0Fzt8CQ8rY4,10442
108
+ cloudnetpy/products/product_tools.py,sha256=rhx_Ru9FLlQqCNM-awoiHx18-Aq1eBwL9LiUaQoJs6k,10412
108
109
  docs/source/conf.py,sha256=IKiFWw6xhUd8NrCg0q7l596Ck1d61XWeVjIFHVSG9Og,1490
109
- cloudnetpy-1.57.0.dist-info/LICENSE,sha256=wcZF72bdaoG9XugpyE95Juo7lBQOwLuTKBOhhtANZMM,1094
110
- cloudnetpy-1.57.0.dist-info/METADATA,sha256=YfJf13JTJBey4rImv0KbljSElInh9z4EygDLWZPDF08,5733
111
- cloudnetpy-1.57.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
112
- cloudnetpy-1.57.0.dist-info/top_level.txt,sha256=ibSPWRr6ojS1i11rtBFz2_gkIe68mggj7aeswYfaOo0,16
113
- cloudnetpy-1.57.0.dist-info/RECORD,,
110
+ cloudnetpy-1.58.1.dist-info/LICENSE,sha256=wcZF72bdaoG9XugpyE95Juo7lBQOwLuTKBOhhtANZMM,1094
111
+ cloudnetpy-1.58.1.dist-info/METADATA,sha256=CqdJACjYG1_pszHVdzrrr6RZGJNO-YUvhSmrNEuBhz8,5733
112
+ cloudnetpy-1.58.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
113
+ cloudnetpy-1.58.1.dist-info/top_level.txt,sha256=ibSPWRr6ojS1i11rtBFz2_gkIe68mggj7aeswYfaOo0,16
114
+ cloudnetpy-1.58.1.dist-info/RECORD,,