cloudnetpy 1.82.3__py3-none-any.whl → 1.83.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.
@@ -13,14 +13,14 @@ def calc_melting_attenuation(
13
13
  data: Observations, classification: ClassificationResult
14
14
  ) -> Attenuation:
15
15
  shape = classification.category_bits.melting.shape
16
- is_rain = classification.is_rain
16
+ no_rain = classification.is_rain == 0
17
17
 
18
18
  affected_region = classification.category_bits.freezing.copy()
19
19
 
20
20
  if data.disdrometer is None:
21
- affected_region[~is_rain, :] = False
22
21
  above_melting = utils.ffill(classification.category_bits.melting)
23
22
  affected_region[~above_melting] = False
23
+ affected_region[no_rain, :] = False
24
24
  return Attenuation(
25
25
  amount=ma.masked_all(shape),
26
26
  error=ma.masked_all(shape),
@@ -29,22 +29,23 @@ def calc_melting_attenuation(
29
29
  )
30
30
 
31
31
  rainfall_rate = data.disdrometer.data["rainfall_rate"][:]
32
- rainfall_rate[is_rain == 0] = ma.masked
32
+ rainfall_rate = ma.where(no_rain, 0, rainfall_rate)
33
+
33
34
  frequency = data.radar.radar_frequency
34
35
 
35
36
  attenuation_array = _calc_melting_attenuation(rainfall_rate, frequency)
36
37
 
37
38
  amount = affected_region * utils.transpose(attenuation_array)
38
39
 
39
- affected_region[amount == 0] = False
40
+ no_attenuation = amount == 0
40
41
 
41
- amount[amount == 0] = ma.masked
42
+ affected_region[no_attenuation] = False
43
+ amount[no_attenuation] = ma.masked
42
44
 
43
45
  band = utils.get_wl_band(data.radar.radar_frequency)
44
46
  error_factor = {"Ka": 0.2, "W": 0.1}[band]
45
47
 
46
48
  error = amount * error_factor
47
- error[~affected_region] = ma.masked
48
49
 
49
50
  return Attenuation(
50
51
  amount=amount,
cloudnetpy/datasource.py CHANGED
@@ -74,14 +74,14 @@ class DataSource:
74
74
  ndarray: The actual data.
75
75
 
76
76
  Raises:
77
- RuntimeError: The variable is not found.
77
+ KeyError: The variable is not found.
78
78
 
79
79
  """
80
80
  for arg in args:
81
81
  if arg in self.dataset.variables:
82
82
  return self.dataset.variables[arg][:]
83
83
  msg = f"Missing variable {args[0]} in the input file."
84
- raise RuntimeError(msg)
84
+ raise KeyError(msg)
85
85
 
86
86
  def append_data(
87
87
  self,
@@ -41,7 +41,7 @@ class NcRadar(DataSource, CloudnetInstrument):
41
41
  name = keymap[key]
42
42
  try:
43
43
  array = self.getvar(key)
44
- except RuntimeError:
44
+ except KeyError:
45
45
  logging.warning("Can not find variable %s from the input file", key)
46
46
  continue
47
47
  array = np.array(array) if utils.isscalar(array) else array
@@ -158,7 +158,7 @@ class NcRadar(DataSource, CloudnetInstrument):
158
158
  self.data[key] = CloudnetArray(np.median(np.array(data)), key)
159
159
  if "NyquistVelocity" in self.data:
160
160
  del self.data["NyquistVelocity"]
161
- except RuntimeError:
161
+ except KeyError:
162
162
  logging.warning("Unable to find nyquist_velocity")
163
163
 
164
164
  def test_if_all_masked(self) -> None:
@@ -104,7 +104,7 @@ class ObservationManager(DataSource):
104
104
  """Check if rainrate in file."""
105
105
  try:
106
106
  self.getvar("rainrate")
107
- except RuntimeError:
107
+ except KeyError:
108
108
  return False
109
109
  return True
110
110
 
@@ -90,6 +90,12 @@ _CLABEL = {
90
90
  ("Clutter", _COLORS["shockred"]),
91
91
  ("_Lidar molecular scattering", _COLORS["pink"]),
92
92
  ),
93
+ "signal_source_status": (
94
+ ("Clear sky", _COLORS["white"]),
95
+ ("Radar & lidar", _COLORS["green"]),
96
+ ("Radar only", _COLORS["lightsteel"]),
97
+ ("Lidar only", _COLORS["yellow"]),
98
+ ),
93
99
  "ice_retrieval_status": (
94
100
  ("_No ice", _COLORS["white"]),
95
101
  ("Reliable", _COLORS["green"]),
@@ -124,6 +130,15 @@ _CLABEL = {
124
130
  ("Unfeasible", _COLORS["red"]),
125
131
  ("Surrounding ice", _COLORS["lightsteel"]),
126
132
  ),
133
+ "radar_attenuation_status": (
134
+ ("_Clear sky", _COLORS["white"]),
135
+ ("Negligible", _COLORS["green"]),
136
+ ("Minor", _COLORS["lightgreen"]),
137
+ ("Moderate", _COLORS["yellow"]),
138
+ ("Severe", _COLORS["red"]),
139
+ ("Unquantifiable", _COLORS["seaweed_roll"]),
140
+ ("Undetected", _COLORS["skyblue"]),
141
+ ),
127
142
  }
128
143
 
129
144
 
@@ -213,6 +228,9 @@ ATTRIBUTES = {
213
228
  ),
214
229
  },
215
230
  "fallback": {
231
+ "cloud_top_height_agl": PlotMeta(
232
+ moving_average=False,
233
+ ),
216
234
  "nubf": PlotMeta(plot_range=(0, 5)),
217
235
  "ze_sat": PlotMeta(
218
236
  plot_range=(-40, 15),
@@ -281,6 +299,9 @@ ATTRIBUTES = {
281
299
  "der_retrieval_status": PlotMeta(
282
300
  clabel=_CLABEL["der_retrieval_status"],
283
301
  ),
302
+ "radar_attenuation_status": PlotMeta(
303
+ clabel=_CLABEL["radar_attenuation_status"],
304
+ ),
284
305
  "mu": PlotMeta(
285
306
  plot_range=(0, 10),
286
307
  ),
@@ -554,6 +575,9 @@ ATTRIBUTES = {
554
575
  "detection_status": PlotMeta(
555
576
  clabel=_CLABEL["detection_status"],
556
577
  ),
578
+ "signal_source_status": PlotMeta(
579
+ clabel=_CLABEL["signal_source_status"],
580
+ ),
557
581
  "iwc": PlotMeta(
558
582
  plot_range=(1e-7, 1e-3),
559
583
  log_scale=True,
@@ -28,6 +28,7 @@ from cloudnetpy.categorize.atmos_utils import calc_altitude
28
28
  from cloudnetpy.exceptions import PlottingError
29
29
  from cloudnetpy.instruments.ceilometer import calc_sigma_units
30
30
  from cloudnetpy.plotting.plot_meta import ATTRIBUTES, PlotMeta
31
+ from cloudnetpy.products.classification import TopStatus
31
32
 
32
33
  EARTHCARE_MAX_X = 517.84
33
34
 
@@ -375,6 +376,7 @@ class Plot:
375
376
  "air_temperature": (add, -273.15, "\u00b0C"),
376
377
  "r_accum_RT": (multiply, 1000, "mm"),
377
378
  "r_accum_NRT": (multiply, 1000, "mm"),
379
+ "cloud_top_height_agl": (multiply, con.M_TO_KM, "Height (km AGL)"),
378
380
  }
379
381
  conversion_method, conversion, units = units_conversion.get(
380
382
  self.sub_plot.variable.name, (multiply, 1, None)
@@ -467,6 +469,12 @@ class Plot:
467
469
  self._data = data_new
468
470
  figure_data.time_including_gaps = time_new
469
471
 
472
+ def _read_cloud_top_flags(
473
+ self, figure_data: FigureData, flag_value: int | tuple[int, ...]
474
+ ) -> ndarray:
475
+ status = figure_data.file.variables["cloud_top_height_status"][:]
476
+ return np.isin(status, flag_value)
477
+
470
478
  def _read_flagged_data(self, figure_data: FigureData) -> ndarray:
471
479
  flag_names = [
472
480
  f"{self.sub_plot.variable.name}_quality_flag",
@@ -503,6 +511,32 @@ class Plot2D(Plot):
503
511
  if figure_data.is_mwrpy_product():
504
512
  self._fill_flagged_data(figure_data)
505
513
 
514
+ if figure_data.variables[0].name == "signal_source_status":
515
+ self._indicate_rainy_profiles(figure_data)
516
+
517
+ def _indicate_rainy_profiles(self, figure_data: FigureData) -> None:
518
+ if "rain_detected" not in figure_data.file.variables:
519
+ return
520
+ rain = figure_data.file.variables["rain_detected"][:]
521
+ is_rain: ma.MaskedArray = ma.masked_array(np.zeros_like(rain), mask=(rain == 0))
522
+ if is_rain.mask.all():
523
+ return
524
+ self._ax.plot(
525
+ figure_data.time,
526
+ is_rain,
527
+ color="red",
528
+ marker="|",
529
+ linestyle="None",
530
+ markersize=10,
531
+ zorder=-999,
532
+ label="Rain",
533
+ )
534
+ self._ax.legend(
535
+ markerscale=0.75,
536
+ numpoints=1,
537
+ frameon=False,
538
+ )
539
+
506
540
  def _fill_flagged_data(self, figure_data: FigureData) -> None:
507
541
  flags = self._read_flagged_data(figure_data)
508
542
  batches = find_batches_of_ones(flags)
@@ -672,11 +706,35 @@ class Plot1D(Plot):
672
706
  self.sub_plot.set_yax(ylabel=units, y_limits=self._get_y_limits())
673
707
  pos = self._ax.get_position()
674
708
  self._ax.set_position((pos.x0, pos.y0, pos.width * 0.965, pos.height))
709
+ self._plot_flags(figure_data)
710
+
711
+ def _plot_flags(self, figure_data: FigureData) -> None:
675
712
  if figure_data.is_mwrpy_product():
676
713
  flags = self._read_flagged_data(figure_data)
677
714
  if np.any(flags):
678
715
  self._plot_flag_data(figure_data.time[flags], self._data_orig[flags])
679
716
  self._add_legend()
717
+ if (
718
+ figure_data.variables[0].name == "cloud_top_height_agl"
719
+ and "cloud_top_height_status" in figure_data.file.variables
720
+ ):
721
+ legend: tuple = ()
722
+ flag_value = (TopStatus.MODERATE_ATT, TopStatus.UNCORR_ATT)
723
+ flags = self._read_cloud_top_flags(figure_data, flag_value)
724
+ if np.any(flags):
725
+ self._plot_flag_data(
726
+ figure_data.time[flags], self._data_orig[flags], color="orange"
727
+ )
728
+ legend += ("Suspicious",)
729
+ flag_value = (TopStatus.SEVERE_ATT, TopStatus.ABOVE_RANGE)
730
+ flags = self._read_cloud_top_flags(figure_data, flag_value)
731
+ if np.any(flags):
732
+ self._plot_flag_data(
733
+ figure_data.time[flags], self._data_orig[flags], color="red"
734
+ )
735
+ legend += ("Unreliable",)
736
+ if legend:
737
+ self._add_legend(name=legend)
680
738
 
681
739
  def plot_tb(self, figure_data: FigureData, freq_ind: int) -> None:
682
740
  if len(self._data.shape) != 2 or freq_ind >= self._data.shape[1]:
@@ -728,20 +786,22 @@ class Plot1D(Plot):
728
786
  },
729
787
  )
730
788
 
731
- def _plot_flag_data(self, time: ndarray, values: ndarray) -> None:
789
+ def _plot_flag_data(
790
+ self, time: ndarray, values: ndarray, color: str = "salmon"
791
+ ) -> None:
732
792
  self._ax.plot(
733
793
  time,
734
794
  values,
735
- color="salmon",
795
+ color=color,
736
796
  marker=".",
737
797
  lw=0,
738
798
  markersize=3,
739
799
  zorder=_get_zorder("flags"),
740
800
  )
741
801
 
742
- def _add_legend(self) -> None:
802
+ def _add_legend(self, name: str | tuple = ("Flagged data",)) -> None:
743
803
  self._ax.legend(
744
- ["Flagged data"],
804
+ name,
745
805
  markerscale=3,
746
806
  numpoints=1,
747
807
  frameon=False,
@@ -772,7 +832,10 @@ class Plot1D(Plot):
772
832
  custom_options = {
773
833
  "tb": {
774
834
  "color": "lightblue",
775
- }
835
+ },
836
+ "cloud_top_height_agl": {
837
+ "color": "steelblue",
838
+ },
776
839
  }
777
840
 
778
841
  variable_name = self.sub_plot.variable.name
@@ -1,6 +1,8 @@
1
1
  """Module for creating classification file."""
2
2
 
3
+ from enum import IntEnum
3
4
  from os import PathLike
5
+ from typing import NamedTuple
4
6
  from uuid import UUID
5
7
 
6
8
  import numpy as np
@@ -11,7 +13,7 @@ from cloudnetpy import output, utils
11
13
  from cloudnetpy.categorize import atmos_utils
12
14
  from cloudnetpy.datasource import DataSource
13
15
  from cloudnetpy.metadata import MetaData
14
- from cloudnetpy.products.product_tools import CategorizeBits
16
+ from cloudnetpy.products.product_tools import CategorizeBits, QualityBits
15
17
 
16
18
 
17
19
  def generate_classification(
@@ -40,89 +42,248 @@ def generate_classification(
40
42
 
41
43
  """
42
44
  uuid = utils.get_uuid(uuid)
43
- with DataSource(categorize_file) as product_container:
44
- categorize_bits = CategorizeBits(categorize_file)
45
+ categorize_bits = CategorizeBits(categorize_file)
46
+ with DataSource(categorize_file) as source:
45
47
  classification = _get_target_classification(categorize_bits)
46
- product_container.append_data(classification, "target_classification")
47
- status = _get_detection_status(categorize_bits)
48
- product_container.append_data(status, "detection_status")
49
- bases, tops = _get_cloud_base_and_top_heights(classification, product_container)
50
- product_container.append_data(bases, "cloud_base_height_amsl")
51
- product_container.append_data(tops, "cloud_top_height_amsl")
52
- product_container.append_data(
53
- bases - product_container.altitude,
48
+ source.append_data(classification, "target_classification")
49
+
50
+ detection_status = _get_detection_status(categorize_bits)
51
+ source.append_data(detection_status, "detection_status")
52
+
53
+ signal_source_status = _get_signal_source_status(categorize_bits)
54
+ source.append_data(signal_source_status, "signal_source_status")
55
+
56
+ att_status = _get_radar_attenuation_status(source, categorize_bits)
57
+ source.append_data(att_status, "radar_attenuation_status")
58
+
59
+ height = source.getvar("height")
60
+ bases, tops = _get_cloud_base_and_top_heights(classification, height)
61
+ source.append_data(bases, "cloud_base_height_amsl")
62
+ source.append_data(tops, "cloud_top_height_amsl")
63
+ source.append_data(
64
+ bases - source.altitude,
54
65
  "cloud_base_height_agl",
55
66
  )
56
- product_container.append_data(
57
- tops - product_container.altitude,
67
+ source.append_data(
68
+ tops - source.altitude,
58
69
  "cloud_top_height_agl",
59
70
  )
60
- date = product_container.get_date()
71
+
72
+ cloud_top_status = _get_cloud_top_height_status(source, tops, att_status)
73
+ source.append_data(cloud_top_status, "cloud_top_height_status")
74
+
75
+ date = source.get_date()
61
76
  attributes = output.add_time_attribute(CLASSIFICATION_ATTRIBUTES, date)
62
- output.update_attributes(product_container.data, attributes)
77
+ output.update_attributes(source.data, attributes)
63
78
  file_type = "classification"
64
- if "liquid_prob" in product_container.dataset.variables:
79
+ if "liquid_prob" in source.dataset.variables:
65
80
  file_type += "-voodoo"
66
81
  output.save_product_file(
67
- file_type,
68
- product_container,
69
- output_file,
70
- uuid,
82
+ file_type, source, output_file, uuid, copy_from_cat=("rain_detected",)
71
83
  )
72
84
  return uuid
73
85
 
74
86
 
87
+ class TopStatus(IntEnum):
88
+ RELIABLE = 0
89
+ MODERATE_ATT = 1
90
+ UNCORR_ATT = 2
91
+ SEVERE_ATT = 3
92
+ ABOVE_RANGE = 4
93
+
94
+
95
+ class AttStatus(IntEnum):
96
+ CLEAR = 0
97
+ NEGLIGIBLE = 1
98
+ SMALL = 2
99
+ MODERATE = 3
100
+ SEVERE = 4
101
+ UNCORRECTED = 5
102
+ UNDETECTED = 6
103
+
104
+
105
+ class SignalStatus(IntEnum):
106
+ CLEAR = 0
107
+ BOTH = 1
108
+ RADAR_ONLY = 2
109
+ LIDAR_ONLY = 3
110
+
111
+
112
+ class Target(IntEnum):
113
+ CLEAR = 0
114
+ DROPLET = 1
115
+ DRIZZLE_OR_RAIN = 2
116
+ DRIZZLE_OR_RAIN_AND_DROPLET = 3
117
+ ICE = 4
118
+ ICE_AND_SUPERCOOLED = 5
119
+ MELTING = 6
120
+ MELTING_AND_DROPLET = 7
121
+ AEROSOL = 8
122
+ INSECT = 9
123
+ INSECT_AND_AEROSOL = 10
124
+
125
+
126
+ class DetectionStatus(IntEnum):
127
+ CLEAR = 0
128
+ LIDAR_ONLY = 1
129
+ RADAR_UNCERTAIN_ATT = 2
130
+ RADAR_AND_LIDAR = 3
131
+ NO_RADAR_UNCERTAIN_ATT = 4
132
+ RADAR_ONLY = 5
133
+ NO_RADAR_KNOWN_ATT = 6
134
+ RADAR_ATT_CORRECTED = 7
135
+ CLUTTER = 8
136
+ MOLECULAR_SCATT = 9
137
+
138
+
139
+ class AttenuationClass(NamedTuple):
140
+ small: npt.NDArray
141
+ moderate: npt.NDArray
142
+ severe: npt.NDArray
143
+
144
+
145
+ def _get_cloud_top_height_status(
146
+ product_container: DataSource, tops: npt.NDArray, att_status: npt.NDArray
147
+ ) -> npt.NDArray:
148
+ height = product_container.dataset.variables["height"][:]
149
+ dist = np.abs(height[None, :] - tops[:, None])
150
+ height_inds = dist.argmin(axis=1)
151
+ att_at_top = att_status[np.arange(att_status.shape[0]), height_inds]
152
+ status = np.zeros(att_at_top.size, dtype=int)
153
+ status[att_at_top == AttStatus.MODERATE] = TopStatus.MODERATE_ATT
154
+ status[att_at_top == AttStatus.SEVERE] = TopStatus.SEVERE_ATT
155
+ status[att_at_top == AttStatus.UNCORRECTED] = TopStatus.UNCORR_ATT
156
+ status[tops >= height[-1]] = TopStatus.ABOVE_RANGE
157
+ return status
158
+
159
+
160
+ def _get_radar_attenuation_status(
161
+ data_source: DataSource, categorize_bits: CategorizeBits
162
+ ) -> npt.NDArray:
163
+ bits = categorize_bits.quality_bits
164
+ is_attenuated = _get_is_attenuated_mask(bits)
165
+ is_corrected = _get_is_corrected_mask(bits)
166
+ att = _get_attenuation_classes(data_source)
167
+ severity = np.zeros_like(att.small, dtype=int)
168
+ severity[bits.radar] = AttStatus.NEGLIGIBLE
169
+ severity[att.small & bits.radar] = AttStatus.SMALL
170
+ severity[att.moderate & bits.radar] = AttStatus.MODERATE
171
+ severity[att.severe & bits.radar] = AttStatus.SEVERE
172
+ severity[~is_corrected & is_attenuated & bits.radar] = AttStatus.UNCORRECTED
173
+ is_severe = severity == AttStatus.SEVERE
174
+ above_severe = utils.ffill(is_severe)
175
+ severity[above_severe & ~is_severe] = AttStatus.UNDETECTED
176
+ return severity
177
+
178
+
179
+ def _get_attenuation_classes(data_source: DataSource) -> AttenuationClass:
180
+ def _read_atten(key: str) -> npt.NDArray:
181
+ if key not in data_source.dataset.variables:
182
+ return np.zeros(data_source.time.shape)
183
+ data = data_source.getvar(key)
184
+ if isinstance(data, ma.MaskedArray):
185
+ return data.filled(0)
186
+ return data
187
+
188
+ liquid_atten = _read_atten("radar_liquid_atten")
189
+ rain_atten = _read_atten("radar_rain_atten")
190
+ melting_atten = _read_atten("radar_melting_atten")
191
+
192
+ if (
193
+ "lwp" not in data_source.dataset.variables
194
+ or data_source.getvar("radar_frequency") < 90
195
+ ):
196
+ lwp = np.zeros(data_source.time.shape)
197
+ else:
198
+ lwp_data = data_source.getvar("lwp")
199
+ lwp = lwp_data.filled(0) if isinstance(lwp_data, ma.MaskedArray) else lwp_data
200
+
201
+ total_atten = liquid_atten + rain_atten + melting_atten
202
+
203
+ threshold_moderate = 10 # dB
204
+ threshold_severe = 15 # dB
205
+ threshold_lwp = 1 # kg/m2
206
+
207
+ small = total_atten > 0
208
+ moderate = total_atten >= threshold_moderate
209
+ severe = (total_atten > threshold_severe) | (lwp[:, np.newaxis] > threshold_lwp)
210
+
211
+ return AttenuationClass(small=small, moderate=moderate, severe=severe)
212
+
213
+
75
214
  def _get_target_classification(
76
215
  categorize_bits: CategorizeBits,
77
216
  ) -> ma.MaskedArray:
78
217
  bits = categorize_bits.category_bits
79
218
  clutter = categorize_bits.quality_bits.clutter
80
219
  classification = ma.zeros(bits.freezing.shape, dtype=int)
81
- classification[bits.droplet & ~bits.falling] = 1 # Cloud droplets
82
- classification[~bits.droplet & bits.falling] = 2 # Drizzle or rain
83
- classification[bits.droplet & bits.falling] = 3 # Drizzle or rain and droplets
84
- classification[~bits.droplet & bits.falling & bits.freezing] = 4 # ice
85
- classification[bits.droplet & bits.falling & bits.freezing] = 5 # ice + supercooled
86
- classification[bits.melting] = 6 # melting layer
87
- classification[bits.melting & bits.droplet] = 7 # melting + droplets
88
- classification[bits.aerosol] = 8 # aerosols
89
- classification[bits.insect & ~clutter] = 9 # insects
90
- classification[bits.aerosol & bits.insect & ~clutter] = 10 # insects + aerosols
91
- classification[clutter & ~bits.aerosol] = 0
220
+ classification[bits.droplet & ~bits.falling] = Target.DROPLET
221
+ classification[~bits.droplet & bits.falling] = Target.DRIZZLE_OR_RAIN
222
+ classification[bits.droplet & bits.falling] = Target.DRIZZLE_OR_RAIN_AND_DROPLET
223
+ classification[~bits.droplet & bits.falling & bits.freezing] = Target.ICE
224
+ classification[bits.droplet & bits.falling & bits.freezing] = (
225
+ Target.ICE_AND_SUPERCOOLED
226
+ )
227
+ classification[bits.melting] = Target.MELTING
228
+ classification[bits.melting & bits.droplet] = Target.MELTING_AND_DROPLET
229
+ classification[bits.aerosol] = Target.AEROSOL
230
+ classification[bits.insect & ~clutter] = Target.INSECT
231
+ classification[bits.aerosol & bits.insect & ~clutter] = Target.INSECT_AND_AEROSOL
232
+ classification[clutter & ~bits.aerosol] = Target.CLEAR
92
233
  return classification
93
234
 
94
235
 
95
236
  def _get_detection_status(categorize_bits: CategorizeBits) -> npt.NDArray:
96
237
  bits = categorize_bits.quality_bits
238
+ is_attenuated = _get_is_attenuated_mask(bits)
239
+ is_corrected = _get_is_corrected_mask(bits)
97
240
 
98
- is_attenuated = (
99
- bits.attenuated_liquid | bits.attenuated_rain | bits.attenuated_melting
241
+ status = np.zeros(bits.radar.shape, dtype=int)
242
+ status[bits.lidar & ~bits.radar] = DetectionStatus.LIDAR_ONLY
243
+ status[bits.radar & bits.lidar] = DetectionStatus.RADAR_AND_LIDAR
244
+ status[~bits.radar & is_attenuated & ~is_corrected] = (
245
+ DetectionStatus.NO_RADAR_UNCERTAIN_ATT
246
+ )
247
+ status[bits.radar & ~bits.lidar & ~is_attenuated] = DetectionStatus.RADAR_ONLY
248
+ status[~bits.radar & is_attenuated & is_corrected] = (
249
+ DetectionStatus.NO_RADAR_KNOWN_ATT
250
+ )
251
+ status[bits.radar & is_corrected] = DetectionStatus.RADAR_ATT_CORRECTED
252
+ status[bits.radar & is_attenuated & ~is_corrected] = (
253
+ DetectionStatus.RADAR_UNCERTAIN_ATT
100
254
  )
101
- is_corrected = (
255
+ status[bits.clutter] = DetectionStatus.CLUTTER
256
+ status[bits.molecular & ~bits.radar] = DetectionStatus.MOLECULAR_SCATT
257
+ return status
258
+
259
+
260
+ def _get_is_corrected_mask(bits: QualityBits) -> npt.NDArray:
261
+ is_attenuated = _get_is_attenuated_mask(bits)
262
+ return (
102
263
  is_attenuated
103
264
  & (~bits.attenuated_liquid | bits.corrected_liquid)
104
265
  & (~bits.attenuated_rain | bits.corrected_rain)
105
266
  & (~bits.attenuated_melting | bits.corrected_melting)
106
267
  )
107
268
 
269
+
270
+ def _get_is_attenuated_mask(bits: QualityBits) -> npt.NDArray:
271
+ return bits.attenuated_liquid | bits.attenuated_rain | bits.attenuated_melting
272
+
273
+
274
+ def _get_signal_source_status(categorize_bits: CategorizeBits) -> npt.NDArray:
275
+ bits = categorize_bits.quality_bits
108
276
  status = np.zeros(bits.radar.shape, dtype=int)
109
- status[bits.lidar & ~bits.radar] = 1
110
- status[bits.radar & bits.lidar] = 3
111
- status[~bits.radar & is_attenuated & ~is_corrected] = 4
112
- status[bits.radar & ~bits.lidar & ~is_attenuated] = 5
113
- status[~bits.radar & is_attenuated & is_corrected] = 6
114
- status[bits.radar & is_corrected] = 7
115
- status[bits.radar & is_attenuated & ~is_corrected] = 2
116
- status[bits.clutter] = 8
117
- status[bits.molecular & ~bits.radar] = 9
277
+ status[bits.radar & bits.lidar] = SignalStatus.BOTH
278
+ status[bits.radar & ~bits.lidar] = SignalStatus.RADAR_ONLY
279
+ status[bits.lidar & ~bits.radar] = SignalStatus.LIDAR_ONLY
118
280
  return status
119
281
 
120
282
 
121
283
  def _get_cloud_base_and_top_heights(
122
284
  classification: npt.NDArray,
123
- product_container: DataSource,
285
+ height: npt.NDArray,
124
286
  ) -> tuple[npt.NDArray, npt.NDArray]:
125
- height = product_container.getvar("height")
126
287
  cloud_mask = _find_cloud_mask(classification)
127
288
  if not cloud_mask.any():
128
289
  return ma.masked_all(cloud_mask.shape[0]), ma.masked_all(cloud_mask.shape[0])
@@ -136,7 +297,12 @@ def _get_cloud_base_and_top_heights(
136
297
 
137
298
  def _find_cloud_mask(classification: npt.NDArray) -> npt.NDArray:
138
299
  cloud_mask = np.zeros(classification.shape, dtype=int)
139
- for value in [1, 3, 4, 5]:
300
+ for value in [
301
+ Target.DROPLET,
302
+ Target.DRIZZLE_OR_RAIN_AND_DROPLET,
303
+ Target.ICE,
304
+ Target.ICE_AND_SUPERCOOLED,
305
+ ]:
140
306
  cloud_mask[classification == value] = 1
141
307
  return cloud_mask
142
308
 
@@ -154,42 +320,92 @@ COMMENTS = {
154
320
  ),
155
321
  }
156
322
 
323
+
157
324
  DEFINITIONS = {
158
325
  "target_classification": utils.status_field_definition(
159
326
  {
160
- 0: "Clear sky.",
161
- 1: "Cloud liquid droplets only.",
162
- 2: "Drizzle or rain.",
163
- 3: "Drizzle or rain coexisting with cloud liquid droplets.",
164
- 4: "Ice particles.",
165
- 5: "Ice coexisting with supercooled liquid droplets.",
166
- 6: "Melting ice particles.",
167
- 7: "Melting ice particles coexisting with cloud liquid droplets.",
168
- 8: "Aerosol particles, no cloud or precipitation.",
169
- 9: "Insects, no cloud or precipitation.",
170
- 10: "Aerosol coexisting with insects, no cloud or precipitation.",
327
+ Target.CLEAR: """Clear sky.""",
328
+ Target.DROPLET: """Cloud liquid droplets only.""",
329
+ Target.DRIZZLE_OR_RAIN: """Drizzle or rain.""",
330
+ Target.DRIZZLE_OR_RAIN_AND_DROPLET: """Drizzle or rain
331
+ coexisting with cloud liquid droplets.""",
332
+ Target.ICE: """Ice particles.""",
333
+ Target.ICE_AND_SUPERCOOLED: """Ice coexisting with
334
+ supercooled liquid droplets.""",
335
+ Target.MELTING: """Melting ice particles.""",
336
+ Target.MELTING_AND_DROPLET: """Melting ice particles
337
+ coexisting with cloud liquid droplets.""",
338
+ Target.AEROSOL: """Aerosol particles, no cloud or precipitation.""",
339
+ Target.INSECT: """Insects, no cloud or precipitation.""",
340
+ Target.INSECT_AND_AEROSOL: """Aerosol coexisting
341
+ with insects, no cloud or precipitation.""",
171
342
  }
172
343
  ),
173
344
  "detection_status": utils.status_field_definition(
174
345
  {
175
- 0: """Clear sky.""",
176
- 1: """Lidar echo only.""",
177
- 2: """Radar echo but reflectivity may be unreliable as attenuation
178
- by rain, melting ice or liquid cloud has not been
179
- corrected.""",
180
- 3: """Good radar and lidar echos.""",
181
- 4: """No radar echo but rain or liquid cloud beneath mean that
182
- attenuation that would be experienced is unknown.""",
183
- 5: """Good radar echo only.""",
184
- 6: """No radar echo but known attenuation.""",
185
- 7: """Radar echo corrected for liquid, rain or melting
186
- attenuation.""",
187
- 8: """Radar ground clutter.""",
188
- 9: """Lidar clear-air molecular scattering.""",
346
+ DetectionStatus.CLEAR: """Clear sky.""",
347
+ DetectionStatus.LIDAR_ONLY: """Lidar echo only.""",
348
+ DetectionStatus.RADAR_UNCERTAIN_ATT: """
349
+ Radar echo but reflectivity may be unreliable as attenuation
350
+ by rain, melting ice or liquid cloud has not been
351
+ corrected.""",
352
+ DetectionStatus.RADAR_AND_LIDAR: """Good radar and lidar echos.""",
353
+ DetectionStatus.NO_RADAR_UNCERTAIN_ATT: """
354
+ No radar echo but rain or liquid cloud beneath mean that
355
+ attenuation that would be experienced is unknown.""",
356
+ DetectionStatus.RADAR_ONLY: """
357
+ Good radar echo only.""",
358
+ DetectionStatus.NO_RADAR_KNOWN_ATT: """
359
+ No radar echo but known attenuation.""",
360
+ DetectionStatus.RADAR_ATT_CORRECTED: """
361
+ Radar echo corrected for liquid, rain or melting attenuation.""",
362
+ DetectionStatus.CLUTTER: """
363
+ Radar ground clutter.""",
364
+ DetectionStatus.MOLECULAR_SCATT: """
365
+ Lidar clear-air molecular scattering.""",
366
+ }
367
+ ),
368
+ "cloud_top_height_status": utils.status_field_definition(
369
+ {
370
+ TopStatus.RELIABLE: """Reliable.""",
371
+ TopStatus.MODERATE_ATT: """Uncertain due to moderate
372
+ radar attenuation.""",
373
+ TopStatus.UNCORR_ATT: """Uncertain due to incomplete
374
+ radar attenuation correction.""",
375
+ TopStatus.SEVERE_ATT: """Likely erroneous due to
376
+ severe radar attenuation.""",
377
+ TopStatus.ABOVE_RANGE: """Cloud top above radar
378
+ measurement range.""",
379
+ }
380
+ ),
381
+ "signal_source_status": utils.status_field_definition(
382
+ {
383
+ SignalStatus.CLEAR: """No signal from radar or lidar.""",
384
+ SignalStatus.BOTH: """Signal from both radar and lidar.""",
385
+ SignalStatus.RADAR_ONLY: """Signal from radar only.""",
386
+ SignalStatus.LIDAR_ONLY: """Signal from lidar only.""",
387
+ }
388
+ ),
389
+ "radar_attenuation_status": utils.status_field_definition(
390
+ {
391
+ AttStatus.CLEAR: """No radar signal.""",
392
+ AttStatus.NEGLIGIBLE: """Radar signal,
393
+ negligible attenuation (corrected).""",
394
+ AttStatus.SMALL: """Radar signal,
395
+ small attenuation (corrected).""",
396
+ AttStatus.MODERATE: """Radar signal,
397
+ moderate attenuation (corrected).""",
398
+ AttStatus.SEVERE: """Radar signal,
399
+ severe attenuation (corrected).""",
400
+ AttStatus.UNCORRECTED: """Radar signal,
401
+ attenuation present but not corrected.""",
402
+ AttStatus.UNDETECTED: """No radar signal, cloud
403
+ may be undetected due to severe attenuation beneath.""",
189
404
  }
190
405
  ),
191
406
  }
192
407
 
408
+
193
409
  CLASSIFICATION_ATTRIBUTES = {
194
410
  "target_classification": MetaData(
195
411
  long_name="Target classification",
@@ -205,10 +421,23 @@ CLASSIFICATION_ATTRIBUTES = {
205
421
  units="1",
206
422
  dimensions=("time", "height"),
207
423
  ),
424
+ "signal_source_status": MetaData(
425
+ long_name="Signal source status",
426
+ units="1",
427
+ dimensions=("time", "height"),
428
+ definition=DEFINITIONS["signal_source_status"],
429
+ ),
430
+ "radar_attenuation_status": MetaData(
431
+ long_name="Radar attenuation status",
432
+ units="1",
433
+ dimensions=("time", "height"),
434
+ definition=DEFINITIONS["radar_attenuation_status"],
435
+ ),
208
436
  "cloud_top_height_amsl": MetaData(
209
437
  long_name="Height of cloud top above mean sea level",
210
438
  units="m",
211
439
  dimensions=("time",),
440
+ ancillary_variables="cloud_top_height_status",
212
441
  ),
213
442
  "cloud_base_height_amsl": MetaData(
214
443
  long_name="Height of cloud base above mean sea level",
@@ -219,10 +448,17 @@ CLASSIFICATION_ATTRIBUTES = {
219
448
  long_name="Height of cloud top above ground level",
220
449
  units="m",
221
450
  dimensions=("time",),
451
+ ancillary_variables="cloud_top_height_status",
222
452
  ),
223
453
  "cloud_base_height_agl": MetaData(
224
454
  long_name="Height of cloud base above ground level",
225
455
  units="m",
226
456
  dimensions=("time",),
227
457
  ),
458
+ "cloud_top_height_status": MetaData(
459
+ long_name="Cloud top height quality status",
460
+ units="1",
461
+ dimensions=("time",),
462
+ definition=DEFINITIONS["cloud_top_height_status"],
463
+ ),
228
464
  }
cloudnetpy/version.py CHANGED
@@ -1,4 +1,4 @@
1
1
  MAJOR = 1
2
- MINOR = 82
3
- PATCH = 3
2
+ MINOR = 83
3
+ PATCH = 0
4
4
  __version__ = f"{MAJOR}.{MINOR}.{PATCH}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudnetpy
3
- Version: 1.82.3
3
+ Version: 1.83.0
4
4
  Summary: Python package for Cloudnet processing
5
5
  Author: Simo Tukiainen
6
6
  License: MIT License
@@ -3,13 +3,13 @@ cloudnetpy/cli.py,sha256=kG48AI5wNK9MfxiUsQPiz56BQLgAZ3AyN0ehUO6MyuI,20892
3
3
  cloudnetpy/cloudnetarray.py,sha256=I_U1W2rEXw8lbLwg3XBrr_qHGPqhQG9z_ouvVEg7p24,4908
4
4
  cloudnetpy/concat_lib.py,sha256=u4UOjYzLnThaq-89iwA837OcOJpfmj_3RmRwBCFUh74,13218
5
5
  cloudnetpy/constants.py,sha256=YnoSzZm35NDooJfhlulSJBc7g0eSchT3yGytRaTaJEI,845
6
- cloudnetpy/datasource.py,sha256=HzvqTTHLCH9GniUsV_IWwyrvvONnFJh0tmBM61hsqxM,6364
6
+ cloudnetpy/datasource.py,sha256=EMJ4UHD8Z-JJ9Q82S7RgU1I2q4Z0RcBzBMKUAIwnZBI,6356
7
7
  cloudnetpy/exceptions.py,sha256=ZB3aUwjVRznR0CcZ5sZHrB0yz13URDf52Ksv7G7C7EA,1817
8
8
  cloudnetpy/metadata.py,sha256=CFpXmdEkVPzvLPv2xHIR-aMMQ-TR26KfESYw-98j7sk,7213
9
9
  cloudnetpy/output.py,sha256=0bybnILsgKHWIuw2GYkqTz2iMCJDZLUN25IQ9o_v3Cg,14968
10
10
  cloudnetpy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  cloudnetpy/utils.py,sha256=Qv60_vxknB3f2S3EFtyoD2CBY3N6mgDRObNp2u1oYUc,31806
12
- cloudnetpy/version.py,sha256=Qfq2h6P6mJkju5YGyo59dgA3ZumLmb6bJqqCtXszG0Q,72
12
+ cloudnetpy/version.py,sha256=eZHEAex9xrlQzhoqlO0MSLCFCdaJCCetQ0O35RSY7VM,72
13
13
  cloudnetpy/categorize/__init__.py,sha256=gtvzWr0IDRn2oA6yHBvinEhTGTuub-JkrOv93lBsgrE,61
14
14
  cloudnetpy/categorize/atmos_utils.py,sha256=uWc9TABVYPI0sn4H5Az9Jf6NVRaWyEKIi17f0pAJQxE,10679
15
15
  cloudnetpy/categorize/attenuation.py,sha256=Y_-fzmQTltWTqIZTulJhovC7a6ifpMcaAazDJcnMIOc,990
@@ -30,7 +30,7 @@ cloudnetpy/categorize/radar.py,sha256=2mTDa9BLxQeaORm-YPQ1lJyjAKew6NYzjtUvjpIvBY
30
30
  cloudnetpy/categorize/attenuations/__init__.py,sha256=kIyQEZ6VVO6jJOAndrt7jNU15pm0Cavh5GnDjFmIG1M,1040
31
31
  cloudnetpy/categorize/attenuations/gas_attenuation.py,sha256=emr-RCxQT0i2N8k6eBNhRsmsCBPHJzQsWJfjC4fVSTo,975
32
32
  cloudnetpy/categorize/attenuations/liquid_attenuation.py,sha256=bmqmPk_93J4njE16-VQ1bPI7oNSS8m9ACuUH7IErBs8,3069
33
- cloudnetpy/categorize/attenuations/melting_attenuation.py,sha256=AHmMMK7upxtps5fXF7Ca_ZF8fwpzaTGyGSwXDJq6q1k,2387
33
+ cloudnetpy/categorize/attenuations/melting_attenuation.py,sha256=zmpF7Gek4W9cF-5tGoXqi8yAUCFC5jzK6DqyVndXUnQ,2403
34
34
  cloudnetpy/categorize/attenuations/rain_attenuation.py,sha256=wJPyCiKWzsQDzMhqbA7mYwj9YRVcJIpXWhBnEYFy3uU,2843
35
35
  cloudnetpy/instruments/__init__.py,sha256=PEgrrQNoiOuN_ctYilmt4LV2QCLg1likPjJdWtuGlLs,528
36
36
  cloudnetpy/instruments/basta.py,sha256=N-kRgl5Vm52pXzr9umo4YsA0hn4zZCOa-0_zZTzhheY,4284
@@ -48,7 +48,7 @@ cloudnetpy/instruments/lufft.py,sha256=G6KeJOeltLUlGCHHEk8ns2K7WJ9ImAr25rSB2Jlta
48
48
  cloudnetpy/instruments/mira.py,sha256=XqmbytpeCJ2-hNugxdsXSBUDB8SAUc97_6lo5mHFG8E,11840
49
49
  cloudnetpy/instruments/mrr.py,sha256=z50VYLOBW2o7enU7FHZYNFQRW2goEQpeGe7-iCBRQtg,6020
50
50
  cloudnetpy/instruments/nc_lidar.py,sha256=PtZauDdI3bX3bv4gIVvV6N53e2Co-ehBL_tByHM9hj8,1713
51
- cloudnetpy/instruments/nc_radar.py,sha256=NKsy0mF2Tdem0lNIYgd3Kbe2dOE-38t4f_rosdhBcy8,7368
51
+ cloudnetpy/instruments/nc_radar.py,sha256=9npjF9xfMY5DkDPpesqAIhrmv1QqPlKB9J-cySI2UbU,7360
52
52
  cloudnetpy/instruments/pollyxt.py,sha256=IFq_RJrhgJ79OVyuo48PwYQK_zZ6VZFB_S5bEirRyzs,10566
53
53
  cloudnetpy/instruments/radiometrics.py,sha256=QKfnrZlQ0sFcFjmv1ShnCMTJQv64w4akjK-JAIY4gCg,16116
54
54
  cloudnetpy/instruments/rain_e_h3.py,sha256=fjv3SgeUNx9GisYqLrBnX9AjnO17VtouyoPh12VE9uo,5465
@@ -74,7 +74,7 @@ cloudnetpy/model_evaluation/products/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeu
74
74
  cloudnetpy/model_evaluation/products/advance_methods.py,sha256=-57z69YL8VVauypa0W1ZAZGHoNybna_CYcxQVORCV9c,8658
75
75
  cloudnetpy/model_evaluation/products/grid_methods.py,sha256=gAbRjM8jL0FBmH1t6U9Hvi0-f8GbmmF9Ctrcckluwh8,9110
76
76
  cloudnetpy/model_evaluation/products/model_products.py,sha256=vt104hKBM3KoLx-4r8E_LXKRR-FZU0X2gRS2TJ1sVII,7036
77
- cloudnetpy/model_evaluation/products/observation_products.py,sha256=ifNGmMBTI3G4EvnrXfS7vizbhv_hXlocLWs73BgeUOw,5578
77
+ cloudnetpy/model_evaluation/products/observation_products.py,sha256=4zmfSA8tantURcXAMh9vbjplWEEqj7wmhMt3hgdrAuI,5574
78
78
  cloudnetpy/model_evaluation/products/product_resampling.py,sha256=PO9PIJFeh2Nhl1bJ5Vg0_mB2xR5Xvj-qMjfD8n28KvY,3781
79
79
  cloudnetpy/model_evaluation/products/tools.py,sha256=pIF3cw3LDhdRfBTuknpzXdr7cxPuD0Reyb_lwLxR-v0,3075
80
80
  cloudnetpy/model_evaluation/statistics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -102,10 +102,10 @@ cloudnetpy/model_evaluation/tests/unit/test_plotting.py,sha256=5hkhtqX-JQ8-Yy6DA
102
102
  cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py,sha256=Ra3r4V0qbqkpDuaTYvEIbaasl0nZ5gmTLR4eGC0glBQ,9724
103
103
  cloudnetpy/model_evaluation/tests/unit/test_tools.py,sha256=Ia_VrLdV2NstX5gbx_3AZTOAlrgLAy_xFZ8fHYVX0xI,3817
104
104
  cloudnetpy/plotting/__init__.py,sha256=lg9Smn4BI0dVBgnDLC3JVJ4GmwoSnO-qoSd4ApvwV6Y,107
105
- cloudnetpy/plotting/plot_meta.py,sha256=qfyZJNis937uM-NJseer8i4FO7I_v5jhQPyFl5Uszi8,17390
106
- cloudnetpy/plotting/plotting.py,sha256=lkLSoeDK6lsE-4ln_XY8h8WwaZtzYm2inHrnHO_-HW0,38987
105
+ cloudnetpy/plotting/plot_meta.py,sha256=9d1OgPDysBDUUhncXdj-_EKmGdK7JutNBkzf8YV2lVg,18249
106
+ cloudnetpy/plotting/plotting.py,sha256=8NBqYC0RnBQarjFAmqTPYnFcfyRejfJWZ4TcR7EqVUI,41426
107
107
  cloudnetpy/products/__init__.py,sha256=cBJdJBYltz5ZTKDqnRo-0StytAZK8gE3RYxxriFA4ak,295
108
- cloudnetpy/products/classification.py,sha256=yg2XThN8ESr2hz0WvfHzAus5QUyLp7oHytR7nZJ0u18,8560
108
+ cloudnetpy/products/classification.py,sha256=6WxiGGJXqlAPgJ9hVNTKm3f7iGsqA7e3L3xqVxFFs7w,16894
109
109
  cloudnetpy/products/der.py,sha256=UXdAxmmwChVVWSI4QSGAXphfMnbymGRTtGdKWEvh-J4,13162
110
110
  cloudnetpy/products/drizzle.py,sha256=0h1N_WVjC2GgIkAN-4ydOwl7WJn3psxeqmPHfX8WHhQ,11935
111
111
  cloudnetpy/products/drizzle_error.py,sha256=QN98Io9UsBoEYxKBqfwoS88OGBiK5U5RYnVQjyTWHCI,6220
@@ -117,10 +117,10 @@ cloudnetpy/products/lwc.py,sha256=xsNiiG6dGKIkWaFk0xWTabc1bZ4ULf6SqcqHs7itAUk,19
117
117
  cloudnetpy/products/mie_lu_tables.nc,sha256=It4fYpqJXlqOgL8jeZ-PxGzP08PMrELIDVe55y9ob58,16637951
118
118
  cloudnetpy/products/mwr_tools.py,sha256=MMWnp68U7bv157-CPB2VeTQvaR6zl7sexbBT_kJ_pn8,6734
119
119
  cloudnetpy/products/product_tools.py,sha256=eyqIw_0KhlpmmYQE69RpGdRIAOW7JVPlEgkTBp2kdps,11302
120
- cloudnetpy-1.82.3.dist-info/licenses/LICENSE,sha256=wcZF72bdaoG9XugpyE95Juo7lBQOwLuTKBOhhtANZMM,1094
120
+ cloudnetpy-1.83.0.dist-info/licenses/LICENSE,sha256=wcZF72bdaoG9XugpyE95Juo7lBQOwLuTKBOhhtANZMM,1094
121
121
  docs/source/conf.py,sha256=IKiFWw6xhUd8NrCg0q7l596Ck1d61XWeVjIFHVSG9Og,1490
122
- cloudnetpy-1.82.3.dist-info/METADATA,sha256=7VGfHxhz7k9nHrMCq0VGFv-yGdgsI1d8v-TnTSWayD0,5836
123
- cloudnetpy-1.82.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
124
- cloudnetpy-1.82.3.dist-info/entry_points.txt,sha256=HhY7LwCFk4qFgDlXx_Fy983ZTd831WlhtdPIzV-Y3dY,51
125
- cloudnetpy-1.82.3.dist-info/top_level.txt,sha256=ibSPWRr6ojS1i11rtBFz2_gkIe68mggj7aeswYfaOo0,16
126
- cloudnetpy-1.82.3.dist-info/RECORD,,
122
+ cloudnetpy-1.83.0.dist-info/METADATA,sha256=wDkaRhm_pjpqqiX-6MteJV6IvZeKpZ8XiqaxCehqnV4,5836
123
+ cloudnetpy-1.83.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
124
+ cloudnetpy-1.83.0.dist-info/entry_points.txt,sha256=HhY7LwCFk4qFgDlXx_Fy983ZTd831WlhtdPIzV-Y3dY,51
125
+ cloudnetpy-1.83.0.dist-info/top_level.txt,sha256=ibSPWRr6ojS1i11rtBFz2_gkIe68mggj7aeswYfaOo0,16
126
+ cloudnetpy-1.83.0.dist-info/RECORD,,