cloudnetpy 1.49.9__py3-none-any.whl → 1.87.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. cloudnetpy/categorize/__init__.py +1 -2
  2. cloudnetpy/categorize/atmos_utils.py +297 -67
  3. cloudnetpy/categorize/attenuation.py +31 -0
  4. cloudnetpy/categorize/attenuations/__init__.py +37 -0
  5. cloudnetpy/categorize/attenuations/gas_attenuation.py +30 -0
  6. cloudnetpy/categorize/attenuations/liquid_attenuation.py +84 -0
  7. cloudnetpy/categorize/attenuations/melting_attenuation.py +78 -0
  8. cloudnetpy/categorize/attenuations/rain_attenuation.py +84 -0
  9. cloudnetpy/categorize/categorize.py +332 -156
  10. cloudnetpy/categorize/classify.py +127 -125
  11. cloudnetpy/categorize/containers.py +107 -76
  12. cloudnetpy/categorize/disdrometer.py +40 -0
  13. cloudnetpy/categorize/droplet.py +23 -21
  14. cloudnetpy/categorize/falling.py +53 -24
  15. cloudnetpy/categorize/freezing.py +25 -12
  16. cloudnetpy/categorize/insects.py +35 -23
  17. cloudnetpy/categorize/itu.py +243 -0
  18. cloudnetpy/categorize/lidar.py +36 -41
  19. cloudnetpy/categorize/melting.py +34 -26
  20. cloudnetpy/categorize/model.py +84 -37
  21. cloudnetpy/categorize/mwr.py +18 -14
  22. cloudnetpy/categorize/radar.py +215 -102
  23. cloudnetpy/cli.py +578 -0
  24. cloudnetpy/cloudnetarray.py +43 -89
  25. cloudnetpy/concat_lib.py +218 -78
  26. cloudnetpy/constants.py +28 -10
  27. cloudnetpy/datasource.py +61 -86
  28. cloudnetpy/exceptions.py +49 -20
  29. cloudnetpy/instruments/__init__.py +5 -0
  30. cloudnetpy/instruments/basta.py +29 -12
  31. cloudnetpy/instruments/bowtie.py +135 -0
  32. cloudnetpy/instruments/ceilo.py +138 -115
  33. cloudnetpy/instruments/ceilometer.py +164 -80
  34. cloudnetpy/instruments/cl61d.py +21 -5
  35. cloudnetpy/instruments/cloudnet_instrument.py +74 -36
  36. cloudnetpy/instruments/copernicus.py +108 -30
  37. cloudnetpy/instruments/da10.py +54 -0
  38. cloudnetpy/instruments/disdrometer/common.py +126 -223
  39. cloudnetpy/instruments/disdrometer/parsivel.py +453 -94
  40. cloudnetpy/instruments/disdrometer/thies.py +254 -87
  41. cloudnetpy/instruments/fd12p.py +201 -0
  42. cloudnetpy/instruments/galileo.py +65 -23
  43. cloudnetpy/instruments/hatpro.py +123 -49
  44. cloudnetpy/instruments/instruments.py +113 -1
  45. cloudnetpy/instruments/lufft.py +39 -17
  46. cloudnetpy/instruments/mira.py +268 -61
  47. cloudnetpy/instruments/mrr.py +187 -0
  48. cloudnetpy/instruments/nc_lidar.py +19 -8
  49. cloudnetpy/instruments/nc_radar.py +109 -55
  50. cloudnetpy/instruments/pollyxt.py +135 -51
  51. cloudnetpy/instruments/radiometrics.py +313 -59
  52. cloudnetpy/instruments/rain_e_h3.py +171 -0
  53. cloudnetpy/instruments/rpg.py +321 -189
  54. cloudnetpy/instruments/rpg_reader.py +74 -40
  55. cloudnetpy/instruments/toa5.py +49 -0
  56. cloudnetpy/instruments/vaisala.py +95 -343
  57. cloudnetpy/instruments/weather_station.py +774 -105
  58. cloudnetpy/metadata.py +90 -19
  59. cloudnetpy/model_evaluation/file_handler.py +55 -52
  60. cloudnetpy/model_evaluation/metadata.py +46 -20
  61. cloudnetpy/model_evaluation/model_metadata.py +1 -1
  62. cloudnetpy/model_evaluation/plotting/plot_tools.py +32 -37
  63. cloudnetpy/model_evaluation/plotting/plotting.py +327 -117
  64. cloudnetpy/model_evaluation/products/advance_methods.py +92 -83
  65. cloudnetpy/model_evaluation/products/grid_methods.py +88 -63
  66. cloudnetpy/model_evaluation/products/model_products.py +43 -35
  67. cloudnetpy/model_evaluation/products/observation_products.py +41 -35
  68. cloudnetpy/model_evaluation/products/product_resampling.py +17 -7
  69. cloudnetpy/model_evaluation/products/tools.py +29 -20
  70. cloudnetpy/model_evaluation/statistics/statistical_methods.py +30 -20
  71. cloudnetpy/model_evaluation/tests/e2e/conftest.py +3 -3
  72. cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +9 -5
  73. cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +15 -14
  74. cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +9 -5
  75. cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +15 -14
  76. cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +9 -5
  77. cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +15 -14
  78. cloudnetpy/model_evaluation/tests/unit/conftest.py +42 -41
  79. cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +41 -48
  80. cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +216 -194
  81. cloudnetpy/model_evaluation/tests/unit/test_model_products.py +23 -21
  82. cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +37 -38
  83. cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +43 -40
  84. cloudnetpy/model_evaluation/tests/unit/test_plotting.py +30 -36
  85. cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +68 -31
  86. cloudnetpy/model_evaluation/tests/unit/test_tools.py +33 -26
  87. cloudnetpy/model_evaluation/utils.py +2 -1
  88. cloudnetpy/output.py +170 -111
  89. cloudnetpy/plotting/__init__.py +2 -1
  90. cloudnetpy/plotting/plot_meta.py +562 -822
  91. cloudnetpy/plotting/plotting.py +1142 -704
  92. cloudnetpy/products/__init__.py +1 -0
  93. cloudnetpy/products/classification.py +370 -88
  94. cloudnetpy/products/der.py +85 -55
  95. cloudnetpy/products/drizzle.py +77 -34
  96. cloudnetpy/products/drizzle_error.py +15 -11
  97. cloudnetpy/products/drizzle_tools.py +79 -59
  98. cloudnetpy/products/epsilon.py +211 -0
  99. cloudnetpy/products/ier.py +27 -50
  100. cloudnetpy/products/iwc.py +55 -48
  101. cloudnetpy/products/lwc.py +96 -70
  102. cloudnetpy/products/mwr_tools.py +186 -0
  103. cloudnetpy/products/product_tools.py +170 -128
  104. cloudnetpy/utils.py +455 -240
  105. cloudnetpy/version.py +2 -2
  106. {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/METADATA +44 -40
  107. cloudnetpy-1.87.3.dist-info/RECORD +127 -0
  108. {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/WHEEL +1 -1
  109. cloudnetpy-1.87.3.dist-info/entry_points.txt +2 -0
  110. docs/source/conf.py +2 -2
  111. cloudnetpy/categorize/atmos.py +0 -361
  112. cloudnetpy/products/mwr_multi.py +0 -68
  113. cloudnetpy/products/mwr_single.py +0 -75
  114. cloudnetpy-1.49.9.dist-info/RECORD +0 -112
  115. {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info/licenses}/LICENSE +0 -0
  116. {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/top_level.txt +0 -0
@@ -1,2 +1 @@
1
- from .categorize import generate_categorize
2
- from .radar import Radar
1
+ from .categorize import CategorizeInput, generate_categorize
@@ -1,63 +1,250 @@
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
10
+
11
+
12
+ def calc_wet_bulb_temperature(model_data: dict) -> npt.NDArray:
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).
5
20
 
6
- HPA_TO_P = 100
7
- P_TO_HPA = 0.01
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: npt.NDArray) -> npt.NDArray:
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
+ )
8
61
 
