cloudnetpy 1.65.8__py3-none-any.whl → 1.66.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. cloudnetpy/categorize/__init__.py +0 -1
  2. cloudnetpy/categorize/atmos_utils.py +278 -59
  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 +80 -0
  7. cloudnetpy/categorize/attenuations/melting_attenuation.py +75 -0
  8. cloudnetpy/categorize/attenuations/rain_attenuation.py +84 -0
  9. cloudnetpy/categorize/categorize.py +140 -81
  10. cloudnetpy/categorize/classify.py +92 -128
  11. cloudnetpy/categorize/containers.py +45 -31
  12. cloudnetpy/categorize/droplet.py +2 -2
  13. cloudnetpy/categorize/falling.py +3 -3
  14. cloudnetpy/categorize/freezing.py +2 -2
  15. cloudnetpy/categorize/itu.py +243 -0
  16. cloudnetpy/categorize/melting.py +0 -3
  17. cloudnetpy/categorize/model.py +31 -14
  18. cloudnetpy/categorize/radar.py +28 -12
  19. cloudnetpy/constants.py +3 -6
  20. cloudnetpy/model_evaluation/file_handler.py +2 -2
  21. cloudnetpy/model_evaluation/products/observation_products.py +8 -8
  22. cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +5 -2
  23. cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +11 -11
  24. cloudnetpy/output.py +46 -26
  25. cloudnetpy/plotting/plot_meta.py +8 -2
  26. cloudnetpy/plotting/plotting.py +28 -6
  27. cloudnetpy/products/classification.py +39 -34
  28. cloudnetpy/products/der.py +15 -13
  29. cloudnetpy/products/drizzle_tools.py +22 -21
  30. cloudnetpy/products/ier.py +8 -45
  31. cloudnetpy/products/iwc.py +7 -22
  32. cloudnetpy/products/lwc.py +14 -15
  33. cloudnetpy/products/mwr_tools.py +15 -2
  34. cloudnetpy/products/product_tools.py +121 -119
  35. cloudnetpy/utils.py +4 -0
  36. cloudnetpy/version.py +2 -2
  37. {cloudnetpy-1.65.8.dist-info → cloudnetpy-1.66.0.dist-info}/METADATA +1 -1
  38. {cloudnetpy-1.65.8.dist-info → cloudnetpy-1.66.0.dist-info}/RECORD +41 -35
  39. {cloudnetpy-1.65.8.dist-info → cloudnetpy-1.66.0.dist-info}/WHEEL +1 -1
  40. cloudnetpy/categorize/atmos.py +0 -376
  41. {cloudnetpy-1.65.8.dist-info → cloudnetpy-1.66.0.dist-info}/LICENSE +0 -0
  42. {cloudnetpy-1.65.8.dist-info → cloudnetpy-1.66.0.dist-info}/top_level.txt +0 -0
@@ -1,2 +1 @@
1
1
  from .categorize import generate_categorize
2
- from .radar import Radar
@@ -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
- """Returns wet bulb temperature.
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
- Returns wet bulb temperature for given temperature,
11
- pressure and relative humidity. Algorithm is based on a Taylor
12
- expansion of a simple expression for the saturated vapour pressure.
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
- model_data: Model variables `temperature`, `pressure`, `rh`.
72
+ pressure: Pressure (Pa)
73
+ specific_humidity: Specific humidity (1)
16
74
 
17
75
  Returns:
18
- Wet bulb temperature (K).
76
+ Vapor pressure (Pa)
19
77
 
20
78
  References:
21
- J. Sullivan and L. D. Sanders: Method for obtaining wet-bulb
22
- temperatures by modifying the psychrometric formula.
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
- def _screen_rh() -> np.ndarray:
27
- rh = model_data["rh"]
28
- rh_min = 1e-5
29
- rh[rh < rh_min] = rh_min
30
- return rh
31
-
32
- def _vapor_derivatives() -> tuple:
33
- m = 17.269
34
- tn = 35.86
35
- f1 = m * (tn - con.T0)
36
- f2 = dew_point - tn
37
- first = -vapor_pressure * f1 / (f2**2)
38
- second = vapor_pressure * ((f1 / (f2**2)) ** 2 + 2 * f1 / (f2**3))
39
- return first, second
40
-
41
- relative_humidity = _screen_rh()
42
- saturation_pressure = calc_saturation_vapor_pressure(model_data["temperature"])
43
- vapor_pressure = saturation_pressure * relative_humidity
44
- dew_point = calc_dew_point_temperature(vapor_pressure)
45
- psychrometric_constant = calc_psychrometric_constant(model_data["pressure"])
46
- first_der, second_der = _vapor_derivatives()
47
- a = 0.5 * second_der
48
- b = first_der + psychrometric_constant - dew_point * second_der
49
- c = (
50
- -model_data["temperature"] * psychrometric_constant
51
- - dew_point * first_der
52
- + 0.5 * dew_point**2 * second_der
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
- return (-b + ma.sqrt(b * b - 4 * a * c)) / (2 * a)
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 calc_psychrometric_constant(pressure: np.ndarray) -> np.ndarray:
82
- """Returns psychrometric constant.
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
- Psychrometric constant value (Pa K-1)
277
+ Mixing ratio (kg kg-1).
92
278
 
93
279
  """
94
- return pressure * con.SPECIFIC_HEAT / (con.LATENT_HEAT * con.MW_RATIO)
280
+ return con.MW_RATIO * vapor_pressure / (pressure - vapor_pressure)
95
281
 
96
282
 
97
- def calc_dew_point_temperature(vapor_pressure: np.ndarray) -> np.ndarray:
98
- """Returns dew point temperature.
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
- vapor_pressure: Water vapor pressure (Pa).
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
- Dew point temperature (K).
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
- Notes:
107
- Method from Vaisala's white paper: "Humidity conversion formulas".
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
- vaisala_parameters_over_water = (6.116441, 7.591386, 240.7263)
111
- a, m, tn = vaisala_parameters_over_water
112
- dew_point_celsius = tn / ((m / np.log10(vapor_pressure * con.PA_TO_HPA / a)) - 1)
113
- return c2k(dew_point_celsius)
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 c2k(temp: np.ndarray) -> np.ndarray:
117
- """Converts Celsius to Kelvins."""
118
- return ma.array(temp) + 273.15
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
- def k2c(temp: np.ndarray) -> np.ndarray:
122
- """Converts Kelvins to Celsius."""
123
- return ma.array(temp) - 273.15
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