cloudnetpy 1.65.8__py3-none-any.whl → 1.66.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cloudnetpy/categorize/__init__.py +0 -1
- cloudnetpy/categorize/atmos_utils.py +278 -59
- 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 +80 -0
- cloudnetpy/categorize/attenuations/melting_attenuation.py +75 -0
- cloudnetpy/categorize/attenuations/rain_attenuation.py +84 -0
- cloudnetpy/categorize/categorize.py +140 -81
- cloudnetpy/categorize/classify.py +92 -128
- cloudnetpy/categorize/containers.py +45 -31
- cloudnetpy/categorize/droplet.py +2 -2
- cloudnetpy/categorize/falling.py +3 -3
- cloudnetpy/categorize/freezing.py +2 -2
- cloudnetpy/categorize/itu.py +243 -0
- cloudnetpy/categorize/melting.py +0 -3
- cloudnetpy/categorize/model.py +31 -14
- cloudnetpy/categorize/radar.py +28 -12
- cloudnetpy/constants.py +3 -6
- cloudnetpy/model_evaluation/file_handler.py +2 -2
- cloudnetpy/model_evaluation/products/observation_products.py +8 -8
- cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +5 -2
- cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +11 -11
- cloudnetpy/output.py +46 -26
- cloudnetpy/plotting/plot_meta.py +8 -2
- cloudnetpy/plotting/plotting.py +37 -7
- cloudnetpy/products/classification.py +39 -34
- cloudnetpy/products/der.py +15 -13
- cloudnetpy/products/drizzle_tools.py +22 -21
- cloudnetpy/products/ier.py +8 -45
- cloudnetpy/products/iwc.py +7 -22
- cloudnetpy/products/lwc.py +14 -15
- cloudnetpy/products/mwr_tools.py +15 -2
- cloudnetpy/products/product_tools.py +121 -119
- cloudnetpy/utils.py +4 -0
- cloudnetpy/version.py +2 -2
- {cloudnetpy-1.65.8.dist-info → cloudnetpy-1.66.1.dist-info}/METADATA +1 -1
- {cloudnetpy-1.65.8.dist-info → cloudnetpy-1.66.1.dist-info}/RECORD +41 -35
- {cloudnetpy-1.65.8.dist-info → cloudnetpy-1.66.1.dist-info}/WHEEL +1 -1
- cloudnetpy/categorize/atmos.py +0 -376
- {cloudnetpy-1.65.8.dist-info → cloudnetpy-1.66.1.dist-info}/LICENSE +0 -0
- {cloudnetpy-1.65.8.dist-info → cloudnetpy-1.66.1.dist-info}/top_level.txt +0 -0
@@ -1,19 +1,28 @@
|
|
1
|
-
"""Module containing low-level functions to classify gridded
|
2
|
-
radar / lidar measurements.
|
3
|
-
"""
|
4
|
-
|
5
1
|
import numpy as np
|
6
2
|
import skimage
|
7
3
|
from numpy import ma
|
4
|
+
from numpy.typing import NDArray
|
8
5
|
|
9
|
-
import cloudnetpy.categorize.atmos
|
10
6
|
from cloudnetpy import utils
|
11
|
-
from cloudnetpy.categorize import
|
12
|
-
|
7
|
+
from cloudnetpy.categorize import (
|
8
|
+
atmos_utils,
|
9
|
+
droplet,
|
10
|
+
falling,
|
11
|
+
freezing,
|
12
|
+
insects,
|
13
|
+
melting,
|
14
|
+
)
|
15
|
+
from cloudnetpy.categorize.attenuations import RadarAttenuation
|
16
|
+
from cloudnetpy.categorize.containers import (
|
17
|
+
ClassData,
|
18
|
+
ClassificationResult,
|
19
|
+
Observations,
|
20
|
+
)
|
13
21
|
from cloudnetpy.constants import T0
|
22
|
+
from cloudnetpy.products.product_tools import CategoryBits, QualityBits
|
14
23
|
|
15
24
|
|
16
|
-
def classify_measurements(data:
|
25
|
+
def classify_measurements(data: Observations) -> ClassificationResult:
|
17
26
|
"""Classifies radar/lidar observations.
|
18
27
|
|
19
28
|
This function classifies atmospheric scatterers from the input data.
|
@@ -21,8 +30,7 @@ def classify_measurements(data: dict) -> ClassificationResult:
|
|
21
30
|
time / height grid before calling this function.
|
22
31
|
|
23
32
|
Args:
|
24
|
-
data:
|
25
|
-
and :class:`Mwr` instances.
|
33
|
+
data: A :class:`Observations` instance.
|
26
34
|
|
27
35
|
Returns:
|
28
36
|
A :class:`ClassificationResult` instance.
|
@@ -37,19 +45,20 @@ def classify_measurements(data: dict) -> ClassificationResult:
|
|
37
45
|
implementation compared to the original Cloudnet methodology.
|
38
46
|
Especially methods classifying insects, melting layer and liquid droplets.
|
39
47
|
|
40
|
-
Explanation of bits:
|
41
|
-
- bit 0: Liquid droplets
|
42
|
-
- bit 1: Falling hydrometeors
|
43
|
-
- bit 2: Freezing region
|
44
|
-
- bit 3: Melting layer
|
45
|
-
- bit 4: Aerosols
|
46
|
-
- bit 5: Insects
|
47
|
-
|
48
48
|
"""
|
49
|
+
bits = CategoryBits(
|
50
|
+
droplet=np.array([], dtype=bool),
|
51
|
+
falling=np.array([], dtype=bool),
|
52
|
+
freezing=np.array([], dtype=bool),
|
53
|
+
melting=np.array([], dtype=bool),
|
54
|
+
aerosol=np.array([], dtype=bool),
|
55
|
+
insect=np.array([], dtype=bool),
|
56
|
+
)
|
57
|
+
|
49
58
|
obs = ClassData(data)
|
50
|
-
|
51
|
-
bits
|
52
|
-
bits
|
59
|
+
|
60
|
+
bits.melting = melting.find_melting_layer(obs)
|
61
|
+
bits.freezing = freezing.find_freezing_region(obs, bits.melting)
|
53
62
|
liquid_from_lidar = droplet.find_liquid(obs)
|
54
63
|
if obs.lv0_files is not None and len(obs.lv0_files) > 0:
|
55
64
|
if "rpg-fmcw-94" not in obs.radar_type.lower():
|
@@ -68,24 +77,24 @@ def classify_measurements(data: dict) -> ClassificationResult:
|
|
68
77
|
liquid_from_radar,
|
69
78
|
liquid_from_lidar,
|
70
79
|
)
|
71
|
-
liquid_from_radar[~bits
|
80
|
+
liquid_from_radar[~bits.freezing] = 0
|
72
81
|
is_liquid = liquid_from_radar | liquid_from_lidar
|
73
82
|
else:
|
74
83
|
is_liquid = liquid_from_lidar
|
75
84
|
liquid_prob = None
|
76
|
-
bits
|
77
|
-
bits
|
78
|
-
bits
|
79
|
-
|
85
|
+
bits.droplet = droplet.correct_liquid_top(obs, is_liquid, bits.freezing, limit=500)
|
86
|
+
bits.insect, insect_prob = insects.find_insects(obs, bits.melting, bits.droplet)
|
87
|
+
bits.falling = falling.find_falling_hydrometeors(obs, bits.droplet, bits.insect)
|
88
|
+
filtered_ice = _filter_falling(bits)
|
80
89
|
for _ in range(5):
|
81
|
-
|
82
|
-
|
83
|
-
bits
|
84
|
-
bits[
|
85
|
-
|
90
|
+
_fix_undetected_melting_layer(bits)
|
91
|
+
_filter_insects(bits)
|
92
|
+
bits.aerosol = _find_aerosols(obs, bits)
|
93
|
+
bits.aerosol[filtered_ice] = False
|
94
|
+
_fix_super_cold_liquid(obs, bits)
|
86
95
|
|
87
96
|
return ClassificationResult(
|
88
|
-
category_bits=
|
97
|
+
category_bits=bits,
|
89
98
|
is_rain=obs.is_rain,
|
90
99
|
is_clutter=obs.is_clutter,
|
91
100
|
insect_prob=insect_prob,
|
@@ -93,112 +102,86 @@ def classify_measurements(data: dict) -> ClassificationResult:
|
|
93
102
|
)
|
94
103
|
|
95
104
|
|
96
|
-
def
|
105
|
+
def fetch_quality(
|
106
|
+
data: Observations,
|
107
|
+
classification: ClassificationResult,
|
108
|
+
attenuations: RadarAttenuation,
|
109
|
+
) -> QualityBits:
|
110
|
+
return QualityBits(
|
111
|
+
radar=~data.radar.data["Z"][:].mask,
|
112
|
+
lidar=~data.lidar.data["beta"][:].mask,
|
113
|
+
clutter=classification.is_clutter,
|
114
|
+
molecular=np.zeros(data.radar.data["Z"][:].shape, dtype=bool),
|
115
|
+
attenuated_liquid=attenuations.liquid.attenuated,
|
116
|
+
corrected_liquid=attenuations.liquid.attenuated
|
117
|
+
& ~attenuations.liquid.uncorrected,
|
118
|
+
attenuated_rain=attenuations.rain.attenuated,
|
119
|
+
corrected_rain=attenuations.rain.attenuated & ~attenuations.rain.uncorrected,
|
120
|
+
attenuated_melting=attenuations.melting.attenuated,
|
121
|
+
corrected_melting=attenuations.melting.attenuated
|
122
|
+
& ~attenuations.melting.uncorrected,
|
123
|
+
)
|
124
|
+
|
125
|
+
|
126
|
+
def _fix_super_cold_liquid(obs: ClassData, bits: CategoryBits) -> None:
|
97
127
|
"""Supercooled liquid droplets do not exist in atmosphere below around -38 C."""
|
98
128
|
t_limit = T0 - 38
|
99
|
-
super_cold_liquid = np.where((obs.tw < t_limit) & bits
|
100
|
-
bits[
|
101
|
-
bits[
|
102
|
-
return bits
|
129
|
+
super_cold_liquid = np.where((obs.tw < t_limit) & bits.droplet)
|
130
|
+
bits.droplet[super_cold_liquid] = False
|
131
|
+
bits.falling[super_cold_liquid] = True
|
103
132
|
|
104
133
|
|
105
134
|
def _remove_false_radar_liquid(
|
106
135
|
liquid_from_radar: np.ndarray,
|
107
136
|
liquid_from_lidar: np.ndarray,
|
108
|
-
) -> np.
|
137
|
+
) -> NDArray[np.bool_]:
|
109
138
|
"""Removes radar-liquid below lidar-detected liquid bases."""
|
110
|
-
lidar_liquid_bases =
|
139
|
+
lidar_liquid_bases = atmos_utils.find_cloud_bases(liquid_from_lidar)
|
111
140
|
for prof, base in zip(*np.where(lidar_liquid_bases), strict=True):
|
112
141
|
liquid_from_radar[prof, 0:base] = 0
|
113
142
|
return liquid_from_radar
|
114
143
|
|
115
144
|
|
116
|
-
def fetch_quality(
|
117
|
-
data: dict,
|
118
|
-
classification: ClassificationResult,
|
119
|
-
attenuations: dict,
|
120
|
-
) -> dict:
|
121
|
-
"""Returns Cloudnet quality bits.
|
122
|
-
|
123
|
-
Args:
|
124
|
-
data: Containing :class:`Radar` and :class:`Lidar` instances.
|
125
|
-
classification: A :class:`ClassificationResult` instance.
|
126
|
-
attenuations: Dictionary containing keys `liquid_corrected`,
|
127
|
-
`liquid_uncorrected`.
|
128
|
-
|
129
|
-
Returns:
|
130
|
-
Dictionary containing `quality_bits`, an integer array with the bits:
|
131
|
-
|
132
|
-
- bit 0: Pixel contains radar data
|
133
|
-
- bit 1: Pixel contains lidar data
|
134
|
-
- bit 2: Pixel contaminated by radar clutter
|
135
|
-
- bit 3: Molecular scattering present (currently not implemented!)
|
136
|
-
- bit 4: Pixel was affected by liquid attenuation
|
137
|
-
- bit 5: Liquid attenuation was corrected
|
138
|
-
- bit 6: Data gap in radar or lidar data
|
139
|
-
|
140
|
-
"""
|
141
|
-
bits: list[np.ndarray] = [np.ndarray([])] * 7
|
142
|
-
radar_echo = data["radar"].data["Z"][:]
|
143
|
-
bits[0] = ~radar_echo.mask
|
144
|
-
bits[1] = ~data["lidar"].data["beta"][:].mask
|
145
|
-
bits[2] = classification.is_clutter
|
146
|
-
bits[4] = attenuations["liquid_corrected"] | attenuations["liquid_uncorrected"]
|
147
|
-
bits[5] = attenuations["liquid_corrected"]
|
148
|
-
qbits = _bits_to_integer(bits)
|
149
|
-
return {"quality_bits": qbits}
|
150
|
-
|
151
|
-
|
152
145
|
def _find_aerosols(
|
153
146
|
obs: ClassData,
|
154
|
-
|
155
|
-
|
156
|
-
) -> np.ndarray:
|
147
|
+
bits: CategoryBits,
|
148
|
+
) -> NDArray[np.bool_]:
|
157
149
|
"""Estimates aerosols from lidar backscattering.
|
158
150
|
|
159
151
|
Aerosols are lidar signals that are: a) not falling, b) not liquid droplets.
|
160
152
|
|
161
153
|
Args:
|
162
154
|
obs: A :class:`ClassData` instance.
|
163
|
-
|
164
|
-
is_liquid: 2-D boolean array of liquid droplets.
|
155
|
+
bits: A :class:`CategoryBits instance.
|
165
156
|
|
166
157
|
Returns:
|
167
158
|
2-D boolean array containing aerosols.
|
168
159
|
|
169
160
|
"""
|
170
161
|
is_beta = ~obs.beta.mask
|
171
|
-
return is_beta & ~
|
162
|
+
return is_beta & ~bits.falling & ~bits.droplet
|
172
163
|
|
173
164
|
|
174
|
-
def _fix_undetected_melting_layer(bits:
|
175
|
-
|
176
|
-
drizzle_and_falling = _find_drizzle_and_falling(*bits[:3])
|
165
|
+
def _fix_undetected_melting_layer(bits: CategoryBits) -> None:
|
166
|
+
drizzle_and_falling = _find_drizzle_and_falling(bits)
|
177
167
|
transition = ma.diff(drizzle_and_falling, axis=1) == -1
|
178
|
-
|
179
|
-
return melting_layer
|
168
|
+
bits.melting[:, 1:][transition] = True
|
180
169
|
|
181
170
|
|
182
|
-
def _find_drizzle_and_falling(
|
183
|
-
is_liquid: np.ndarray,
|
184
|
-
is_falling: np.ndarray,
|
185
|
-
is_freezing: np.ndarray,
|
186
|
-
) -> np.ndarray:
|
171
|
+
def _find_drizzle_and_falling(bits: CategoryBits) -> np.ndarray:
|
187
172
|
"""Classifies pixels as falling, drizzle and others.
|
188
173
|
|
189
174
|
Args:
|
190
|
-
|
191
|
-
is_falling: 2D boolean array denoting falling pixels.
|
192
|
-
is_freezing: 2D boolean array denoting subzero temperatures.
|
175
|
+
bits: A :class:`CategoryBits instance.
|
193
176
|
|
194
177
|
Returns:
|
195
178
|
2D array where values are 1 (falling, drizzle, supercooled liquids),
|
196
179
|
2 (drizzle), and masked (all others).
|
197
180
|
|
198
181
|
"""
|
199
|
-
falling_dry =
|
200
|
-
supercooled_liquids =
|
201
|
-
drizzle = falling_dry & ~
|
182
|
+
falling_dry = bits.falling & ~bits.droplet
|
183
|
+
supercooled_liquids = bits.droplet & bits.freezing
|
184
|
+
drizzle = falling_dry & ~bits.freezing
|
202
185
|
drizzle_and_falling = falling_dry.astype(int) + drizzle.astype(int)
|
203
186
|
drizzle_and_falling = ma.copy(drizzle_and_falling)
|
204
187
|
drizzle_and_falling[supercooled_liquids] = 1
|
@@ -206,28 +189,10 @@ def _find_drizzle_and_falling(
|
|
206
189
|
return drizzle_and_falling
|
207
190
|
|
208
191
|
|
209
|
-
def
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
bits: List of bit fields (of similar sizes) to be saved in the resulting
|
214
|
-
array of integers. bits[0] is saved as bit 0, bits[1] as bit 1, etc.
|
215
|
-
|
216
|
-
Returns:
|
217
|
-
Array of integers containing the information of the individual boolean arrays.
|
218
|
-
|
219
|
-
"""
|
220
|
-
int_array = np.zeros_like(bits[0], dtype=int)
|
221
|
-
for n, bit in enumerate(bits):
|
222
|
-
ind = np.where(bit) # works also if bit is None
|
223
|
-
int_array[ind] = utils.setbit(int_array[ind].astype(int), n)
|
224
|
-
return int_array
|
225
|
-
|
226
|
-
|
227
|
-
def _filter_insects(bits: list) -> list:
|
228
|
-
is_melting_layer = bits[3]
|
229
|
-
is_insects = bits[5]
|
230
|
-
is_falling = bits[1]
|
192
|
+
def _filter_insects(bits: CategoryBits) -> None:
|
193
|
+
is_melting_layer = bits.melting
|
194
|
+
is_insects = bits.insect
|
195
|
+
is_falling = bits.falling
|
231
196
|
|
232
197
|
# Remove above melting layer
|
233
198
|
above_melting = utils.ffill(is_melting_layer)
|
@@ -256,15 +221,14 @@ def _filter_insects(bits: list) -> list:
|
|
256
221
|
is_insects[ind1[ind11], y - 1 : y + 2] = False
|
257
222
|
except IndexError:
|
258
223
|
continue
|
259
|
-
bits
|
260
|
-
bits
|
261
|
-
return bits
|
224
|
+
bits.falling = is_falling
|
225
|
+
bits.insect = is_insects
|
262
226
|
|
263
227
|
|
264
|
-
def _filter_falling(bits:
|
228
|
+
def _filter_falling(bits: CategoryBits) -> tuple:
|
265
229
|
# filter falling ice speckle noise
|
266
|
-
is_freezing = bits
|
267
|
-
is_falling = bits
|
230
|
+
is_freezing = bits.freezing
|
231
|
+
is_falling = bits.falling
|
268
232
|
is_falling_filtered = skimage.morphology.remove_small_objects(
|
269
233
|
is_falling,
|
270
234
|
10,
|
@@ -276,6 +240,6 @@ def _filter_falling(bits: list) -> tuple:
|
|
276
240
|
# in warm these are (probably) insects
|
277
241
|
insect_ind = np.where(~is_freezing & is_filtered)
|
278
242
|
is_falling[insect_ind] = False
|
279
|
-
bits
|
280
|
-
bits[
|
281
|
-
return
|
243
|
+
bits.falling = is_falling
|
244
|
+
bits.insect[insect_ind] = True
|
245
|
+
return ice_ind
|
@@ -5,13 +5,28 @@ from numpy import ma
|
|
5
5
|
|
6
6
|
from cloudnetpy import utils
|
7
7
|
from cloudnetpy.constants import MM_H_TO_M_S
|
8
|
+
from cloudnetpy.products.product_tools import CategoryBits
|
9
|
+
|
10
|
+
from .disdrometer import Disdrometer
|
11
|
+
from .lidar import Lidar
|
12
|
+
from .model import Model
|
13
|
+
from .mwr import Mwr
|
14
|
+
from .radar import Radar
|
8
15
|
|
9
16
|
|
10
17
|
@dataclass
|
11
|
-
class
|
12
|
-
|
18
|
+
class Observations:
|
19
|
+
radar: Radar
|
20
|
+
lidar: Lidar
|
21
|
+
model: Model
|
22
|
+
mwr: Mwr | None = None
|
23
|
+
disdrometer: Disdrometer | None = None
|
24
|
+
lv0_files: list[str] | None = None
|
13
25
|
|
14
|
-
|
26
|
+
|
27
|
+
@dataclass
|
28
|
+
class ClassificationResult:
|
29
|
+
category_bits: CategoryBits
|
15
30
|
is_rain: np.ndarray
|
16
31
|
is_clutter: np.ndarray
|
17
32
|
insect_prob: np.ndarray
|
@@ -44,30 +59,30 @@ class ClassData:
|
|
44
59
|
|
45
60
|
"""
|
46
61
|
|
47
|
-
def __init__(self, data:
|
62
|
+
def __init__(self, data: Observations):
|
48
63
|
self.data = data
|
49
|
-
self.z = data
|
50
|
-
self.v = data
|
51
|
-
self.v_sigma = data
|
64
|
+
self.z = data.radar.data["Z"][:]
|
65
|
+
self.v = data.radar.data["v"][:]
|
66
|
+
self.v_sigma = data.radar.data["v_sigma"][:]
|
52
67
|
for key in ("width", "ldr", "sldr"):
|
53
|
-
if key in data
|
54
|
-
setattr(self, key, data
|
55
|
-
self.time = data
|
56
|
-
self.height = data
|
57
|
-
self.radar_type = data
|
58
|
-
self.tw = data
|
59
|
-
self.model_type = data
|
60
|
-
self.beta = data
|
68
|
+
if key in data.radar.data:
|
69
|
+
setattr(self, key, data.radar.data[key][:])
|
70
|
+
self.time = data.radar.time
|
71
|
+
self.height = data.radar.height
|
72
|
+
self.radar_type = data.radar.source_type
|
73
|
+
self.tw = data.model.data["Tw"][:]
|
74
|
+
self.model_type = data.model.source_type
|
75
|
+
self.beta = data.lidar.data["beta"][:]
|
61
76
|
self.lwp = (
|
62
|
-
data
|
63
|
-
if data
|
77
|
+
data.mwr.data["lwp"][:]
|
78
|
+
if data.mwr is not None
|
64
79
|
else ma.masked_all(self.time.shape)
|
65
80
|
)
|
66
81
|
self.is_rain = self._find_profiles_with_rain()
|
67
82
|
self.is_clutter = _find_clutter(self.v, self.is_rain)
|
68
|
-
self.altitude = data
|
69
|
-
self.lv0_files = data
|
70
|
-
self.date = data
|
83
|
+
self.altitude = data.radar.altitude
|
84
|
+
self.lv0_files = data.lv0_files
|
85
|
+
self.date = data.radar.get_date()
|
71
86
|
|
72
87
|
def _find_profiles_with_rain(self) -> np.ndarray:
|
73
88
|
is_rain = self._find_rain_from_radar_echo()
|
@@ -77,7 +92,8 @@ class ClassData:
|
|
77
92
|
return is_rain
|
78
93
|
|
79
94
|
def _find_rain_from_radar_echo(self) -> np.ndarray:
|
80
|
-
|
95
|
+
first_gate_with_data = np.argmin(self.z.mask.all(axis=0))
|
96
|
+
gate_number = first_gate_with_data + 3
|
81
97
|
threshold = {"z": 3, "v": 0, "ldr": -15}
|
82
98
|
z = self.z[:, gate_number]
|
83
99
|
v = self.v[:, gate_number]
|
@@ -98,19 +114,17 @@ class ClassData:
|
|
98
114
|
)
|
99
115
|
|
100
116
|
def _find_rain_from_disdrometer(self) -> ma.MaskedArray:
|
117
|
+
if self.data.disdrometer is None:
|
118
|
+
return ma.masked_all(self.time.shape, dtype=int)
|
101
119
|
threshold_mm_h = 0.25 # Standard threshold for drizzle -> rain
|
102
120
|
threshold_particles = 30 # This is arbitrary and should be better tested
|
103
121
|
threshold_rate = threshold_mm_h * MM_H_TO_M_S
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
)
|
111
|
-
except (AttributeError, KeyError):
|
112
|
-
is_rain = ma.masked_all(self.time.shape, dtype=int)
|
113
|
-
return is_rain
|
122
|
+
rainfall_rate = self.data.disdrometer.data["rainfall_rate"].data
|
123
|
+
n_particles = self.data.disdrometer.data["n_particles"].data
|
124
|
+
return ma.array(
|
125
|
+
(rainfall_rate > threshold_rate) & (n_particles > threshold_particles),
|
126
|
+
dtype=int,
|
127
|
+
)
|
114
128
|
|
115
129
|
|
116
130
|
def _find_clutter(
|
cloudnetpy/categorize/droplet.py
CHANGED
@@ -4,8 +4,8 @@ import numpy as np
|
|
4
4
|
import scipy.signal
|
5
5
|
from numpy import ma
|
6
6
|
|
7
|
-
import cloudnetpy.categorize.atmos
|
8
7
|
from cloudnetpy import utils
|
8
|
+
from cloudnetpy.categorize import atmos_utils
|
9
9
|
from cloudnetpy.categorize.containers import ClassData
|
10
10
|
|
11
11
|
|
@@ -32,7 +32,7 @@ def correct_liquid_top(
|
|
32
32
|
|
33
33
|
"""
|
34
34
|
is_liquid_corrected = np.copy(is_liquid)
|
35
|
-
liquid_tops =
|
35
|
+
liquid_tops = atmos_utils.find_cloud_tops(is_liquid)
|
36
36
|
top_above = utils.n_elements(obs.height, limit)
|
37
37
|
for prof, top in zip(*np.where(liquid_tops), strict=True):
|
38
38
|
ind = _find_ind_above_top(is_freezing[prof, top:], top_above)
|
cloudnetpy/categorize/falling.py
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
import numpy as np
|
4
4
|
from numpy import ma
|
5
5
|
|
6
|
-
from cloudnetpy.categorize import
|
6
|
+
from cloudnetpy.categorize import atmos_utils
|
7
7
|
from cloudnetpy.categorize.containers import ClassData
|
8
8
|
from cloudnetpy.constants import T0
|
9
9
|
|
@@ -96,8 +96,8 @@ def _fix_liquid_dominated_radar(
|
|
96
96
|
"""Radar signals inside liquid clouds are NOT ice if Z is
|
97
97
|
increasing in height inside the cloud.
|
98
98
|
"""
|
99
|
-
liquid_bases =
|
100
|
-
liquid_tops =
|
99
|
+
liquid_bases = atmos_utils.find_cloud_bases(is_liquid)
|
100
|
+
liquid_tops = atmos_utils.find_cloud_tops(is_liquid)
|
101
101
|
base_indices = np.where(liquid_bases)
|
102
102
|
top_indices = np.where(liquid_tops)
|
103
103
|
|
@@ -35,7 +35,7 @@ def find_freezing_region(obs: ClassData, melting_layer: np.ndarray) -> np.ndarra
|
|
35
35
|
|
36
36
|
"""
|
37
37
|
is_freezing = np.zeros(obs.tw.shape, dtype=bool)
|
38
|
-
t0_alt =
|
38
|
+
t0_alt = find_t0_alt(obs.tw, obs.height)
|
39
39
|
mean_melting_alt = _find_mean_melting_alt(obs, melting_layer)
|
40
40
|
|
41
41
|
if _is_all_freezing(mean_melting_alt, t0_alt, obs.height):
|
@@ -78,7 +78,7 @@ def _find_mean_melting_alt(obs: ClassData, melting_layer: np.ndarray) -> ma.Mask
|
|
78
78
|
return ma.median(melting_alts, axis=1)
|
79
79
|
|
80
80
|
|
81
|
-
def
|
81
|
+
def find_t0_alt(temperature: np.ndarray, height: np.ndarray) -> np.ndarray:
|
82
82
|
"""Interpolates altitudes where temperature goes below freezing.
|
83
83
|
|
84
84
|
Args:
|