62
+ return c2k(tw)
9
63
 
10
- def calc_wet_bulb_temperature(model_data: dict) -> np.ndarray:
11
- """Returns wet bulb temperature.
12
64
 
13
- Returns wet bulb temperature for given temperature,
14
- pressure and relative humidity. Algorithm is based on a Taylor
15
- 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.
16
70
 
17
71
  Args:
18
- model_data: Model variables `temperature`, `pressure`, `rh`.
72
+ pressure: Pressure (Pa)
73
+ specific_humidity: Specific humidity (1)
19
74
 
20
75
  Returns:
21
- Wet bulb temperature (K).
76
+ Vapor pressure (Pa)
22
77
 
23
78
  References:
24
- J. Sullivan and L. D. Sanders: Method for obtaining wet-bulb
25
- 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: npt.NDArray) -> npt.NDArray:
90
+ """Converts Celsius to Kelvins."""
91
+ return ma.array(temp) + 273.15
92
+
93
+
94
+ def k2c(temp: npt.NDArray) -> npt.NDArray:
95
+ """Converts Kelvins to Celsius."""
96
+ return ma.array(temp) - 273.15
97
+
98
+
99
+ def find_cloud_bases(array: npt.NDArray) -> npt.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: npt.NDArray) -> npt.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.
26
122
 
27
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: npt.NDArray,
131
+ height: npt.NDArray,
132
+ ) -> ma.MaskedArray:
133
+ """Finds altitudes of cloud bases."""
134
+ cloud_heights = cloud_mask * height
135
+ return _find_lowest_heights(cloud_heights)
136
+
137
+
138
+ def find_highest_cloud_tops(
139
+ cloud_mask: npt.NDArray,
140
+ height: npt.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: npt.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: npt.NDArray, pressure: npt.NDArray, is_liquid: npt.NDArray
156
+ ) -> npt.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.
28
167
 
29
- def _screen_rh() -> np.ndarray:
30
- rh = model_data["rh"]
31
- rh_min = 1e-5
32
- rh[rh < rh_min] = rh_min
33
- return rh
34
-
35
- def _vapor_derivatives() -> tuple:
36
- m = 17.269
37
- tn = 35.86
38
- f1 = m * (tn - con.T0)
39
- f2 = dew_point - tn
40
- first = -vapor_pressure * f1 / (f2**2)
41
- second = vapor_pressure * ((f1 / (f2**2)) ** 2 + 2 * f1 / (f2**3))
42
- return first, second
43
-
44
- relative_humidity = _screen_rh()
45
- saturation_pressure = calc_saturation_vapor_pressure(model_data["temperature"])
46
- vapor_pressure = saturation_pressure * relative_humidity
47
- dew_point = calc_dew_point_temperature(vapor_pressure)
48
- psychrometric_constant = calc_psychrometric_constant(model_data["pressure"])
49
- first_der, second_der = _vapor_derivatives()
50
- a = 0.5 * second_der
51
- b = first_der + psychrometric_constant - dew_point * second_der
52
- c = (
53
- -model_data["temperature"] * psychrometric_constant
54
- - dew_point * first_der
55
- + 0.5 * dew_point**2 * second_der
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: npt.NDArray,
177
+ pressure: npt.NDArray,
178
+ is_liquid: npt.NDArray,
179
+ ) -> npt.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],
196
+ )
197
+
198
+ return lwc_dz
199
+
200
+
201
+ def calc_lwc_change_rate(
202
+ temperature: npt.NDArray, pressure: npt.NDArray
203
+ ) -> npt.NDArray:
204
+ """Returns rate of change of condensable water (LWC).
205
+
206
+ Calculates the theoretical adiabatic rate of increase of LWC
207
+ with height, given the cloud base temperature and pressure.
208
+
209
+ Args:
210
+ temperature: Temperature of cloud base (K).
211
+ pressure: Pressure of cloud base (Pa).
212
+
213
+ Returns:
214
+ dlwc/dz (kg m-3 m-1)
215
+
216
+ References:
217
+ Brenguier, 1991, https://doi.org/10.1175/1520-0469(1991)048<0264:POTCPA>2.0.CO;2
218
+
219
+ """
220
+ svp = calc_saturation_vapor_pressure(temperature)
221
+ svp_mixing_ratio = calc_mixing_ratio(svp, pressure)
222
+ air_density = calc_air_density(pressure, temperature, svp_mixing_ratio)
223
+
224
+ e = 0.622
225
+ Cp = 1004 # J kg-1 K-1
226
+ Lv = 2.45e6 # J kg-1 = Pa m3 kg-1
227
+ qs = svp_mixing_ratio # kg kg-1
228
+ pa = air_density # kg m-3
229
+ es = svp # Pa
230
+ P = pressure # Pa
231
+ T = temperature # K
232
+
233
+ # See Appendix B in Brenguier (1991) for the derivation of the following equation
234
+ dqs_dp = (
235
+ -(1 - (Cp * T) / (e * Lv))
236
+ * (((Cp * T) / (e * Lv)) + ((Lv * qs * pa) / (P - es))) ** -1
237
+ * (e * es)
238
+ * (P - es) ** -2
56
239
  )
57
- return (-b + ma.sqrt(b * b - 4 * a * c)) / (2 * a)
240
+
241
+ # Using hydrostatic equation to convert dqs_dp to dqs_dz
242
+ dqs_dz = dqs_dp * air_density * -scipy.constants.g
243
+
244
+ return dqs_dz * air_density
58
245
 
59
246
 
60
- def calc_saturation_vapor_pressure(temperature: np.ndarray) -> np.ndarray:
247
+ def calc_saturation_vapor_pressure(temperature: npt.NDArray) -> npt.NDArray:
61
248
  """Goff-Gratch formula for saturation vapor pressure over water adopted by WMO.
