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.
- cloudnetpy/categorize/__init__.py +1 -2
- cloudnetpy/categorize/atmos_utils.py +297 -67
- cloudnetpy/categorize/attenuation.py +31 -0
- cloudnetpy/categorize/attenuations/__init__.py +37 -0
- cloudnetpy/categorize/attenuations/gas_attenuation.py +30 -0
- cloudnetpy/categorize/attenuations/liquid_attenuation.py +84 -0
- cloudnetpy/categorize/attenuations/melting_attenuation.py +78 -0
- cloudnetpy/categorize/attenuations/rain_attenuation.py +84 -0
- cloudnetpy/categorize/categorize.py +332 -156
- cloudnetpy/categorize/classify.py +127 -125
- cloudnetpy/categorize/containers.py +107 -76
- cloudnetpy/categorize/disdrometer.py +40 -0
- cloudnetpy/categorize/droplet.py +23 -21
- cloudnetpy/categorize/falling.py +53 -24
- cloudnetpy/categorize/freezing.py +25 -12
- cloudnetpy/categorize/insects.py +35 -23
- cloudnetpy/categorize/itu.py +243 -0
- cloudnetpy/categorize/lidar.py +36 -41
- cloudnetpy/categorize/melting.py +34 -26
- cloudnetpy/categorize/model.py +84 -37
- cloudnetpy/categorize/mwr.py +18 -14
- cloudnetpy/categorize/radar.py +215 -102
- cloudnetpy/cli.py +578 -0
- cloudnetpy/cloudnetarray.py +43 -89
- cloudnetpy/concat_lib.py +218 -78
- cloudnetpy/constants.py +28 -10
- cloudnetpy/datasource.py +61 -86
- cloudnetpy/exceptions.py +49 -20
- cloudnetpy/instruments/__init__.py +5 -0
- cloudnetpy/instruments/basta.py +29 -12
- cloudnetpy/instruments/bowtie.py +135 -0
- cloudnetpy/instruments/ceilo.py +138 -115
- cloudnetpy/instruments/ceilometer.py +164 -80
- cloudnetpy/instruments/cl61d.py +21 -5
- cloudnetpy/instruments/cloudnet_instrument.py +74 -36
- cloudnetpy/instruments/copernicus.py +108 -30
- cloudnetpy/instruments/da10.py +54 -0
- cloudnetpy/instruments/disdrometer/common.py +126 -223
- cloudnetpy/instruments/disdrometer/parsivel.py +453 -94
- cloudnetpy/instruments/disdrometer/thies.py +254 -87
- cloudnetpy/instruments/fd12p.py +201 -0
- cloudnetpy/instruments/galileo.py +65 -23
- cloudnetpy/instruments/hatpro.py +123 -49
- cloudnetpy/instruments/instruments.py +113 -1
- cloudnetpy/instruments/lufft.py +39 -17
- cloudnetpy/instruments/mira.py +268 -61
- cloudnetpy/instruments/mrr.py +187 -0
- cloudnetpy/instruments/nc_lidar.py +19 -8
- cloudnetpy/instruments/nc_radar.py +109 -55
- cloudnetpy/instruments/pollyxt.py +135 -51
- cloudnetpy/instruments/radiometrics.py +313 -59
- cloudnetpy/instruments/rain_e_h3.py +171 -0
- cloudnetpy/instruments/rpg.py +321 -189
- cloudnetpy/instruments/rpg_reader.py +74 -40
- cloudnetpy/instruments/toa5.py +49 -0
- cloudnetpy/instruments/vaisala.py +95 -343
- cloudnetpy/instruments/weather_station.py +774 -105
- cloudnetpy/metadata.py +90 -19
- cloudnetpy/model_evaluation/file_handler.py +55 -52
- cloudnetpy/model_evaluation/metadata.py +46 -20
- cloudnetpy/model_evaluation/model_metadata.py +1 -1
- cloudnetpy/model_evaluation/plotting/plot_tools.py +32 -37
- cloudnetpy/model_evaluation/plotting/plotting.py +327 -117
- cloudnetpy/model_evaluation/products/advance_methods.py +92 -83
- cloudnetpy/model_evaluation/products/grid_methods.py +88 -63
- cloudnetpy/model_evaluation/products/model_products.py +43 -35
- cloudnetpy/model_evaluation/products/observation_products.py +41 -35
- cloudnetpy/model_evaluation/products/product_resampling.py +17 -7
- cloudnetpy/model_evaluation/products/tools.py +29 -20
- cloudnetpy/model_evaluation/statistics/statistical_methods.py +30 -20
- cloudnetpy/model_evaluation/tests/e2e/conftest.py +3 -3
- cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +9 -5
- cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +15 -14
- cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +9 -5
- cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +15 -14
- cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +9 -5
- cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +15 -14
- cloudnetpy/model_evaluation/tests/unit/conftest.py +42 -41
- cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +41 -48
- cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +216 -194
- cloudnetpy/model_evaluation/tests/unit/test_model_products.py +23 -21
- cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +37 -38
- cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +43 -40
- cloudnetpy/model_evaluation/tests/unit/test_plotting.py +30 -36
- cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +68 -31
- cloudnetpy/model_evaluation/tests/unit/test_tools.py +33 -26
- cloudnetpy/model_evaluation/utils.py +2 -1
- cloudnetpy/output.py +170 -111
- cloudnetpy/plotting/__init__.py +2 -1
- cloudnetpy/plotting/plot_meta.py +562 -822
- cloudnetpy/plotting/plotting.py +1142 -704
- cloudnetpy/products/__init__.py +1 -0
- cloudnetpy/products/classification.py +370 -88
- cloudnetpy/products/der.py +85 -55
- cloudnetpy/products/drizzle.py +77 -34
- cloudnetpy/products/drizzle_error.py +15 -11
- cloudnetpy/products/drizzle_tools.py +79 -59
- cloudnetpy/products/epsilon.py +211 -0
- cloudnetpy/products/ier.py +27 -50
- cloudnetpy/products/iwc.py +55 -48
- cloudnetpy/products/lwc.py +96 -70
- cloudnetpy/products/mwr_tools.py +186 -0
- cloudnetpy/products/product_tools.py +170 -128
- cloudnetpy/utils.py +455 -240
- cloudnetpy/version.py +2 -2
- {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/METADATA +44 -40
- cloudnetpy-1.87.3.dist-info/RECORD +127 -0
- {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/WHEEL +1 -1
- cloudnetpy-1.87.3.dist-info/entry_points.txt +2 -0
- docs/source/conf.py +2 -2
- cloudnetpy/categorize/atmos.py +0 -361
- cloudnetpy/products/mwr_multi.py +0 -68
- cloudnetpy/products/mwr_single.py +0 -75
- cloudnetpy-1.49.9.dist-info/RECORD +0 -112
- {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info/licenses}/LICENSE +0 -0
- {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/top_level.txt +0 -0
cloudnetpy/products/__init__.py
CHANGED
|
@@ -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
|
|
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
|
|
14
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
tops -
|
|
68
|
+
source.append_data(
|
|
69
|
+
tops - source.altitude,
|
|
70
|
+
"cloud_top_height_agl",
|
|
49
71
|
)
|
|
50
|
-
|
|
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(
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
|
64
|
-
classification = ma.zeros(bits
|
|
65
|
-
classification[bits
|
|
66
|
-
classification[~bits
|
|
67
|
-
classification[
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
] =
|
|
74
|
-
classification[bits
|
|
75
|
-
classification[bits
|
|
76
|
-
classification[bits
|
|
77
|
-
classification[
|
|
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) ->
|
|
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
|
|
88
|
-
status[bits
|
|
89
|
-
status[bits
|
|
90
|
-
status[
|
|
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:
|
|
102
|
-
|
|
103
|
-
|
|
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 =
|
|
108
|
-
highest_tops =
|
|
109
|
-
|
|
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:
|
|
311
|
+
def _find_cloud_mask(classification: npt.NDArray) -> npt.NDArray:
|
|
114
312
|
cloud_mask = np.zeros(classification.shape, dtype=int)
|
|
115
|
-
for value in [
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
}
|