cloudnetpy 1.49.9__py3-none-any.whl → 1.87.3__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 (116) hide show
  1. cloudnetpy/categorize/__init__.py +1 -2
  2. cloudnetpy/categorize/atmos_utils.py +297 -67
  3. cloudnetpy/categorize/attenuation.py +31 -0
  4. cloudnetpy/categorize/attenuations/__init__.py +37 -0
  5. cloudnetpy/categorize/attenuations/gas_attenuation.py +30 -0
  6. cloudnetpy/categorize/attenuations/liquid_attenuation.py +84 -0
  7. cloudnetpy/categorize/attenuations/melting_attenuation.py +78 -0
  8. cloudnetpy/categorize/attenuations/rain_attenuation.py +84 -0
  9. cloudnetpy/categorize/categorize.py +332 -156
  10. cloudnetpy/categorize/classify.py +127 -125
  11. cloudnetpy/categorize/containers.py +107 -76
  12. cloudnetpy/categorize/disdrometer.py +40 -0
  13. cloudnetpy/categorize/droplet.py +23 -21
  14. cloudnetpy/categorize/falling.py +53 -24
  15. cloudnetpy/categorize/freezing.py +25 -12
  16. cloudnetpy/categorize/insects.py +35 -23
  17. cloudnetpy/categorize/itu.py +243 -0
  18. cloudnetpy/categorize/lidar.py +36 -41
  19. cloudnetpy/categorize/melting.py +34 -26
  20. cloudnetpy/categorize/model.py +84 -37
  21. cloudnetpy/categorize/mwr.py +18 -14
  22. cloudnetpy/categorize/radar.py +215 -102
  23. cloudnetpy/cli.py +578 -0
  24. cloudnetpy/cloudnetarray.py +43 -89
  25. cloudnetpy/concat_lib.py +218 -78
  26. cloudnetpy/constants.py +28 -10
  27. cloudnetpy/datasource.py +61 -86
  28. cloudnetpy/exceptions.py +49 -20
  29. cloudnetpy/instruments/__init__.py +5 -0
  30. cloudnetpy/instruments/basta.py +29 -12
  31. cloudnetpy/instruments/bowtie.py +135 -0
  32. cloudnetpy/instruments/ceilo.py +138 -115
  33. cloudnetpy/instruments/ceilometer.py +164 -80
  34. cloudnetpy/instruments/cl61d.py +21 -5
  35. cloudnetpy/instruments/cloudnet_instrument.py +74 -36
  36. cloudnetpy/instruments/copernicus.py +108 -30
  37. cloudnetpy/instruments/da10.py +54 -0
  38. cloudnetpy/instruments/disdrometer/common.py +126 -223
  39. cloudnetpy/instruments/disdrometer/parsivel.py +453 -94
  40. cloudnetpy/instruments/disdrometer/thies.py +254 -87
  41. cloudnetpy/instruments/fd12p.py +201 -0
  42. cloudnetpy/instruments/galileo.py +65 -23
  43. cloudnetpy/instruments/hatpro.py +123 -49
  44. cloudnetpy/instruments/instruments.py +113 -1
  45. cloudnetpy/instruments/lufft.py +39 -17
  46. cloudnetpy/instruments/mira.py +268 -61
  47. cloudnetpy/instruments/mrr.py +187 -0
  48. cloudnetpy/instruments/nc_lidar.py +19 -8
  49. cloudnetpy/instruments/nc_radar.py +109 -55
  50. cloudnetpy/instruments/pollyxt.py +135 -51
  51. cloudnetpy/instruments/radiometrics.py +313 -59
  52. cloudnetpy/instruments/rain_e_h3.py +171 -0
  53. cloudnetpy/instruments/rpg.py +321 -189
  54. cloudnetpy/instruments/rpg_reader.py +74 -40
  55. cloudnetpy/instruments/toa5.py +49 -0
  56. cloudnetpy/instruments/vaisala.py +95 -343
  57. cloudnetpy/instruments/weather_station.py +774 -105
  58. cloudnetpy/metadata.py +90 -19
  59. cloudnetpy/model_evaluation/file_handler.py +55 -52
  60. cloudnetpy/model_evaluation/metadata.py +46 -20
  61. cloudnetpy/model_evaluation/model_metadata.py +1 -1
  62. cloudnetpy/model_evaluation/plotting/plot_tools.py +32 -37
  63. cloudnetpy/model_evaluation/plotting/plotting.py +327 -117
  64. cloudnetpy/model_evaluation/products/advance_methods.py +92 -83
  65. cloudnetpy/model_evaluation/products/grid_methods.py +88 -63
  66. cloudnetpy/model_evaluation/products/model_products.py +43 -35
  67. cloudnetpy/model_evaluation/products/observation_products.py +41 -35
  68. cloudnetpy/model_evaluation/products/product_resampling.py +17 -7
  69. cloudnetpy/model_evaluation/products/tools.py +29 -20
  70. cloudnetpy/model_evaluation/statistics/statistical_methods.py +30 -20
  71. cloudnetpy/model_evaluation/tests/e2e/conftest.py +3 -3
  72. cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +9 -5
  73. cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +15 -14
  74. cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +9 -5
  75. cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +15 -14
  76. cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +9 -5
  77. cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +15 -14
  78. cloudnetpy/model_evaluation/tests/unit/conftest.py +42 -41
  79. cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +41 -48
  80. cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +216 -194
  81. cloudnetpy/model_evaluation/tests/unit/test_model_products.py +23 -21
  82. cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +37 -38
  83. cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +43 -40
  84. cloudnetpy/model_evaluation/tests/unit/test_plotting.py +30 -36
  85. cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +68 -31
  86. cloudnetpy/model_evaluation/tests/unit/test_tools.py +33 -26
  87. cloudnetpy/model_evaluation/utils.py +2 -1
  88. cloudnetpy/output.py +170 -111
  89. cloudnetpy/plotting/__init__.py +2 -1
  90. cloudnetpy/plotting/plot_meta.py +562 -822
  91. cloudnetpy/plotting/plotting.py +1142 -704
  92. cloudnetpy/products/__init__.py +1 -0
  93. cloudnetpy/products/classification.py +370 -88
  94. cloudnetpy/products/der.py +85 -55
  95. cloudnetpy/products/drizzle.py +77 -34
  96. cloudnetpy/products/drizzle_error.py +15 -11
  97. cloudnetpy/products/drizzle_tools.py +79 -59
  98. cloudnetpy/products/epsilon.py +211 -0
  99. cloudnetpy/products/ier.py +27 -50
  100. cloudnetpy/products/iwc.py +55 -48
  101. cloudnetpy/products/lwc.py +96 -70
  102. cloudnetpy/products/mwr_tools.py +186 -0
  103. cloudnetpy/products/product_tools.py +170 -128
  104. cloudnetpy/utils.py +455 -240
  105. cloudnetpy/version.py +2 -2
  106. {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/METADATA +44 -40
  107. cloudnetpy-1.87.3.dist-info/RECORD +127 -0
  108. {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/WHEEL +1 -1
  109. cloudnetpy-1.87.3.dist-info/entry_points.txt +2 -0
  110. docs/source/conf.py +2 -2
  111. cloudnetpy/categorize/atmos.py +0 -361
  112. cloudnetpy/products/mwr_multi.py +0 -68
  113. cloudnetpy/products/mwr_single.py +0 -75
  114. cloudnetpy-1.49.9.dist-info/RECORD +0 -112
  115. {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info/licenses}/LICENSE +0 -0
  116. {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/top_level.txt +0 -0
@@ -4,3 +4,4 @@ from .drizzle import generate_drizzle
4
4
  from .ier import generate_ier
5
5
  from .iwc import generate_iwc
6
6
  from .lwc import generate_lwc
7
+ from .mwr_tools import generate_mwr_lhumpro, generate_mwr_multi, generate_mwr_single
@@ -1,17 +1,27 @@
1
1
  """Module for creating classification file."""
2
+
3
+ from enum import IntEnum
4
+ from os import PathLike
5
+ from typing import NamedTuple
6
+ from uuid import UUID
7
+
2
8
  import numpy as np
9
+ import numpy.typing as npt
3
10
  from numpy import ma
4
11
 
5
- from cloudnetpy import output
6
- from cloudnetpy.categorize import atmos
12
+ from cloudnetpy import output, utils
13
+ from cloudnetpy.categorize import atmos_utils
14
+ from cloudnetpy.constants import M_S_TO_MM_H
7
15
  from cloudnetpy.datasource import DataSource
8
16
  from cloudnetpy.metadata import MetaData
9
- from cloudnetpy.products.product_tools import CategorizeBits
17
+ from cloudnetpy.products.product_tools import CategorizeBits, QualityBits
10
18
 
11
19
 
12
20
  def generate_classification(
13
- categorize_file: str, output_file: str, uuid: str | None = None
14
- ) -> str:
21
+ categorize_file: str | PathLike,
22
+ output_file: str | PathLike,
23
+ uuid: str | UUID | None = None,
24
+ ) -> UUID:
15
25
  """Generates Cloudnet classification product.
16
26
 
17
27
  This function reads the initial classification masks from a
@@ -32,87 +42,280 @@ def generate_classification(
32
42
  >>> generate_classification('categorize.nc', 'classification.nc')
33
43
 
34
44
  """
35
- with DataSource(categorize_file) as product_container:
36
- categorize_bits = CategorizeBits(categorize_file)
45
+ uuid = utils.get_uuid(uuid)
46
+ categorize_bits = CategorizeBits(categorize_file)
47
+ with DataSource(categorize_file) as source:
37
48
  classification = _get_target_classification(categorize_bits)
38
- product_container.append_data(classification, "target_classification")
39
- status = _get_detection_status(categorize_bits)
40
- product_container.append_data(status, "detection_status")
41
- bases, tops = _get_cloud_base_and_top_heights(classification, product_container)
42
- product_container.append_data(bases, "cloud_base_height_amsl")
43
- product_container.append_data(tops, "cloud_top_height_amsl")
44
- product_container.append_data(
45
- bases - product_container.altitude, "cloud_base_height_agl"
49
+ source.append_data(classification, "target_classification")
50
+
51
+ detection_status = _get_detection_status(categorize_bits)
52
+ source.append_data(detection_status, "detection_status")
53
+
54
+ signal_source_status = _get_signal_source_status(categorize_bits)
55
+ source.append_data(signal_source_status, "signal_source_status")
56
+
57
+ att_status = _get_radar_attenuation_status(source, categorize_bits)
58
+ source.append_data(att_status, "radar_attenuation_status")
59
+
60
+ height = source.getvar("height")
61
+ bases, tops = _get_cloud_base_and_top_heights(classification, height)
62
+ source.append_data(bases, "cloud_base_height_amsl")
63
+ source.append_data(tops, "cloud_top_height_amsl")
64
+ source.append_data(
65
+ bases - source.altitude,
66
+ "cloud_base_height_agl",
46
67
  )
47
- product_container.append_data(
48
- tops - product_container.altitude, "cloud_top_height_agl"
68
+ source.append_data(
69
+ tops - source.altitude,
70
+ "cloud_top_height_agl",
49
71
  )
50
- date = product_container.get_date()
72
+
73
+ cloud_top_status = _get_cloud_top_height_status(source, tops, att_status)
74
+ source.append_data(cloud_top_status, "cloud_top_height_status")
75
+
76
+ date = source.get_date()
51
77
  attributes = output.add_time_attribute(CLASSIFICATION_ATTRIBUTES, date)
52
- output.update_attributes(product_container.data, attributes)
53
- uuid = output.save_product_file(
54
- "classification", product_container, output_file, uuid
78
+ output.update_attributes(source.data, attributes)
79
+ file_type = "classification"
80
+ if "liquid_prob" in source.dataset.variables:
81
+ file_type += "-voodoo"
82
+ output.save_product_file(
83
+ file_type, source, output_file, uuid, copy_from_cat=("rain_detected",)
55
84
  )
56
- return uuid
85
+ return uuid
86
+
87
+
88
+ class TopStatus(IntEnum):
89
+ RELIABLE = 0
90
+ MODERATE_ATT = 1
91
+ UNCORR_ATT = 2
92
+ SEVERE_ATT = 3
93
+ ABOVE_RANGE = 4
94
+
95
+
96
+ class AttStatus(IntEnum):
97
+ CLEAR = 0
98
+ NEGLIGIBLE = 1
99
+ SMALL = 2
100
+ MODERATE = 3
101
+ SEVERE = 4
102
+ UNCORRECTED = 5
103
+ UNDETECTED = 6
104
+
105
+
106
+ class SignalStatus(IntEnum):
107
+ CLEAR = 0
108
+ BOTH = 1
109
+ RADAR_ONLY = 2
110
+ LIDAR_ONLY = 3
111
+
112
+
113
+ class Target(IntEnum):
114
+ CLEAR = 0
115
+ DROPLET = 1
116
+ DRIZZLE_OR_RAIN = 2
117
+ DRIZZLE_OR_RAIN_AND_DROPLET = 3
118
+ ICE = 4
119
+ ICE_AND_SUPERCOOLED = 5
120
+ MELTING = 6
121
+ MELTING_AND_DROPLET = 7
122
+ AEROSOL = 8
123
+ INSECT = 9
124
+ INSECT_AND_AEROSOL = 10
125
+
126
+
127
+ class DetectionStatus(IntEnum):
128
+ CLEAR = 0
129
+ LIDAR_ONLY = 1
130
+ RADAR_UNCERTAIN_ATT = 2
131
+ RADAR_AND_LIDAR = 3
132
+ NO_RADAR_UNCERTAIN_ATT = 4
133
+ RADAR_ONLY = 5
134
+ NO_RADAR_KNOWN_ATT = 6
135
+ RADAR_ATT_CORRECTED = 7
136
+ CLUTTER = 8
137
+ MOLECULAR_SCATT = 9
138
+
139
+
140
+ class AttenuationClass(NamedTuple):
141
+ small: npt.NDArray
142
+ moderate: npt.NDArray
143
+ severe: npt.NDArray
144
+
145
+
146
+ def _get_cloud_top_height_status(
147
+ product_container: DataSource, tops: npt.NDArray, att_status: npt.NDArray
148
+ ) -> npt.NDArray:
149
+ height = product_container.dataset.variables["height"][:]
150
+ dist = np.abs(height[None, :] - tops[:, None])
151
+ height_inds = dist.argmin(axis=1)
152
+ att_at_top = att_status[np.arange(att_status.shape[0]), height_inds]
153
+ status = np.zeros(att_at_top.size, dtype=int)
154
+ status[att_at_top == AttStatus.MODERATE] = TopStatus.MODERATE_ATT
155
+ status[att_at_top == AttStatus.SEVERE] = TopStatus.SEVERE_ATT
156
+ status[att_at_top == AttStatus.UNCORRECTED] = TopStatus.UNCORR_ATT
157
+ status[tops >= height[-1]] = TopStatus.ABOVE_RANGE
158
+ return status
159
+
160
+
161
+ def _get_radar_attenuation_status(
162
+ data_source: DataSource, categorize_bits: CategorizeBits
163
+ ) -> npt.NDArray:
164
+ bits = categorize_bits.quality_bits
165
+ is_attenuated = _get_is_attenuated_mask(bits)
166
+ is_corrected = _get_is_corrected_mask(bits)
167
+ att = _get_attenuation_classes(data_source)
168
+ severity = np.zeros_like(att.small, dtype=int)
169
+ severity[bits.radar] = AttStatus.NEGLIGIBLE
170
+ severity[att.small & bits.radar] = AttStatus.SMALL
171
+ severity[att.moderate & bits.radar] = AttStatus.MODERATE
172
+ severity[att.severe & bits.radar] = AttStatus.SEVERE
173
+ severity[~is_corrected & is_attenuated & bits.radar] = AttStatus.UNCORRECTED
174
+ is_severe = severity == AttStatus.SEVERE
175
+ above_severe = utils.ffill(is_severe)
176
+ severity[above_severe & ~is_severe] = AttStatus.UNDETECTED
177
+ return severity
178
+
179
+
180
+ def _get_attenuation_classes(data_source: DataSource) -> AttenuationClass:
181
+ def _read_atten(key: str) -> npt.NDArray:
182
+ if key not in data_source.dataset.variables:
183
+ return np.zeros(data_source.time.shape)
184
+ data = data_source.getvar(key)
185
+ if isinstance(data, ma.MaskedArray):
186
+ return data.filled(0)
187
+ return data
188
+
189
+ liquid_atten = _read_atten("radar_liquid_atten")
190
+ rain_atten = _read_atten("radar_rain_atten")
191
+ melting_atten = _read_atten("radar_melting_atten")
192
+
193
+ not_w_band = data_source.getvar("radar_frequency") < 90
194
+
195
+ if "lwp" not in data_source.dataset.variables or not_w_band:
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
+ if "rainfall_rate" not in data_source.dataset.variables or not_w_band:
202
+ rain_rate = np.zeros(data_source.time.shape)
203
+ else:
204
+ rain_data = data_source.getvar("rainfall_rate") * M_S_TO_MM_H
205
+ rain_rate = (
206
+ rain_data.filled(0) if isinstance(rain_data, ma.MaskedArray) else rain_data
207
+ )
208
+
209
+ total_atten = liquid_atten + rain_atten + melting_atten
210
+
211
+ threshold_moderate = 10 # dB
212
+ threshold_severe = 15 # dB
213
+ threshold_lwp = 1 # kg/m2
214
+ threshold_rain = 3 # mm/h
215
+
216
+ small = total_atten > 0
217
+ moderate = total_atten >= threshold_moderate
218
+ severe = (
219
+ (total_atten > threshold_severe)
220
+ | (lwp[:, np.newaxis] > threshold_lwp)
221
+ | (rain_rate[:, np.newaxis] > threshold_rain)
222
+ )
223
+
224
+ return AttenuationClass(small=small, moderate=moderate, severe=severe)
57
225
 
58
226
 
59
227
  def _get_target_classification(
60
228
  categorize_bits: CategorizeBits,
61
229
  ) -> ma.MaskedArray:
62
230
  bits = categorize_bits.category_bits
63
- clutter = categorize_bits.quality_bits["clutter"]
64
- classification = ma.zeros(bits["cold"].shape, dtype=int)
65
- classification[bits["droplet"] & ~bits["falling"]] = 1 # Cloud droplets
66
- classification[~bits["droplet"] & bits["falling"]] = 2 # Drizzle or rain
67
- classification[
68
- bits["droplet"] & bits["falling"]
69
- ] = 3 # Drizzle or rain and droplets
70
- classification[~bits["droplet"] & bits["falling"] & bits["cold"]] = 4 # ice
71
- classification[
72
- bits["droplet"] & bits["falling"] & bits["cold"]
73
- ] = 5 # ice + supercooled
74
- classification[bits["melting"]] = 6 # melting layer
75
- classification[bits["melting"] & bits["droplet"]] = 7 # melting + droplets
76
- classification[bits["aerosol"]] = 8 # aerosols
77
- classification[bits["insect"] & ~clutter] = 9 # insects
78
- classification[
79
- bits["aerosol"] & bits["insect"] & ~clutter
80
- ] = 10 # insects + aerosols
81
- classification[clutter & ~bits["aerosol"]] = 0
231
+ clutter = categorize_bits.quality_bits.clutter
232
+ classification = ma.zeros(bits.freezing.shape, dtype=int)
233
+ classification[bits.droplet & ~bits.falling] = Target.DROPLET
234
+ classification[~bits.droplet & bits.falling] = Target.DRIZZLE_OR_RAIN
235
+ classification[bits.droplet & bits.falling] = Target.DRIZZLE_OR_RAIN_AND_DROPLET
236
+ classification[~bits.droplet & bits.falling & bits.freezing] = Target.ICE
237
+ classification[bits.droplet & bits.falling & bits.freezing] = (
238
+ Target.ICE_AND_SUPERCOOLED
239
+ )
240
+ classification[bits.melting] = Target.MELTING
241
+ classification[bits.melting & bits.droplet] = Target.MELTING_AND_DROPLET
242
+ classification[bits.aerosol] = Target.AEROSOL
243
+ classification[bits.insect & ~clutter] = Target.INSECT
244
+ classification[bits.aerosol & bits.insect & ~clutter] = Target.INSECT_AND_AEROSOL
245
+ classification[clutter & ~bits.aerosol] = Target.CLEAR
82
246
  return classification
83
247
 
84
248
 
85
- def _get_detection_status(categorize_bits: CategorizeBits) -> np.ndarray:
249
+ def _get_detection_status(categorize_bits: CategorizeBits) -> npt.NDArray:
250
+ bits = categorize_bits.quality_bits
251
+ is_attenuated = _get_is_attenuated_mask(bits)
252
+ is_corrected = _get_is_corrected_mask(bits)
253
+
254
+ status = np.zeros(bits.radar.shape, dtype=int)
255
+ status[bits.lidar & ~bits.radar] = DetectionStatus.LIDAR_ONLY
256
+ status[bits.radar & bits.lidar] = DetectionStatus.RADAR_AND_LIDAR
257
+ status[~bits.radar & is_attenuated & ~is_corrected] = (
258
+ DetectionStatus.NO_RADAR_UNCERTAIN_ATT
259
+ )
260
+ status[bits.radar & ~bits.lidar & ~is_attenuated] = DetectionStatus.RADAR_ONLY
261
+ status[~bits.radar & is_attenuated & is_corrected] = (
262
+ DetectionStatus.NO_RADAR_KNOWN_ATT
263
+ )
264
+ status[bits.radar & is_corrected] = DetectionStatus.RADAR_ATT_CORRECTED
265
+ status[bits.radar & is_attenuated & ~is_corrected] = (
266
+ DetectionStatus.RADAR_UNCERTAIN_ATT
267
+ )
268
+ status[bits.clutter] = DetectionStatus.CLUTTER
269
+ status[bits.molecular & ~bits.radar] = DetectionStatus.MOLECULAR_SCATT
270
+ return status
271
+
272
+
273
+ def _get_is_corrected_mask(bits: QualityBits) -> npt.NDArray:
274
+ is_attenuated = _get_is_attenuated_mask(bits)
275
+ return (
276
+ is_attenuated
277
+ & (~bits.attenuated_liquid | bits.corrected_liquid)
278
+ & (~bits.attenuated_rain | bits.corrected_rain)
279
+ & (~bits.attenuated_melting | bits.corrected_melting)
280
+ )
281
+
282
+
283
+ def _get_is_attenuated_mask(bits: QualityBits) -> npt.NDArray:
284
+ return bits.attenuated_liquid | bits.attenuated_rain | bits.attenuated_melting
285
+
286
+
287
+ def _get_signal_source_status(categorize_bits: CategorizeBits) -> npt.NDArray:
86
288
  bits = categorize_bits.quality_bits
87
- status = np.zeros(bits["radar"].shape, dtype=int)
88
- status[bits["lidar"] & ~bits["radar"]] = 1
89
- status[bits["radar"] & bits["lidar"]] = 3
90
- status[~bits["radar"] & bits["attenuated"] & ~bits["corrected"]] = 4
91
- status[bits["radar"] & ~bits["lidar"] & ~bits["attenuated"]] = 5
92
- status[~bits["radar"] & bits["attenuated"] & bits["corrected"]] = 6
93
- status[bits["radar"] & bits["corrected"]] = 7
94
- status[bits["radar"] & bits["attenuated"] & ~bits["corrected"]] = 2
95
- status[bits["clutter"]] = 8
96
- status[bits["molecular"] & ~bits["radar"]] = 9
289
+ status = np.zeros(bits.radar.shape, dtype=int)
290
+ status[bits.radar & bits.lidar] = SignalStatus.BOTH
291
+ status[bits.radar & ~bits.lidar] = SignalStatus.RADAR_ONLY
292
+ status[bits.lidar & ~bits.radar] = SignalStatus.LIDAR_ONLY
97
293
  return status
98
294
 
99
295
 
100
296
  def _get_cloud_base_and_top_heights(
101
- classification: np.ndarray, product_container: DataSource
102
- ) -> tuple[np.ndarray, np.ndarray]:
103
- height = product_container.getvar("height")
297
+ classification: npt.NDArray,
298
+ height: npt.NDArray,
299
+ ) -> tuple[npt.NDArray, npt.NDArray]:
104
300
  cloud_mask = _find_cloud_mask(classification)
105
301
  if not cloud_mask.any():
106
302
  return ma.masked_all(cloud_mask.shape[0]), ma.masked_all(cloud_mask.shape[0])
107
- lowest_bases = atmos.find_lowest_cloud_bases(cloud_mask, height)
108
- highest_tops = atmos.find_highest_cloud_tops(cloud_mask, height)
109
- assert (highest_tops - lowest_bases >= 0).all()
303
+ lowest_bases = atmos_utils.find_lowest_cloud_bases(cloud_mask, height)
304
+ highest_tops = atmos_utils.find_highest_cloud_tops(cloud_mask, height)
305
+ if not (highest_tops - lowest_bases >= 0).all():
306
+ msg = "Cloud base higher than cloud top!"
307
+ raise ValueError(msg)
110
308
  return lowest_bases, highest_tops
111
309
 
112
310
 
113
- def _find_cloud_mask(classification: np.ndarray) -> np.ndarray:
311
+ def _find_cloud_mask(classification: npt.NDArray) -> npt.NDArray:
114
312
  cloud_mask = np.zeros(classification.shape, dtype=int)
115
- for value in [1, 3, 4, 5]:
313
+ for value in [
314
+ Target.DROPLET,
315
+ Target.DRIZZLE_OR_RAIN_AND_DROPLET,
316
+ Target.ICE,
317
+ Target.ICE_AND_SUPERCOOLED,
318
+ ]:
116
319
  cloud_mask[classification == value] = 1
117
320
  return cloud_mask
118
321
 
@@ -130,66 +333,145 @@ COMMENTS = {
130
333
  ),
131
334
  }
132
335
 
336
+
133
337
  DEFINITIONS = {
134
- "target_classification": (
135
- "\n"
136
- "Value 0: Clear sky.\n"
137
- "Value 1: Cloud liquid droplets only.\n"
138
- "Value 2: Drizzle or rain.\n"
139
- "Value 3: Drizzle or rain coexisting with cloud liquid droplets.\n"
140
- "Value 4: Ice particles.\n"
141
- "Value 5: Ice coexisting with supercooled liquid droplets.\n"
142
- "Value 6: Melting ice particles.\n"
143
- "Value 7: Melting ice particles coexisting with cloud liquid droplets.\n"
144
- "Value 8: Aerosol particles, no cloud or precipitation.\n"
145
- "Value 9: Insects, no cloud or precipitation.\n"
146
- "Value 10: Aerosol coexisting with insects, no cloud or precipitation."
338
+ "target_classification": utils.status_field_definition(
339
+ {
340
+ Target.CLEAR: """Clear sky.""",
341
+ Target.DROPLET: """Cloud liquid droplets only.""",
342
+ Target.DRIZZLE_OR_RAIN: """Drizzle or rain.""",
343
+ Target.DRIZZLE_OR_RAIN_AND_DROPLET: """Drizzle or rain
344
+ coexisting with cloud liquid droplets.""",
345
+ Target.ICE: """Ice particles.""",
346
+ Target.ICE_AND_SUPERCOOLED: """Ice coexisting with
347
+ supercooled liquid droplets.""",
348
+ Target.MELTING: """Melting ice particles.""",
349
+ Target.MELTING_AND_DROPLET: """Melting ice particles
350
+ coexisting with cloud liquid droplets.""",
351
+ Target.AEROSOL: """Aerosol particles, no cloud or precipitation.""",
352
+ Target.INSECT: """Insects, no cloud or precipitation.""",
353
+ Target.INSECT_AND_AEROSOL: """Aerosol coexisting
354
+ with insects, no cloud or precipitation.""",
355
+ }
147
356
  ),
148
- "detection_status": (
149
- "\n"
150
- "Value 0: Clear sky.\n"
151
- "Value 1: Lidar echo only.\n"
152
- "Value 2: Radar echo but reflectivity may be unreliable as attenuation\n"
153
- " by rain, melting ice or liquid cloud has not been corrected.\n"
154
- "Value 3: Good radar and lidar echos.\n"
155
- "Value 4: No radar echo but rain or liquid cloud beneath mean that\n"
156
- " attenuation that would be experienced is unknown.\n"
157
- "Value 5: Good radar echo only.\n"
158
- "Value 6: No radar echo but known attenuation.\n"
159
- "Value 7: Radar echo corrected for liquid attenuation using microwave\n"
160
- " radiometer data."
161
- "Value 8: Radar ground clutter.\n"
162
- "Value 9: Lidar clear-air molecular scattering."
357
+ "detection_status": utils.status_field_definition(
358
+ {
359
+ DetectionStatus.CLEAR: """Clear sky.""",
360
+ DetectionStatus.LIDAR_ONLY: """Lidar echo only.""",
361
+ DetectionStatus.RADAR_UNCERTAIN_ATT: """
362
+ Radar echo but reflectivity may be unreliable as attenuation
363
+ by rain, melting ice or liquid cloud has not been
364
+ corrected.""",
365
+ DetectionStatus.RADAR_AND_LIDAR: """Good radar and lidar echos.""",
366
+ DetectionStatus.NO_RADAR_UNCERTAIN_ATT: """
367
+ No radar echo but rain or liquid cloud beneath mean that
368
+ attenuation that would be experienced is unknown.""",
369
+ DetectionStatus.RADAR_ONLY: """
370
+ Good radar echo only.""",
371
+ DetectionStatus.NO_RADAR_KNOWN_ATT: """
372
+ No radar echo but known attenuation.""",
373
+ DetectionStatus.RADAR_ATT_CORRECTED: """
374
+ Radar echo corrected for liquid, rain or melting attenuation.""",
375
+ DetectionStatus.CLUTTER: """
376
+ Radar ground clutter.""",
377
+ DetectionStatus.MOLECULAR_SCATT: """
378
+ Lidar clear-air molecular scattering.""",
379
+ }
380
+ ),
381
+ "cloud_top_height_status": utils.status_field_definition(
382
+ {
383
+ TopStatus.RELIABLE: """Reliable.""",
384
+ TopStatus.MODERATE_ATT: """Uncertain due to moderate
385
+ radar attenuation.""",
386
+ TopStatus.UNCORR_ATT: """Uncertain due to incomplete
387
+ radar attenuation correction.""",
388
+ TopStatus.SEVERE_ATT: """Likely erroneous due to
389
+ severe radar attenuation.""",
390
+ TopStatus.ABOVE_RANGE: """Cloud top above radar
391
+ measurement range.""",
392
+ }
393
+ ),
394
+ "signal_source_status": utils.status_field_definition(
395
+ {
396
+ SignalStatus.CLEAR: """No signal from radar or lidar.""",
397
+ SignalStatus.BOTH: """Signal from both radar and lidar.""",
398
+ SignalStatus.RADAR_ONLY: """Signal from radar only.""",
399
+ SignalStatus.LIDAR_ONLY: """Signal from lidar only.""",
400
+ }
401
+ ),
402
+ "radar_attenuation_status": utils.status_field_definition(
403
+ {
404
+ AttStatus.CLEAR: """No radar signal.""",
405
+ AttStatus.NEGLIGIBLE: """Radar signal,
406
+ negligible attenuation (corrected).""",
407
+ AttStatus.SMALL: """Radar signal,
408
+ small attenuation (corrected).""",
409
+ AttStatus.MODERATE: """Radar signal,
410
+ moderate attenuation (corrected).""",
411
+ AttStatus.SEVERE: """Radar signal,
412
+ severe attenuation (corrected).""",
413
+ AttStatus.UNCORRECTED: """Radar signal,
414
+ attenuation present but not corrected.""",
415
+ AttStatus.UNDETECTED: """No radar signal, cloud
416
+ may be undetected due to severe attenuation beneath.""",
417
+ }
163
418
  ),
164
419
  }
165
420
 
421
+
166
422
  CLASSIFICATION_ATTRIBUTES = {
167
423
  "target_classification": MetaData(
168
424
  long_name="Target classification",
169
425
  comment=COMMENTS["target_classification"],
170
426
  definition=DEFINITIONS["target_classification"],
171
427
  units="1",
428
+ dimensions=("time", "height"),
172
429
  ),
173
430
  "detection_status": MetaData(
174
431
  long_name="Radar and lidar detection status",
175
432
  comment=COMMENTS["detection_status"],
176
433
  definition=DEFINITIONS["detection_status"],
177
434
  units="1",
435
+ dimensions=("time", "height"),
436
+ ),
437
+ "signal_source_status": MetaData(
438
+ long_name="Signal source status",
439
+ units="1",
440
+ dimensions=("time", "height"),
441
+ definition=DEFINITIONS["signal_source_status"],
442
+ ),
443
+ "radar_attenuation_status": MetaData(
444
+ long_name="Radar attenuation status",
445
+ units="1",
446
+ dimensions=("time", "height"),
447
+ definition=DEFINITIONS["radar_attenuation_status"],
178
448
  ),
179
449
  "cloud_top_height_amsl": MetaData(
180
450
  long_name="Height of cloud top above mean sea level",
181
451
  units="m",
452
+ dimensions=("time",),
453
+ ancillary_variables="cloud_top_height_status",
182
454
  ),
183
455
  "cloud_base_height_amsl": MetaData(
184
456
  long_name="Height of cloud base above mean sea level",
185
457
  units="m",
458
+ dimensions=("time",),
186
459
  ),
187
460
  "cloud_top_height_agl": MetaData(
188
461
  long_name="Height of cloud top above ground level",
189
462
  units="m",
463
+ dimensions=("time",),
464
+ ancillary_variables="cloud_top_height_status",
190
465
  ),
191
466
  "cloud_base_height_agl": MetaData(
192
467
  long_name="Height of cloud base above ground level",
193
468
  units="m",
469
+ dimensions=("time",),
470
+ ),
471
+ "cloud_top_height_status": MetaData(
472
+ long_name="Cloud top height quality status",
473
+ units="1",
474
+ dimensions=("time",),
475
+ definition=DEFINITIONS["cloud_top_height_status"],
194
476
  ),
195
477
  }