62
249
 
63
250
  Args:
@@ -78,54 +265,97 @@ def calc_saturation_vapor_pressure(temperature: np.ndarray) -> np.ndarray:
78
265
  + 0.42873e-3 * (10 ** (4.76955 * (1 - ratio)) - 1)
79
266
  + 0.78614
80
267
  )
81
- ) * HPA_TO_P
268
+ ) * con.HPA_TO_PA
82
269
 
83
270
 
84
- def calc_psychrometric_constant(pressure: np.ndarray) -> np.ndarray:
85
- """Returns psychrometric constant.
86
-
87
- Psychrometric constant relates the partial pressure
88
- of water in air to the air temperature.
271
+ def calc_mixing_ratio(
272
+ vapor_pressure: npt.NDArray, pressure: npt.NDArray
273
+ ) -> npt.NDArray:
274
+ """Calculates mixing ratio from partial vapor pressure and pressure.
89
275
 
90
276
  Args:
277
+ vapor_pressure: Partial pressure of water vapor (Pa).
91
278
  pressure: Atmospheric pressure (Pa).
92
279
 
93
280
  Returns:
94
- Psychrometric constant value (Pa K-1)
281
+ Mixing ratio (kg kg-1).
95
282
 
96
283
  """
97
- return pressure * con.SPECIFIC_HEAT / (con.LATENT_HEAT * con.MW_RATIO)
284
+ return con.MW_RATIO * vapor_pressure / (pressure - vapor_pressure)
98
285
 
99
286
 
100
- def calc_dew_point_temperature(vapor_pressure: np.ndarray) -> np.ndarray:
101
- """Returns dew point temperature.
287
+ def calc_air_density(
288
+ pressure: npt.NDArray,
289
+ temperature: npt.NDArray,
290
+ svp_mixing_ratio: npt.NDArray,
291
+ ) -> npt.NDArray:
292
+ """Calculates air density (kg m-3).
102
293
 
103
294
  Args:
104
- vapor_pressure: Water vapor pressure (Pa).
295
+ pressure: Pressure (Pa).
296
+ temperature: Temperature (K).
297
+ svp_mixing_ratio: Saturation vapor pressure mixing ratio (kg kg-1).
105
298
 
106
299
  Returns:
107
- Dew point temperature (K).
300
+ Air density (kg m-3).
301
+
302
+ """
303
+ return pressure / (con.RS * temperature * (0.6 * svp_mixing_ratio + 1))
304
+
305
+
306
+ def calc_adiabatic_lwc(lwc_dz: npt.NDArray, height_agl: npt.NDArray) -> npt.NDArray:
307
+ """Calculates adiabatic liquid water content (kg m-3).
108
308
 
109
- Notes:
110
- Method from Vaisala's white paper: "Humidity conversion formulas".
309
+ Args:
310
+ lwc_dz: Liquid water content change rate (kg m-3 m-1) calculated at the
311
+ base of each cloud and filled to that cloud.
312
+ height_agl: Height above ground level vector (m).
313
+
314
+ Returns:
315
+ Liquid water content (kg m-3).
111
316
 
112
317
  """
