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,57 +1,245 @@
|
|
1
|
+
import logging
|
2
|
+
|
1
3
|
import numpy as np
|
4
|
+
import numpy.typing as npt
|
5
|
+
import scipy.constants
|
2
6
|
from numpy import ma
|
3
7
|
|
4
8
|
import cloudnetpy.constants as con
|
9
|
+
from cloudnetpy import utils
|
5
10
|
|
6
11
|
|
7
12
|
def calc_wet_bulb_temperature(model_data: dict) -> np.ndarray:
|
8
|
-
"""
|
13
|
+
"""Calculate wet-bulb temperature iteratively.
|
14
|
+
|
15
|
+
Args:
|
16
|
+
model_data: Model variables `temperature`, `pressure`, `q`.
|
17
|
+
|
18
|
+
Returns:
|
19
|
+
Wet-bulb temperature (K).
|
20
|
+
|
21
|
+
References:
|
22
|
+
Al-Ismaili, A. M., & Al-Azri, N. A. (2016). Simple Iterative Approach to
|
23
|
+
Calculate Wet-Bulb Temperature for Estimating Evaporative Cooling
|
24
|
+
Efficiency. Int. J. Agric. Innovations Res., 4, 1013-1018.
|
25
|
+
"""
|
26
|
+
specific_humidity = model_data["q"]
|
27
|
+
pressure = model_data["pressure"]
|
28
|
+
td = k2c(model_data["temperature"])
|
29
|
+
vp = calc_vapor_pressure(pressure, specific_humidity)
|
30
|
+
W = calc_mixing_ratio(vp, pressure)
|
31
|
+
L_v_0 = 2501e3 # Latent heat of vaporization at 0degC (J kg-1)
|
32
|
+
|
33
|
+
def f(tw):
|
34
|
+
svp = calc_saturation_vapor_pressure(c2k(tw))
|
35
|
+
W_s = calc_mixing_ratio(svp, pressure)
|
36
|
+
C_p_w = 0.0265 * tw**2 - 1.7688 * tw + 4205.6 # Eq. 6 (J kg-1 C-1)
|
37
|
+
C_p_wv = 0.0016 * td**2 + 0.1546 * td + 1858.7 # Eq. 7 (J kg-1 C-1)
|
38
|
+
C_p_da = 0.0667 * ((td + tw) / 2) + 1005 # Eq. 8 (J kg-1 C-1)
|
39
|
+
a = (L_v_0 - (C_p_w - C_p_wv) * tw) * W_s - C_p_da * (td - tw)
|
40
|
+
b = L_v_0 + C_p_wv * td - C_p_w * tw
|
41
|
+
return a / b - W
|
42
|
+
|
43
|
+
min_err = 1e-6 * np.maximum(np.abs(td), 1)
|
44
|
+
delta = 1e-8
|
45
|
+
tw = td
|
46
|
+
max_iter = 20
|
47
|
+
for _ in range(max_iter):
|
48
|
+
f_tw = f(tw)
|
49
|
+
if np.all(np.abs(f_tw) < min_err):
|
50
|
+
break
|
51
|
+
df_tw = (f(tw + delta) - f_tw) / delta
|
52
|
+
tw = tw - f_tw / df_tw
|
53
|
+
else:
|
54
|
+
msg = (
|
55
|
+
"Wet-bulb temperature didn't converge after %d iterations: "
|
56
|
+
"error min %g, max %g, mean %g, median %g"
|
57
|
+
)
|
58
|
+
logging.warning(
|
59
|
+
msg, max_iter, np.min(f_tw), np.max(f_tw), np.mean(f_tw), np.median(f_tw)
|
60
|
+
)
|
61
|
+
|
62
|
+
return c2k(tw)
|
63
|
+
|
9
64
|
|
10
|
-
|
11
|
-
pressure
|
12
|
-
|
65
|
+
def calc_vapor_pressure(
|
66
|
+
pressure: npt.NDArray, specific_humidity: npt.NDArray
|
67
|
+
) -> npt.NDArray:
|
68
|
+
"""Calculate vapor pressure of water based on pressure and specific
|
69
|
+
humidity.
|
13
70
|
|
14
71
|
Args:
|
15
|
-
|
72
|
+
pressure: Pressure (Pa)
|
73
|
+
specific_humidity: Specific humidity (1)
|
16
74
|
|
17
75
|
Returns:
|
18
|
-
|
76
|
+
Vapor pressure (Pa)
|
19
77
|
|
20
78
|
References:
|
21
|
-
J.
|
22
|
-
|
79
|
+
Cai, J. (2019). Humidity Measures.
|
80
|
+
https://cran.r-project.org/web/packages/humidity/vignettes/humidity-measures.html
|
81
|
+
"""
|
82
|
+
return (
|
83
|
+
specific_humidity
|
84
|
+
* pressure
|
85
|
+
/ (con.MW_RATIO + (1 - con.MW_RATIO) * specific_humidity)
|
86
|
+
)
|
87
|
+
|
88
|
+
|
89
|
+
def c2k(temp: np.ndarray) -> np.ndarray:
|
90
|
+
"""Converts Celsius to Kelvins."""
|
91
|
+
return ma.array(temp) + 273.15
|
92
|
+
|
93
|
+
|
94
|
+
def k2c(temp: np.ndarray) -> np.ndarray:
|
95
|
+
"""Converts Kelvins to Celsius."""
|
96
|
+
return ma.array(temp) - 273.15
|
97
|
+
|
98
|
+
|
99
|
+
def find_cloud_bases(array: np.ndarray) -> np.ndarray:
|
100
|
+
"""Finds bases of clouds.
|
101
|
+
|
102
|
+
Args:
|
103
|
+
array: 2D boolean array denoting clouds or some other similar field.
|
104
|
+
|
105
|
+
Returns:
|
106
|
+
Boolean array indicating bases of the individual clouds.
|
107
|
+
|
108
|
+
"""
|
109
|
+
zeros = np.zeros(array.shape[0])
|
110
|
+
array_padded = np.insert(array, 0, zeros, axis=1).astype(int)
|
111
|
+
return np.diff(array_padded, axis=1) == 1
|
112
|
+
|
113
|
+
|
114
|
+
def find_cloud_tops(array: np.ndarray) -> np.ndarray:
|
115
|
+
"""Finds tops of clouds.
|
116
|
+
|
117
|
+
Args:
|
118
|
+
array: 2D boolean array denoting clouds or some other similar field.
|
119
|
+
|
120
|
+
Returns:
|
121
|
+
Boolean array indicating tops of the individual clouds.
|
23
122
|
|
24
123
|
"""
|
124
|
+
array_flipped = np.fliplr(array)
|
125
|
+
bases_of_flipped = find_cloud_bases(array_flipped)
|
126
|
+
return np.fliplr(bases_of_flipped)
|
127
|
+
|
128
|
+
|
129
|
+
def find_lowest_cloud_bases(
|
130
|
+
cloud_mask: np.ndarray,
|
131
|
+
height: np.ndarray,
|
132
|
+
) -> ma.MaskedArray:
|
133
|
+
"""Finds altitudes of cloud bases."""
|
134
|
+
cloud_heights = cloud_mask * height
|
135
|
+
return _find_lowest_heights(cloud_heights)
|
136
|
+
|
25
137
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
138
|
+
def find_highest_cloud_tops(
|
139
|
+
cloud_mask: np.ndarray,
|
140
|
+
height: np.ndarray,
|
141
|
+
) -> ma.MaskedArray:
|
142
|
+
"""Finds altitudes of cloud tops."""
|
143
|
+
cloud_heights = cloud_mask * height
|
144
|
+
cloud_heights_flipped = np.fliplr(cloud_heights)
|
145
|
+
return _find_lowest_heights(cloud_heights_flipped)
|
146
|
+
|
147
|
+
|
148
|
+
def _find_lowest_heights(cloud_heights: np.ndarray) -> ma.MaskedArray:
|
149
|
+
inds = (cloud_heights != 0).argmax(axis=1)
|
150
|
+
heights = np.array([cloud_heights[i, ind] for i, ind in enumerate(inds)])
|
151
|
+
return ma.masked_equal(heights, 0.0)
|
152
|
+
|
153
|
+
|
154
|
+
def fill_clouds_with_lwc_dz(
|
155
|
+
temperature: np.ndarray, pressure: np.ndarray, is_liquid: np.ndarray
|
156
|
+
) -> np.ndarray:
|
157
|
+
"""Fills liquid clouds with lwc change rate at the cloud bases.
|
158
|
+
|
159
|
+
Args:
|
160
|
+
temperature: 2D temperature array (K).
|
161
|
+
pressure: 2D pressure array (Pa).
|
162
|
+
is_liquid: Boolean array indicating presence of liquid clouds.
|
163
|
+
|
164
|
+
Returns:
|
165
|
+
Liquid water content change rate (kg m-3 m-1), so that for each cloud the base
|
166
|
+
value is filled for the whole cloud.
|
167
|
+
|
168
|
+
"""
|
169
|
+
lwc_dz = get_lwc_change_rate_at_bases(temperature, pressure, is_liquid)
|
170
|
+
lwc_dz_filled = ma.zeros(lwc_dz.shape)
|
171
|
+
lwc_dz_filled[is_liquid] = utils.ffill(lwc_dz[is_liquid])
|
172
|
+
return lwc_dz_filled
|
173
|
+
|
174
|
+
|
175
|
+
def get_lwc_change_rate_at_bases(
|
176
|
+
temperature: np.ndarray,
|
177
|
+
pressure: np.ndarray,
|
178
|
+
is_liquid: np.ndarray,
|
179
|
+
) -> np.ndarray:
|
180
|
+
"""Finds LWC change rate in liquid cloud bases.
|
181
|
+
|
182
|
+
Args:
|
183
|
+
temperature: 2D temperature array (K).
|
184
|
+
pressure: 2D pressure array (Pa).
|
185
|
+
is_liquid: Boolean array indicating presence of liquid clouds.
|
186
|
+
|
187
|
+
Returns:
|
188
|
+
Liquid water content change rate at cloud bases (kg m-3 m-1).
|
189
|
+
|
190
|
+
"""
|
191
|
+
liquid_bases = find_cloud_bases(is_liquid)
|
192
|
+
lwc_dz = ma.zeros(liquid_bases.shape)
|
193
|
+
lwc_dz[liquid_bases] = calc_lwc_change_rate(
|
194
|
+
temperature[liquid_bases],
|
195
|
+
pressure[liquid_bases],
|
53
196
|
)
|
54
|
-
|
197
|
+
|
198
|
+
return lwc_dz
|
199
|
+
|
200
|
+
|
201
|
+
def calc_lwc_change_rate(temperature: np.ndarray, pressure: np.ndarray) -> np.ndarray:
|
202
|
+
"""Returns rate of change of condensable water (LWC).
|
203
|
+
|
204
|
+
Calculates the theoretical adiabatic rate of increase of LWC
|
205
|
+
with height, given the cloud base temperature and pressure.
|
206
|
+
|
207
|
+
Args:
|
208
|
+
temperature: Temperature of cloud base (K).
|
209
|
+
pressure: Pressure of cloud base (Pa).
|
210
|
+
|
211
|
+
Returns:
|
212
|
+
dlwc/dz (kg m-3 m-1)
|
213
|
+
|
214
|
+
References:
|
215
|
+
Brenguier, 1991, https://doi.org/10.1175/1520-0469(1991)048<0264:POTCPA>2.0.CO;2
|
216
|
+
|
217
|
+
"""
|
218
|
+
svp = calc_saturation_vapor_pressure(temperature)
|
219
|
+
svp_mixing_ratio = calc_mixing_ratio(svp, pressure)
|
220
|
+
air_density = calc_air_density(pressure, temperature, svp_mixing_ratio)
|
221
|
+
|
222
|
+
e = 0.622
|
223
|
+
Cp = 1004 # J kg-1 K-1
|
224
|
+
Lv = 2.45e6 # J kg-1 = Pa m3 kg-1
|
225
|
+
qs = svp_mixing_ratio # kg kg-1
|
226
|
+
pa = air_density # kg m-3
|
227
|
+
es = svp # Pa
|
228
|
+
P = pressure # Pa
|
229
|
+
T = temperature # K
|
230
|
+
|
231
|
+
# See Appendix B in Brenguier (1991) for the derivation of the following equation
|
232
|
+
dqs_dp = (
|
233
|
+
-(1 - (Cp * T) / (e * Lv))
|
234
|
+
* (((Cp * T) / (e * Lv)) + ((Lv * qs * pa) / (P - es))) ** -1
|
235
|
+
* (e * es)
|
236
|
+
* (P - es) ** -2
|
237
|
+
)
|
238
|
+
|
239
|
+
# Using hydrostatic equation to convert dqs_dp to dqs_dz
|
240
|
+
dqs_dz = dqs_dp * air_density * -scipy.constants.g
|
241
|
+
|
242
|
+
return dqs_dz * air_density
|
55
243
|
|
56
244
|
|
57
245
|
def calc_saturation_vapor_pressure(temperature: np.ndarray) -> np.ndarray:
|
@@ -78,46 +266,77 @@ def calc_saturation_vapor_pressure(temperature: np.ndarray) -> np.ndarray:
|
|
78
266
|
) * con.HPA_TO_PA
|
79
267
|
|
80
268
|
|
81
|
-
def
|
82
|
-
"""
|
83
|
-
|
84
|
-
Psychrometric constant relates the partial pressure
|
85
|
-
of water in air to the air temperature.
|
269
|
+
def calc_mixing_ratio(vapor_pressure: np.ndarray, pressure: np.ndarray) -> np.ndarray:
|
270
|
+
"""Calculates mixing ratio from partial vapor pressure and pressure.
|
86
271
|
|
87
272
|
Args:
|
273
|
+
vapor_pressure: Partial pressure of water vapor (Pa).
|
88
274
|
pressure: Atmospheric pressure (Pa).
|
89
275
|
|
90
276
|
Returns:
|
91
|
-
|
277
|
+
Mixing ratio (kg kg-1).
|
92
278
|
|
93
279
|
"""
|
94
|
-
return
|
280
|
+
return con.MW_RATIO * vapor_pressure / (pressure - vapor_pressure)
|
95
281
|
|
96
282
|
|
97
|
-
def
|
98
|
-
|
283
|
+
def calc_air_density(
|
284
|
+
pressure: np.ndarray,
|
285
|
+
temperature: np.ndarray,
|
286
|
+
svp_mixing_ratio: np.ndarray,
|
287
|
+
) -> np.ndarray:
|
288
|
+
"""Calculates air density (kg m-3).
|
99
289
|
|
100
290
|
Args:
|
101
|
-
|
291
|
+
pressure: Pressure (Pa).
|
292
|
+
temperature: Temperature (K).
|
293
|
+
svp_mixing_ratio: Saturation vapor pressure mixing ratio (kg kg-1).
|
102
294
|
|
103
295
|
Returns:
|
104
|
-
|
296
|
+
Air density (kg m-3).
|
297
|
+
|
298
|
+
"""
|
299
|
+
return pressure / (con.RS * temperature * (0.6 * svp_mixing_ratio + 1))
|
300
|
+
|
301
|
+
|
302
|
+
def calc_adiabatic_lwc(lwc_dz: np.ndarray, height: np.ndarray) -> np.ndarray:
|
303
|
+
"""Calculates adiabatic liquid water content (kg m-3).
|
105
304
|
|
106
|
-
|
107
|
-
|
305
|
+
Args:
|
306
|
+
lwc_dz: Liquid water content change rate (kg m-3 m-1) calculated at the
|
307
|
+
base of each cloud and filled to that cloud.
|
308
|
+
height: Height vector (m).
|
309
|
+
|
310
|
+
Returns:
|
311
|
+
Liquid water content (kg m-3).
|
108
312
|
|
109
313
|
"""
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
314
|
+
is_cloud = lwc_dz != 0
|
315
|
+
cloud_indices = utils.cumsumr(is_cloud, axis=1)
|
316
|
+
dz = utils.path_lengths_from_ground(height) * np.ones_like(lwc_dz)
|
317
|
+
dz[cloud_indices < 1] = 0
|
318
|
+
return utils.cumsumr(dz, axis=1) * lwc_dz
|
114
319
|
|
115
320
|
|
116
|
-
def
|
117
|
-
|
118
|
-
|
321
|
+
def normalize_lwc_by_lwp(
|
322
|
+
lwc_adiabatic: np.ndarray, lwp: np.ndarray, height: np.ndarray
|
323
|
+
) -> np.ndarray:
|
324
|
+
"""Finds LWC that would produce measured LWP.
|
119
325
|
|
326
|
+
Calculates LWP-weighted, normalized LWC. This is the measured
|
327
|
+
LWP distributed to liquid cloud pixels according to their
|
328
|
+
theoretical proportion.
|
120
329
|
|
121
|
-
|
122
|
-
|
123
|
-
|
330
|
+
Args:
|
331
|
+
lwc_adiabatic: Theoretical 2D liquid water content (kg m-3).
|
332
|
+
lwp: 1D liquid water path (kg m-2).
|
333
|
+
height: Height vector (m).
|
334
|
+
|
335
|
+
Returns:
|
336
|
+
2D LWP-weighted, scaled LWC (kg m-3) that would produce the observed LWP.
|
337
|
+
|
338
|
+
"""
|
339
|
+
path_lengths = utils.path_lengths_from_ground(height)
|
340
|
+
theoretical_lwp = ma.sum(lwc_adiabatic * path_lengths, axis=1)
|
341
|
+
scaling_factors = lwp / theoretical_lwp
|
342
|
+
return lwc_adiabatic * utils.transpose(scaling_factors)
|
@@ -0,0 +1,31 @@
|
|
1
|
+
from numpy import ma
|
2
|
+
|
3
|
+
from cloudnetpy.categorize.attenuations import (
|
4
|
+
RadarAttenuation,
|
5
|
+
gas_attenuation,
|
6
|
+
liquid_attenuation,
|
7
|
+
melting_attenuation,
|
8
|
+
rain_attenuation,
|
9
|
+
)
|
10
|
+
from cloudnetpy.categorize.containers import ClassificationResult, Observations
|
11
|
+
|
12
|
+
|
13
|
+
def get_attenuations(
|
14
|
+
data: Observations, classification: ClassificationResult
|
15
|
+
) -> RadarAttenuation:
|
16
|
+
rain = rain_attenuation.calc_rain_attenuation(data, classification)
|
17
|
+
gas = gas_attenuation.calc_gas_attenuation(data, classification)
|
18
|
+
liquid = liquid_attenuation.LiquidAttenuation(data, classification).attenuation
|
19
|
+
melting = melting_attenuation.calc_melting_attenuation(data, classification)
|
20
|
+
|
21
|
+
liquid.amount[rain.attenuated] = ma.masked
|
22
|
+
liquid.error[rain.attenuated] = ma.masked
|
23
|
+
liquid.attenuated[rain.attenuated] = False
|
24
|
+
liquid.uncorrected[rain.attenuated] = False
|
25
|
+
|
26
|
+
return RadarAttenuation(
|
27
|
+
gas=gas,
|
28
|
+
liquid=liquid,
|
29
|
+
rain=rain,
|
30
|
+
melting=melting,
|
31
|
+
)
|
@@ -0,0 +1,37 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from typing import Annotated
|
3
|
+
|
4
|
+
import numpy as np
|
5
|
+
from numpy import ma
|
6
|
+
from numpy.typing import NDArray
|
7
|
+
|
8
|
+
from cloudnetpy import constants as con
|
9
|
+
from cloudnetpy.utils import path_lengths_from_ground
|
10
|
+
|
11
|
+
|
12
|
+
@dataclass
|
13
|
+
class Attenuation:
|
14
|
+
amount: Annotated[ma.MaskedArray, "float32"]
|
15
|
+
error: Annotated[ma.MaskedArray, "float32"]
|
16
|
+
attenuated: NDArray[np.bool_]
|
17
|
+
uncorrected: NDArray[np.bool_]
|
18
|
+
|
19
|
+
|
20
|
+
@dataclass
|
21
|
+
class RadarAttenuation:
|
22
|
+
gas: Attenuation
|
23
|
+
liquid: Attenuation
|
24
|
+
rain: Attenuation
|
25
|
+
melting: Attenuation
|
26
|
+
|
27
|
+
|
28
|
+
def calc_two_way_attenuation(
|
29
|
+
height: np.ndarray, specific_attenuation: ma.MaskedArray
|
30
|
+
) -> ma.MaskedArray:
|
31
|
+
"""Calculates two-way attenuation (dB) for given specific attenuation
|
32
|
+
(dB km-1) and height (m).
|
33
|
+
"""
|
34
|
+
path_lengths = path_lengths_from_ground(height) * con.M_TO_KM # km
|
35
|
+
one_way_attenuation = specific_attenuation * path_lengths
|
36
|
+
accumulated_attenuation = ma.cumsum(one_way_attenuation, axis=1)
|
37
|
+
return accumulated_attenuation * con.TWO_WAY
|
@@ -0,0 +1,30 @@
|
|
1
|
+
import numpy as np
|
2
|
+
|
3
|
+
from cloudnetpy.categorize.attenuations import (
|
4
|
+
Attenuation,
|
5
|
+
calc_two_way_attenuation,
|
6
|
+
)
|
7
|
+
from cloudnetpy.categorize.containers import ClassificationResult, Observations
|
8
|
+
|
9
|
+
|
10
|
+
def calc_gas_attenuation(
|
11
|
+
data: Observations, classification: ClassificationResult
|
12
|
+
) -> Attenuation:
|
13
|
+
model_data = data.model.data_dense
|
14
|
+
|
15
|
+
specific_attenuation = model_data["specific_gas_atten"].copy()
|
16
|
+
saturated_attenuation = model_data["specific_saturated_gas_atten"]
|
17
|
+
|
18
|
+
liquid_in_pixel = classification.category_bits.droplet
|
19
|
+
specific_attenuation[liquid_in_pixel] = saturated_attenuation[liquid_in_pixel]
|
20
|
+
|
21
|
+
two_way_attenuation = calc_two_way_attenuation(
|
22
|
+
data.radar.height, specific_attenuation
|
23
|
+
)
|
24
|
+
|
25
|
+
return Attenuation(
|
26
|
+
amount=two_way_attenuation,
|
27
|
+
error=two_way_attenuation * 0.1,
|
28
|
+
attenuated=np.ones_like(two_way_attenuation, dtype=bool),
|
29
|
+
uncorrected=np.zeros_like(two_way_attenuation, dtype=bool),
|
30
|
+
)
|
@@ -0,0 +1,80 @@
|
|
1
|
+
import numpy as np
|
2
|
+
from numpy import ma
|
3
|
+
|
4
|
+
import cloudnetpy.constants as con
|
5
|
+
from cloudnetpy import utils
|
6
|
+
from cloudnetpy.categorize import atmos_utils
|
7
|
+
from cloudnetpy.categorize.attenuations import Attenuation, calc_two_way_attenuation
|
8
|
+
from cloudnetpy.categorize.containers import ClassificationResult, Observations
|
9
|
+
|
10
|
+
|
11
|
+
class LiquidAttenuation:
|
12
|
+
"""Class for calculating liquid attenuation.
|
13
|
+
|
14
|
+
References:
|
15
|
+
Hogan, Robin & Connor, Ewan. (2004). Facilitating cloud radar and lidar
|
16
|
+
algorithms: the Cloudnet Instrument Synergy/Target Categorization product.
|
17
|
+
"""
|
18
|
+
|
19
|
+
def __init__(self, data: Observations, classification: ClassificationResult):
|
20
|
+
self._model = data.model.data_dense
|
21
|
+
self._liquid_in_pixel = classification.category_bits.droplet
|
22
|
+
self._height = data.radar.height
|
23
|
+
|
24
|
+
if data.mwr is not None:
|
25
|
+
lwp = data.mwr.data["lwp"][:]
|
26
|
+
lwp_error = data.mwr.data["lwp_error"][:]
|
27
|
+
else:
|
28
|
+
lwp = ma.masked_all(data.radar.time.size)
|
29
|
+
lwp_error = ma.masked_all(data.radar.time.size)
|
30
|
+
|
31
|
+
lwc_dz = atmos_utils.fill_clouds_with_lwc_dz(
|
32
|
+
self._model["temperature"], self._model["pressure"], self._liquid_in_pixel
|
33
|
+
)
|
34
|
+
|
35
|
+
two_way_attenuation = self._calc_liquid_atten(lwp, lwc_dz)
|
36
|
+
two_way_attenuation_error = self._calc_liquid_atten_err(lwp_error, lwc_dz)
|
37
|
+
|
38
|
+
attenuated = utils.ffill(self._liquid_in_pixel)
|
39
|
+
|
40
|
+
two_way_attenuation[~attenuated] = ma.masked
|
41
|
+
two_way_attenuation_error[~attenuated] = ma.masked
|
42
|
+
|
43
|
+
uncorrected = attenuated & two_way_attenuation.mask
|
44
|
+
|
45
|
+
self.attenuation = Attenuation(
|
46
|
+
amount=two_way_attenuation,
|
47
|
+
error=two_way_attenuation_error,
|
48
|
+
attenuated=attenuated,
|
49
|
+
uncorrected=uncorrected,
|
50
|
+
)
|
51
|
+
|
52
|
+
def _calc_liquid_atten(
|
53
|
+
self, lwp: ma.MaskedArray, lwc_dz: np.ndarray
|
54
|
+
) -> ma.MaskedArray:
|
55
|
+
"""Finds radar liquid attenuation."""
|
56
|
+
lwp = lwp.copy()
|
57
|
+
lwp[lwp < 0] = 0
|
58
|
+
lwc_adiabatic = atmos_utils.calc_adiabatic_lwc(lwc_dz, self._height)
|
59
|
+
lwc_scaled = atmos_utils.normalize_lwc_by_lwp(lwc_adiabatic, lwp, self._height)
|
60
|
+
return self._calc_two_way_attenuation(lwc_scaled)
|
61
|
+
|
62
|
+
def _calc_liquid_atten_err(
|
63
|
+
self, lwp_error: ma.MaskedArray, lwc_dz: np.ndarray
|
64
|
+
) -> ma.MaskedArray:
|
65
|
+
"""Finds radar liquid attenuation error."""
|
66
|
+
lwc_err_scaled = atmos_utils.normalize_lwc_by_lwp(
|
67
|
+
lwc_dz, lwp_error, self._height
|
68
|
+
)
|
69
|
+
return self._calc_two_way_attenuation(lwc_err_scaled)
|
70
|
+
|
71
|
+
def _calc_two_way_attenuation(self, lwc_scaled: np.ndarray) -> ma.MaskedArray:
|
72
|
+
"""Calculates liquid attenuation (dB).
|
73
|
+
|
74
|
+
Args:
|
75
|
+
lwc_scaled: Liquid water content (kg m-3).
|
76
|
+
|
77
|
+
"""
|
78
|
+
specific_attenuation_rate = self._model["specific_liquid_atten"]
|
79
|
+
specific_attenuation = specific_attenuation_rate * lwc_scaled * con.KG_TO_G
|
80
|
+
return calc_two_way_attenuation(self._height, specific_attenuation)
|
@@ -0,0 +1,75 @@
|
|
1
|
+
import numpy as np
|
2
|
+
from numpy import ma
|
3
|
+
|
4
|
+
import cloudnetpy.constants as con
|
5
|
+
from cloudnetpy import utils
|
6
|
+
from cloudnetpy.categorize.attenuations import (
|
7
|
+
Attenuation,
|
8
|
+
)
|
9
|
+
from cloudnetpy.categorize.containers import ClassificationResult, Observations
|
10
|
+
|
11
|
+
|
12
|
+
def calc_melting_attenuation(
|
13
|
+
data: Observations, classification: ClassificationResult
|
14
|
+
) -> Attenuation:
|
15
|
+
shape = classification.category_bits.melting.shape
|
16
|
+
is_rain = classification.is_rain
|
17
|
+
|
18
|
+
affected_region = classification.category_bits.freezing.copy()
|
19
|
+
|
20
|
+
if data.disdrometer is None:
|
21
|
+
affected_region[~is_rain, :] = False
|
22
|
+
above_melting = utils.ffill(classification.category_bits.melting)
|
23
|
+
affected_region[~above_melting] = False
|
24
|
+
return Attenuation(
|
25
|
+
amount=ma.masked_all(shape),
|
26
|
+
error=ma.masked_all(shape),
|
27
|
+
attenuated=affected_region,
|
28
|
+
uncorrected=affected_region,
|
29
|
+
)
|
30
|
+
|
31
|
+
rainfall_rate = data.disdrometer.data["rainfall_rate"][:]
|
32
|
+
frequency = data.radar.radar_frequency
|
33
|
+
|
34
|
+
attenuation_array = _calc_melting_attenuation(rainfall_rate, frequency)
|
35
|
+
|
36
|
+
amount = affected_region * utils.transpose(attenuation_array)
|
37
|
+
|
38
|
+
affected_region[amount == 0] = False
|
39
|
+
|
40
|
+
amount[amount == 0] = ma.masked
|
41
|
+
|
42
|
+
band = utils.get_wl_band(data.radar.radar_frequency)
|
43
|
+
error_factor = 0.2 if band == 0 else 0.1
|
44
|
+
|
45
|
+
error = amount * error_factor
|
46
|
+
error[~affected_region] = ma.masked
|
47
|
+
|
48
|
+
return Attenuation(
|
49
|
+
amount=amount,
|
50
|
+
error=error,
|
51
|
+
attenuated=affected_region,
|
52
|
+
uncorrected=affected_region & amount.mask,
|
53
|
+
)
|
54
|
+
|
55
|
+
|
56
|
+
def _calc_melting_attenuation(
|
57
|
+
rainfall_rate: np.ndarray, frequency: float
|
58
|
+
) -> np.ndarray:
|
59
|
+
"""Calculates total attenuation due to melting layer (dB).
|
60
|
+
|
61
|
+
References:
|
62
|
+
Li, H., & Moisseev, D. (2019). Melting layer attenuation
|
63
|
+
at Ka- and W-bands as derived from multifrequency radar
|
64
|
+
Doppler spectra observations. Journal of Geophysical
|
65
|
+
Research: Atmospheres, 124, 9520–9533. https://doi.org/10.1029/2019JD030316
|
66
|
+
|
67
|
+
"""
|
68
|
+
if frequency > 34 and frequency < 37:
|
69
|
+
a, b = 0.97, 0.61
|
70
|
+
elif frequency > 93 and frequency < 96:
|
71
|
+
a, b = 2.9, 0.42
|
72
|
+
else:
|
73
|
+
msg = "Radar frequency not supported"
|
74
|
+
raise ValueError(msg)
|
75
|
+
return a * (rainfall_rate * con.M_S_TO_MM_H) ** b
|