113
- vaisala_parameters_over_water = (6.116441, 7.591386, 240.7263)
114
- a, m, tn = vaisala_parameters_over_water
115
- dew_point_celsius = tn / ((m / np.log10(vapor_pressure * P_TO_HPA / a)) - 1)
116
- return c2k(dew_point_celsius)
318
+ is_cloud = lwc_dz != 0
319
+ cloud_indices = utils.cumsumr(is_cloud, axis=1)
320
+ dz = utils.path_lengths_from_ground(height_agl) * np.ones_like(lwc_dz)
321
+ dz[cloud_indices < 1] = 0
322
+ return utils.cumsumr(dz, axis=1) * lwc_dz
117
323
 
118
324
 
119
- def c2k(temp: np.ndarray) -> np.ndarray:
120
- """Converts Celsius to Kelvins."""
121
- return ma.array(temp) + 273.15
325
+ def normalize_lwc_by_lwp(
326
+ lwc_adiabatic: npt.NDArray, lwp: npt.NDArray, height_agl: npt.NDArray
327
+ ) -> npt.NDArray:
328
+ """Finds LWC that would produce measured LWP.
122
329
 
330
+ Calculates LWP-weighted, normalized LWC. This is the measured
331
+ LWP distributed to liquid cloud pixels according to their
332
+ theoretical proportion.
123
333
 
124
- def k2c(temp: np.ndarray) -> np.ndarray:
125
- """Converts Kelvins to Celsius."""
126
- return ma.array(temp) - 273.15
334
+ Args:
335
+ lwc_adiabatic: Theoretical 2D liquid water content (kg m-3).
336
+ lwp: 1D liquid water path (kg m-2).
337
+ height_agl: Height above ground level vector (m).
338
+
339
+ Returns:
340
+ 2D LWP-weighted, scaled LWC (kg m-3) that would produce the observed LWP.
341
+
342
+ """
343
+ path_lengths = utils.path_lengths_from_ground(height_agl)
344
+ theoretical_lwp = ma.sum(lwc_adiabatic * path_lengths, axis=1)
345
+ scaling_factors = lwp / theoretical_lwp
346
+ return lwc_adiabatic * utils.transpose(scaling_factors)
127
347
 
128
348
 
129
- def mmh2ms(data: np.ndarray) -> np.ndarray:
130
- """Converts mm h-1 to m s-1"""
131
- return data / 3600_000
349
+ def calc_altitude(temperature: float, pressure: float) -> float:
350
+ """Calculate altitude (m) based on observed pressure (Pa) and temperature (K)
351
+ using the International Standard Atmosphere (ISA) model.
352
+
353
+ Args:
354
+ temperature: Observed temperature (K).
355
+ pressure: Observed atmospheric pressure (Pa).
356
+
357
+ Returns:
358
+ Altitude (m).
359
+ """
360
+ L = 0.0065 # Temperature lapse rate (K/m)
361
+ return (temperature / L) * (1 - (pressure / con.P0) ** (con.RS * L / con.G))
@@ -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
+ import numpy.typing as npt
6
+ from numpy import ma
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: npt.NDArray[np.bool_]
17
+ uncorrected: npt.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_agl: npt.NDArray, specific_attenuation: ma.MaskedArray
30
+ ) -> ma.MaskedArray:
31
+ """Calculates two-way attenuation (dB) for given specific attenuation
32
+ (dB km-1) and height above ground level (m).
33
+ """
34
+ path_lengths = path_lengths_from_ground(height_agl) * 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_agl, 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,84 @@
1
+ import numpy.typing as npt
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__(
20
+ self, data: Observations, classification: ClassificationResult
21
+ ) -> None:
22
+ self._model = data.model.data_dense
23
+ self._liquid_in_pixel = classification.category_bits.droplet
24
+ self._height_agl = data.radar.height_agl
25
+
26
+ if data.mwr is not None:
27
+ lwp = data.mwr.data["lwp"][:]
28
+ lwp_error = data.mwr.data["lwp_error"][:]
29
+ else:
30
+ lwp = ma.masked_all(data.radar.time.size)
31
+ lwp_error = ma.masked_all(data.radar.time.size)
32
+
33
+ lwc_dz = atmos_utils.fill_clouds_with_lwc_dz(
34
+ self._model["temperature"], self._model["pressure"], self._liquid_in_pixel
35
+ )
36
+
37
+ two_way_attenuation = self._calc_liquid_atten(lwp, lwc_dz)
38
+ two_way_attenuation_error = self._calc_liquid_atten_err(lwp_error, lwc_dz)
39
+
40
+ attenuated = utils.ffill(self._liquid_in_pixel)
41
+
42
+ two_way_attenuation[~attenuated] = ma.masked
43
+ two_way_attenuation_error[~attenuated] = ma.masked
44
+
45
+ uncorrected = attenuated & two_way_attenuation.mask
46
+
47
+ self.attenuation = Attenuation(
48
+ amount=two_way_attenuation,
49
+ error=two_way_attenuation_error,
50
+ attenuated=attenuated,
51
+ uncorrected=uncorrected,
52
+ )
53
+
54
+ def _calc_liquid_atten(
55
+ self, lwp: ma.MaskedArray, lwc_dz: npt.NDArray
56
+ ) -> ma.MaskedArray:
57
+ """Finds radar liquid attenuation."""
58
+ lwp = lwp.copy()
59
+ lwp[lwp < 0] = 0
60
+ lwc_adiabatic = atmos_utils.calc_adiabatic_lwc(lwc_dz, self._height_agl)
61
+ lwc_scaled = atmos_utils.normalize_lwc_by_lwp(
62
+ lwc_adiabatic, lwp, self._height_agl
63
+ )
64
+ return self._calc_two_way_attenuation(lwc_scaled)
65
+
66
+ def _calc_liquid_atten_err(
67
+ self, lwp_error: ma.MaskedArray, lwc_dz: npt.NDArray
68
+ ) -> ma.MaskedArray:
69
+ """Finds radar liquid attenuation error."""
70
+ lwc_err_scaled = atmos_utils.normalize_lwc_by_lwp(
71
+ lwc_dz, lwp_error, self._height_agl
72
+ )
73
+ return self._calc_two_way_attenuation(lwc_err_scaled)
74
+
75
+ def _calc_two_way_attenuation(self, lwc_scaled: npt.NDArray) -> ma.MaskedArray:
76
+ """Calculates liquid attenuation (dB).
77
+
78
+ Args:
79
+ lwc_scaled: Liquid water content (kg m-3).
80
+
81
+ """
82
+ specific_attenuation_rate = self._model["specific_liquid_atten"]
83
+ specific_attenuation = specific_attenuation_rate * lwc_scaled * con.KG_TO_G
84
+ return calc_two_way_attenuation(self._height_agl, specific_attenuation)
@@ -0,0 +1,78 @@
1
+ import numpy.typing as npt
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
+ no_rain = classification.is_rain == 0
17
+
18
+ affected_region = classification.category_bits.freezing.copy()
19
+
20
+ if data.disdrometer is None:
21
+ above_melting = utils.ffill(classification.category_bits.melting)
22
+ affected_region[~above_melting] = False
23
+ affected_region[no_rain, :] = 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
+ rainfall_rate = ma.where(no_rain, 0, rainfall_rate)
33
+
34
+ frequency = data.radar.radar_frequency
35
+
36
+ attenuation_array = _calc_melting_attenuation(rainfall_rate, frequency)
37
+
38
+ amount = affected_region * utils.transpose(attenuation_array)
39
+
40
+ no_attenuation = amount == 0
41
+
42
+ affected_region[no_attenuation] = False
43
+ amount[no_attenuation] = ma.masked
44
+
45
+ band = utils.get_wl_band(data.radar.radar_frequency)
46
+ error_factor = {"Ka": 0.2, "W": 0.1}[band]
47
+
48
+ error = amount * error_factor
49
+
50
+ return Attenuation(
51
+ amount=amount,
52
+ error=error,
53
+ attenuated=affected_region,
54
+ uncorrected=affected_region & amount.mask,
55
+ )
56
+
57
+
58
+ def _calc_melting_attenuation(
59
+ rainfall_rate: npt.NDArray, frequency: float
60
+ ) -> npt.NDArray:
61
+ """Calculates total attenuation due to melting layer (dB).
62
+
63
+ References:
64
+ Li, H., & Moisseev, D. (2019). Melting layer attenuation
65
+ at Ka- and W-bands as derived from multifrequency radar
66
+ Doppler spectra observations. Journal of Geophysical
67
+ Research: Atmospheres, 124, 9520–9533. https://doi.org/10.1029/2019JD030316
68
+
69
+ """
70
+ band = utils.get_wl_band(frequency)
71
+ if band == "Ka":
72
+ a, b = 0.97, 0.61
73
+ elif band == "W":
74
+ a, b = 2.9, 0.42
75
+ else:
76
+ msg = "Radar frequency not supported"
77
+ raise ValueError(msg)
78
+ return a * (rainfall_rate * con.M_S_TO_MM_H) ** b