cloudnetpy 1.65.7__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 +31 -8
  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.7.dist-info → cloudnetpy-1.66.0.dist-info}/METADATA +1 -1
  38. {cloudnetpy-1.65.7.dist-info → cloudnetpy-1.66.0.dist-info}/RECORD +41 -35
  39. {cloudnetpy-1.65.7.dist-info → cloudnetpy-1.66.0.dist-info}/WHEEL +1 -1
  40. cloudnetpy/categorize/atmos.py +0 -376
  41. {cloudnetpy-1.65.7.dist-info → cloudnetpy-1.66.0.dist-info}/LICENSE +0 -0
  42. {cloudnetpy-1.65.7.dist-info → cloudnetpy-1.66.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,243 @@
1
+ import numpy as np
2
+ import numpy.typing as npt
3
+
4
+ import cloudnetpy.constants as con
5
+
6
+
7
+ def calc_liquid_specific_attenuation(
8
+ temperature: npt.NDArray, frequency: float
9
+ ) -> npt.NDArray:
10
+ """Calculate cloud liquid water specific attenuation coefficient for
11
+ frequency up to 200 GHz.
12
+
13
+ Args:
14
+ temperature: Temperature (K)
15
+ frequency: Frequency (GHz)
16
+
17
+ Returns:
18
+ Cloud liquid water specific attenuation coefficient ((dB km-1)/(g m-3))
19
+
20
+ References:
21
+ ITU-R P.840-9: Attenuation due to clouds and fog.
22
+ https://www.itu.int/dms_pubrec/itu-r/rec/p/R-REC-P.840-9-202308-I!!PDF-E.pdf
23
+ """
24
+ theta1 = 300 / temperature - 1
25
+ e0 = 77.66 + 103.3 * theta1
26
+ e1 = 0.0671 * e0
27
+ e2 = 3.52
28
+ fp = 20.20 - 146 * theta1 + 316 * theta1**2
29
+ fs = 39.8 * fp
30
+ ei = frequency * (e0 - e1) / (fp * (1 + (frequency / fp) ** 2)) + frequency * (
31
+ e1 - e2
32
+ ) / (fs * (1 + (frequency / fs) ** 2))
33
+ er = (
34
+ (e0 - e1) / (1 + (frequency / fp) ** 2)
35
+ + (e1 - e2) / (1 + (frequency / fs) ** 2)
36
+ + e2
37
+ )
38
+ eta = (2 + er) / ei
39
+ return 0.819 * frequency / (ei * (1 + eta**2))
40
+
41
+
42
+ def calc_gas_specific_attenuation(
43
+ pressure: npt.NDArray,
44
+ vapor_pressure: npt.NDArray,
45
+ temperature: npt.NDArray,
46
+ frequency: float,
47
+ ) -> npt.NDArray:
48
+ """Calculate specific attenuation due to dry air and water vapor for
49
+ frequency up to 1000 GHz.
50
+
51
+ Args:
52
+ pressure: Pressure (Pa)
53
+ vapor_pressure: Water vapor partial pressure (Pa)
54
+ temperature: Temperature (K)
55
+ frequency: Frequency (GHz)
56
+
57
+ References:
58
+ ITU-R P.676-13: Attenuation by atmospheric gases and related effects.
59
+ https://www.itu.int/dms_pubrec/itu-r/rec/p/R-REC-P.676-13-202208-I!!PDF-E.pdf
60
+ """
61
+ pressure = pressure * con.PA_TO_HPA
62
+ vapor_pressure = vapor_pressure * con.PA_TO_HPA
63
+ dry_pressure = pressure - vapor_pressure
64
+ theta = 300 / temperature
65
+ oxygen_refractivity = _calc_oxygen_refractivity(
66
+ dry_pressure, vapor_pressure, frequency, theta
67
+ )
68
+ vapor_refractivity = _calc_vapor_refractivity(
69
+ dry_pressure, vapor_pressure, frequency, theta
70
+ )
71
+ return 0.1820 * frequency * (oxygen_refractivity + vapor_refractivity)
72
+
73
+
74
+ def _calc_line_shape(
75
+ frequency: float,
76
+ center: npt.NDArray,
77
+ width: npt.NDArray,
78
+ correction: npt.NDArray | float,
79
+ ) -> npt.NDArray:
80
+ return (
81
+ frequency
82
+ / center
83
+ * (
84
+ (width - correction * (center - frequency))
85
+ / ((center - frequency) ** 2 + width**2)
86
+ + (width - correction * (center + frequency))
87
+ / ((center + frequency) ** 2 + width**2)
88
+ )
89
+ )
90
+
91
+
92
+ def _calc_oxygen_refractivity(
93
+ dry_pressure: npt.NDArray,
94
+ vapor_pressure: npt.NDArray,
95
+ frequency: float,
96
+ theta: npt.NDArray,
97
+ ) -> npt.NDArray:
98
+ f0, a1, a2, a3, a4, a5, a6 = OXYGEN_TABLE[:, :, np.newaxis, np.newaxis]
99
+ strength = a1 * 1e-7 * dry_pressure * theta**3 * np.exp(a2 * (1 - theta))
100
+ width = (
101
+ a3 * 1e-4 * (dry_pressure * theta ** (0.8 - a4) + 1.1 * vapor_pressure * theta)
102
+ )
103
+ width = np.sqrt(width**2 + 2.25e-6)
104
+ correction = (a5 + a6 * theta) * 1e-4 * (dry_pressure + vapor_pressure) * theta**0.8
105
+ shape = _calc_line_shape(frequency, f0, width, correction)
106
+ d = 5.6e-4 * (dry_pressure + vapor_pressure) * theta**0.8
107
+ continuum = (
108
+ frequency
109
+ * dry_pressure
110
+ * theta**2
111
+ * (
112
+ 6.14e-5 / (d * (1 + (frequency / d) ** 2))
113
+ + ((1.4e-12 * dry_pressure * theta**1.5) / (1 + 1.9e-5 * frequency**1.5))
114
+ )
115
+ )
116
+ return np.sum(strength * shape, axis=0) + continuum
117
+
118
+
119
+ def _calc_vapor_refractivity(
120
+ dry_pressure: npt.NDArray,
121
+ vapor_pressure: npt.NDArray,
122
+ frequency: float,
123
+ theta: npt.NDArray,
124
+ ) -> npt.NDArray:
125
+ f0, b1, b2, b3, b4, b5, b6 = VAPOR_TABLE[:, :, np.newaxis, np.newaxis]
126
+ strength = b1 * 1e-1 * vapor_pressure * theta**3.5 * np.exp(b2 * (1 - theta))
127
+ width = b3 * 1e-4 * (dry_pressure * theta**b4 + b5 * vapor_pressure * theta**b6)
128
+ width = 0.535 * width + np.sqrt(0.217 * width**2 + (2.1316e-12 * f0**2) / theta)
129
+ correction = 0.0
130
+ shape = _calc_line_shape(frequency, f0, width, correction)
131
+ return np.sum(strength * shape, axis=0)
132
+
133
+
134
+ def calc_saturation_vapor_pressure(temperature: npt.NDArray) -> npt.NDArray:
135
+ """Calculate saturation vapor pressure using Tetens equation with respect to
136
+ water or ice depending on whether the temperature is above freezing or not.
137
+
138
+ Args:
139
+ temperature: Temperature (K)
140
+
141
+ Returns:
142
+ Saturation vapor pressure (Pa)
143
+
144
+ References:
145
+ Murray, F. W. (1967). On the Computation of Saturation Vapor Pressure.
146
+ Journal of Applied Meteorology and Climatology, 6(1), 203-204.
147
+ https://doi.org/10.1175/1520-0450(1967)006<0203:OTCOSV>2.0.CO;2
148
+ """
149
+ freezing = 273.16
150
+ is_freezing = temperature < freezing
151
+ a = np.where(is_freezing, 21.8745584, 17.2693882)
152
+ b = np.where(is_freezing, 7.66, 35.86)
153
+ return 610.78 * np.exp(a * (temperature - freezing) / (temperature - b))
154
+
155
+
156
+ OXYGEN_TABLE = np.array(
157
+ [
158
+ [50.474214, 0.975, 9.651, 6.690, 0.0, 2.566, 6.850],
159
+ [50.987745, 2.529, 8.653, 7.170, 0.0, 2.246, 6.800],
160
+ [51.503360, 6.193, 7.709, 7.640, 0.0, 1.947, 6.729],
161
+ [52.021429, 14.320, 6.819, 8.110, 0.0, 1.667, 6.640],
162
+ [52.542418, 31.240, 5.983, 8.580, 0.0, 1.388, 6.526],
163
+ [53.066934, 64.290, 5.201, 9.060, 0.0, 1.349, 6.206],
164
+ [53.595775, 124.600, 4.474, 9.550, 0.0, 2.227, 5.085],
165
+ [54.130025, 227.300, 3.800, 9.960, 0.0, 3.170, 3.750],
166
+ [54.671180, 389.700, 3.182, 10.370, 0.0, 3.558, 2.654],
167
+ [55.221384, 627.100, 2.618, 10.890, 0.0, 2.560, 2.952],
168
+ [55.783815, 945.300, 2.109, 11.340, 0.0, -1.172, 6.135],
169
+ [56.264774, 543.400, 0.014, 17.030, 0.0, 3.525, -0.978],
170
+ [56.363399, 1331.800, 1.654, 11.890, 0.0, -2.378, 6.547],
171
+ [56.968211, 1746.600, 1.255, 12.230, 0.0, -3.545, 6.451],
172
+ [57.612486, 2120.100, 0.910, 12.620, 0.0, -5.416, 6.056],
173
+ [58.323877, 2363.700, 0.621, 12.950, 0.0, -1.932, 0.436],
174
+ [58.446588, 1442.100, 0.083, 14.910, 0.0, 6.768, -1.273],
175
+ [59.164204, 2379.900, 0.387, 13.530, 0.0, -6.561, 2.309],
176
+ [59.590983, 2090.700, 0.207, 14.080, 0.0, 6.957, -0.776],
177
+ [60.306056, 2103.400, 0.207, 14.150, 0.0, -6.395, 0.699],
178
+ [60.434778, 2438.000, 0.386, 13.390, 0.0, 6.342, -2.825],
179
+ [61.150562, 2479.500, 0.621, 12.920, 0.0, 1.014, -0.584],
180
+ [61.800158, 2275.900, 0.910, 12.630, 0.0, 5.014, -6.619],
181
+ [62.411220, 1915.400, 1.255, 12.170, 0.0, 3.029, -6.759],
182
+ [62.486253, 1503.000, 0.083, 15.130, 0.0, -4.499, 0.844],
183
+ [62.997984, 1490.200, 1.654, 11.740, 0.0, 1.856, -6.675],
184
+ [63.568526, 1078.000, 2.108, 11.340, 0.0, 0.658, -6.139],
185
+ [64.127775, 728.700, 2.617, 10.880, 0.0, -3.036, -2.895],
186
+ [64.678910, 461.300, 3.181, 10.380, 0.0, -3.968, -2.590],
187
+ [65.224078, 274.000, 3.800, 9.960, 0.0, -3.528, -3.680],
188
+ [65.764779, 153.000, 4.473, 9.550, 0.0, -2.548, -5.002],
189
+ [66.302096, 80.400, 5.200, 9.060, 0.0, -1.660, -6.091],
190
+ [66.836834, 39.800, 5.982, 8.580, 0.0, -1.680, -6.393],
191
+ [67.369601, 18.560, 6.818, 8.110, 0.0, -1.956, -6.475],
192
+ [67.900868, 8.172, 7.708, 7.640, 0.0, -2.216, -6.545],
193
+ [68.431006, 3.397, 8.652, 7.170, 0.0, -2.492, -6.600],
194
+ [68.960312, 1.334, 9.650, 6.690, 0.0, -2.773, -6.650],
195
+ [118.750334, 940.300, 0.010, 16.640, 0.0, -0.439, 0.079],
196
+ [368.498246, 67.400, 0.048, 16.400, 0.0, 0.000, 0.000],
197
+ [424.763020, 637.700, 0.044, 16.400, 0.0, 0.000, 0.000],
198
+ [487.249273, 237.400, 0.049, 16.000, 0.0, 0.000, 0.000],
199
+ [715.392902, 98.100, 0.145, 16.000, 0.0, 0.000, 0.000],
200
+ [773.839490, 572.300, 0.141, 16.200, 0.0, 0.000, 0.000],
201
+ [834.145546, 183.100, 0.145, 14.700, 0.0, 0.000, 0.000],
202
+ ]
203
+ ).T
204
+
205
+ VAPOR_TABLE = np.array(
206
+ [
207
+ [22.235080, 0.1079, 2.144, 26.38, 0.76, 5.087, 1.00],
208
+ [67.803960, 0.0011, 8.732, 28.58, 0.69, 4.930, 0.82],
209
+ [119.995940, 0.0007, 8.353, 29.48, 0.70, 4.780, 0.79],
210
+ [183.310087, 2.273, 0.668, 29.06, 0.77, 5.022, 0.85],
211
+ [321.225630, 0.0470, 6.179, 24.04, 0.67, 4.398, 0.54],
212
+ [325.152888, 1.514, 1.541, 28.23, 0.64, 4.893, 0.74],
213
+ [336.227764, 0.0010, 9.825, 26.93, 0.69, 4.740, 0.61],
214
+ [380.197353, 11.67, 1.048, 28.11, 0.54, 5.063, 0.89],
215
+ [390.134508, 0.0045, 7.347, 21.52, 0.63, 4.810, 0.55],
216
+ [437.346667, 0.0632, 5.048, 18.45, 0.60, 4.230, 0.48],
217
+ [439.150807, 0.9098, 3.595, 20.07, 0.63, 4.483, 0.52],
218
+ [443.018343, 0.1920, 5.048, 15.55, 0.60, 5.083, 0.50],
219
+ [448.001085, 10.41, 1.405, 25.64, 0.66, 5.028, 0.67],
220
+ [470.888999, 0.3254, 3.597, 21.34, 0.66, 4.506, 0.65],
221
+ [474.689092, 1.260, 2.379, 23.20, 0.65, 4.804, 0.64],
222
+ [488.490108, 0.2529, 2.852, 25.86, 0.69, 5.201, 0.72],
223
+ [503.568532, 0.0372, 6.731, 16.12, 0.61, 3.980, 0.43],
224
+ [504.482692, 0.0124, 6.731, 16.12, 0.61, 4.010, 0.45],
225
+ [547.676440, 0.9785, 0.158, 26.00, 0.70, 4.500, 1.00],
226
+ [552.020960, 0.1840, 0.158, 26.00, 0.70, 4.500, 1.00],
227
+ [556.935985, 497.0, 0.159, 30.86, 0.69, 4.552, 1.00],
228
+ [620.700807, 5.015, 2.391, 24.38, 0.71, 4.856, 0.68],
229
+ [645.766085, 0.0067, 8.633, 18.00, 0.60, 4.000, 0.50],
230
+ [658.005280, 0.2732, 7.816, 32.10, 0.69, 4.140, 1.00],
231
+ [752.033113, 243.4, 0.396, 30.86, 0.68, 4.352, 0.84],
232
+ [841.051732, 0.0134, 8.177, 15.90, 0.33, 5.760, 0.45],
233
+ [859.965698, 0.1325, 8.055, 30.60, 0.68, 4.090, 0.84],
234
+ [899.303175, 0.0547, 7.914, 29.85, 0.68, 4.530, 0.90],
235
+ [902.611085, 0.0386, 8.429, 28.65, 0.70, 5.100, 0.95],
236
+ [906.205957, 0.1836, 5.110, 24.08, 0.70, 4.700, 0.53],
237
+ [916.171582, 8.400, 1.441, 26.73, 0.70, 5.150, 0.78],
238
+ [923.112692, 0.0079, 10.293, 29.00, 0.70, 5.000, 0.80],
239
+ [970.315022, 9.009, 1.919, 25.50, 0.64, 4.940, 0.67],
240
+ [987.926764, 134.6, 0.257, 29.85, 0.68, 4.550, 0.90],
241
+ [1780.000000, 17506.0, 0.952, 196.3, 2.00, 24.15, 5.00],
242
+ ]
243
+ ).T
@@ -107,9 +107,6 @@ def _find_melting_layer_from_ldr(
107
107
  v_prof: np.ndarray,
108
108
  z_prof: np.ndarray,
109
109
  ) -> np.ndarray | None:
110
- if ldr_prof is None:
111
- raise ValueError
112
-
113
110
  peak = int(np.argmax(ldr_prof))
114
111
  base, top = _basetop(ldr_dprof, peak)
115
112
  conditions = (
@@ -6,6 +6,11 @@ from scipy.interpolate import interp1d
6
6
 
7
7
  from cloudnetpy import utils
8
8
  from cloudnetpy.categorize import atmos_utils
9
+ from cloudnetpy.categorize.itu import (
10
+ calc_gas_specific_attenuation,
11
+ calc_liquid_specific_attenuation,
12
+ calc_saturation_vapor_pressure,
13
+ )
9
14
  from cloudnetpy.cloudnetarray import CloudnetArray
10
15
  from cloudnetpy.datasource import DataSource
11
16
  from cloudnetpy.exceptions import ModelDataError
@@ -35,12 +40,14 @@ class Model(DataSource):
35
40
  "temperature",
36
41
  "pressure",
37
42
  "rh",
38
- "gas_atten",
43
+ "q",
44
+ )
45
+ fields_sparse = (*fields_dense, "uwind", "vwind")
46
+ fields_atten = (
39
47
  "specific_gas_atten",
40
48
  "specific_saturated_gas_atten",
41
49
  "specific_liquid_atten",
42
50
  )
43
- fields_sparse = (*fields_dense, "q", "uwind", "vwind")
44
51
 
45
52
  def __init__(self, model_file: str, alt_site: float, options: dict | None = None):
46
53
  super().__init__(model_file)
@@ -53,14 +60,8 @@ class Model(DataSource):
53
60
  self.data_dense: dict = {}
54
61
  self._append_grid()
55
62
 
56
- def interpolate_to_common_height(self, wl_band: int) -> None:
57
- """Interpolates model variables to common height grid.
58
-
59
- Args:
60
- wl_band: Integer denoting the approximate wavelength band of the
61
- cloud radar (0 = ~35.5 GHz, 1 = ~94 GHz).
62
-
63
- """
63
+ def interpolate_to_common_height(self) -> None:
64
+ """Interpolates model variables to common height grid."""
64
65
 
65
66
  def _interpolate_variable(data_in: ma.MaskedArray) -> CloudnetArray:
66
67
  datai = ma.zeros((len(self.time), len(self.mean_height)))
@@ -78,8 +79,6 @@ class Model(DataSource):
78
79
  variable = self.dataset.variables[key]
79
80
  data = variable[:]
80
81
  units = variable.units
81
- if "atten" in key:
82
- data = data[wl_band, :, :]
83
82
  self.data_sparse[key] = _interpolate_variable(data)
84
83
 
85
84
  def interpolate_to_grid(
@@ -97,7 +96,8 @@ class Model(DataSource):
97
96
  Indices fully masked profiles.
98
97
 
99
98
  """
100
- for key in self.fields_dense:
99
+ half_height = height_grid - np.diff(height_grid, prepend=0) / 2
100
+ for key in self.fields_dense + self.fields_atten:
101
101
  array = self.data_sparse[key][:]
102
102
  valid_profiles = _find_number_of_valid_profiles(array)
103
103
  if valid_profiles < 2:
@@ -107,7 +107,7 @@ class Model(DataSource):
107
107
  self.mean_height,
108
108
  array,
109
109
  time_grid,
110
- height_grid,
110
+ half_height if "atten" in key else height_grid,
111
111
  )
112
112
  self.height = height_grid
113
113
  return utils.find_masked_profiles_indices(self.data_dense["temperature"])
@@ -139,6 +139,23 @@ class Model(DataSource):
139
139
  raise ModelDataError(msg) from err
140
140
  return self.to_m(model_heights) + alt_site
141
141
 
142
+ def calc_attenuations(self, frequency: float):
143
+ temperature = self.getvar("temperature")
144
+ pressure = self.getvar("pressure")
145
+ specific_humidity = self.getvar("q")
146
+
147
+ self.data_sparse["specific_liquid_atten"] = calc_liquid_specific_attenuation(
148
+ temperature, frequency
149
+ )
150
+ vp = atmos_utils.calc_vapor_pressure(pressure, specific_humidity)
151
+ svp = calc_saturation_vapor_pressure(temperature)
152
+ self.data_sparse["specific_gas_atten"] = calc_gas_specific_attenuation(
153
+ pressure, vp, temperature, frequency
154
+ )
155
+ self.data_sparse["specific_saturated_gas_atten"] = (
156
+ calc_gas_specific_attenuation(pressure, svp, temperature, frequency)
157
+ )
158
+
142
159
 
143
160
  def _calc_mean_height(model_heights: np.ndarray) -> np.ndarray:
144
161
  mean_height = ma.mean(model_heights, axis=0)
@@ -8,6 +8,7 @@ from numpy import ma
8
8
  from scipy import constants
9
9
 
10
10
  from cloudnetpy import utils
11
+ from cloudnetpy.categorize.attenuations import RadarAttenuation
11
12
  from cloudnetpy.constants import GHZ_TO_HZ, SEC_IN_HOUR, SPEED_OF_LIGHT
12
13
  from cloudnetpy.datasource import DataSource
13
14
 
@@ -42,6 +43,8 @@ class Radar(DataSource):
42
43
  self.sequence_indices = self._get_sequence_indices()
43
44
  self.location = getattr(self.dataset, "location", "")
44
45
  self.source_type = getattr(self.dataset, "source", "")
46
+ self.height: np.ndarray
47
+ self.altitude: float
45
48
  self._init_data()
46
49
  self._init_sigma_v()
47
50
  self._get_folding_velocity_full()
@@ -200,26 +203,29 @@ class Radar(DataSource):
200
203
 
201
204
  return n_removed_total
202
205
 
203
- def correct_atten(self, attenuations: dict) -> None:
206
+ def correct_atten(self, attenuations: RadarAttenuation) -> None:
204
207
  """Corrects radar echo for liquid and gas attenuation.
205
208
 
206
209
  Args:
207
- attenuations: 2-D attenuations due to atmospheric gases and liquid:
208
- `radar_gas_atten`, `radar_liquid_atten`.
210
+ attenuations: Radar attenuation object.
209
211
 
210
212
  References:
211
213
  The method is based on Hogan R. and O'Connor E., 2004,
212
214
  https://bit.ly/2Yjz9DZ and the original Cloudnet Matlab implementation.
213
215
 
214
216
  """
215
- z_corrected = self.data["Z"][:] + attenuations["radar_gas_atten"]
216
- ind = ma.where(attenuations["radar_liquid_atten"])
217
- z_corrected[ind] += attenuations["radar_liquid_atten"][ind]
217
+ z_corrected = self.data["Z"][:] + attenuations.gas.amount
218
+ ind = ma.where(attenuations.liquid.amount)
219
+ z_corrected[ind] += attenuations.liquid.amount[ind]
220
+ ind = ma.where(attenuations.rain.amount)
221
+ z_corrected[ind] += attenuations.rain.amount[ind]
222
+ ind = ma.where(attenuations.melting.amount)
223
+ z_corrected[ind] += attenuations.melting.amount[ind]
218
224
  self.append_data(z_corrected, "Z")
219
225
 
220
226
  def calc_errors(
221
227
  self,
222
- attenuations: dict,
228
+ attenuations: RadarAttenuation,
223
229
  is_clutter: np.ndarray,
224
230
  ) -> None:
225
231
  """Calculates uncertainties of radar echo.
@@ -239,7 +245,7 @@ class Radar(DataSource):
239
245
 
240
246
  def _calc_sensitivity() -> np.ndarray:
241
247
  """Returns sensitivity of radar as function of altitude."""
242
- mean_gas_atten = ma.mean(attenuations["radar_gas_atten"], axis=0)
248
+ mean_gas_atten = ma.mean(attenuations.gas.amount, axis=0)
243
249
  z_sensitivity = z_power_min + log_range + mean_gas_atten
244
250
  zc = ma.median(ma.array(z, mask=~is_clutter), axis=0)
245
251
  valid_values = np.logical_not(zc.mask)
@@ -260,10 +266,20 @@ class Radar(DataSource):
260
266
  z_precision = ma.divide(ln_to_log10, np.sqrt(n_pulses)) * (
261
267
  1 + (utils.db2lin(z_power_min - z_power) / noise_threshold)
262
268
  )
263
- gas_error = attenuations["radar_gas_atten"] * 0.1
264
- liq_error = attenuations["liquid_atten_err"].filled(0)
265
- z_error = utils.l2norm(gas_error, liq_error, z_precision)
266
- z_error[attenuations["liquid_uncorrected"]] = ma.masked
269
+
270
+ z_error = utils.l2norm(
271
+ z_precision,
272
+ attenuations.liquid.error.filled(0),
273
+ attenuations.rain.error.filled(0),
274
+ attenuations.melting.error.filled(0),
275
+ )
276
+
277
+ z_error[
278
+ attenuations.liquid.uncorrected
279
+ | attenuations.rain.uncorrected
280
+ | attenuations.melting.uncorrected
281
+ ] = ma.masked
282
+
267
283
  return z_error
268
284
 
269
285
  def _number_of_independent_pulses() -> float:
cloudnetpy/constants.py CHANGED
@@ -8,12 +8,6 @@ T0: Final = 273.16
8
8
  # Ratio of the molecular weight of water vapor to dry air
9
9
  MW_RATIO: Final = 0.62198
10
10
 
11
- # Specific heat capacity of air at around 275K (J kg-1 K-1)
12
- SPECIFIC_HEAT: Final = 1004
13
-
14
- # Latent heat of evaporation (J kg-1)
15
- LATENT_HEAT: Final = 2.26e6
16
-
17
11
  # Specific gas constant for dry air (J kg-1 K-1)
18
12
  RS: Final = 287.058
19
13
 
@@ -27,9 +21,12 @@ SEC_IN_HOUR: Final = 3600
27
21
  SEC_IN_DAY: Final = 86400
28
22
  MM_TO_M: Final = 1e-3
29
23
  G_TO_KG: Final = 1e-3
24
+ KG_TO_G: Final = 1e3
30
25
  M_S_TO_MM_H: Final = SEC_IN_HOUR / MM_TO_M
31
26
  MM_H_TO_M_S: Final = 1 / M_S_TO_MM_H
32
27
  GHZ_TO_HZ: Final = 1e9
33
28
  HPA_TO_PA: Final = 100
34
29
  PA_TO_HPA: Final = 1 / HPA_TO_PA
35
30
  KM_H_TO_M_S: Final = 1000 / SEC_IN_HOUR
31
+ M_TO_KM: Final = 1e-3
32
+ TWO_WAY: Final = 2
@@ -94,7 +94,7 @@ def save_downsampled_file(
94
94
  output.copy_global(obj.dataset, root_group, ("location", "day", "month", "year"))
95
95
  if not hasattr(obj.dataset, "day"):
96
96
  root_group.year, root_group.month, root_group.day = obj.date
97
- output.merge_history(root_group, id_mark, {"l3": obj})
97
+ output.merge_history(root_group, id_mark, obj)
98
98
  root_group.close()
99
99
  if not isinstance(uuid, str):
100
100
  msg = "UUID is not a string."
@@ -156,7 +156,7 @@ def _add_source(root_ground: netCDF4.Dataset, objects: tuple, files: tuple) -> N
156
156
  if i < len(model_files) - 1:
157
157
  source += "\n"
158
158
  root_ground.source = source
159
- root_ground.source_file_uuids = output.get_source_uuids(model, obs)
159
+ root_ground.source_file_uuids = output.get_source_uuids([model, obs])
160
160
 
161
161
 
162
162
  def add_time_attribute(date: datetime) -> dict:
@@ -6,7 +6,7 @@ from numpy import ma
6
6
 
7
7
  from cloudnetpy import utils
8
8
  from cloudnetpy.datasource import DataSource
9
- from cloudnetpy.products.product_tools import CategorizeBits
9
+ from cloudnetpy.products.product_tools import CategorizeBits, CategoryBits
10
10
 
11
11
 
12
12
  class ObservationManager(DataSource):
@@ -79,14 +79,14 @@ class ObservationManager(DataSource):
79
79
  return self._mask_cloud_bits(cloud_mask)
80
80
 
81
81
  @staticmethod
82
- def _classify_basic_mask(bits: dict) -> np.ndarray:
83
- cloud_mask = bits["droplet"] + bits["falling"] * 2
84
- cloud_mask[bits["falling"] & bits["cold"]] = (
85
- cloud_mask[bits["falling"] & bits["cold"]] + 2
82
+ def _classify_basic_mask(bits: CategoryBits) -> np.ndarray:
83
+ cloud_mask = bits.droplet + bits.falling * 2
84
+ cloud_mask[bits.falling & bits.freezing] = (
85
+ cloud_mask[bits.falling & bits.freezing] + 2
86
86
  )
87
- cloud_mask[bits["aerosol"]] = 6
88
- cloud_mask[bits["insect"]] = 7
89
- cloud_mask[bits["aerosol"] & bits["insect"]] = 8
87
+ cloud_mask[bits.aerosol] = 6
88
+ cloud_mask[bits.insect] = 7
89
+ cloud_mask[bits.aerosol & bits.insect] = 8
90
90
  return cloud_mask
91
91
 
92
92
  @staticmethod
@@ -174,7 +174,8 @@ def test_regrid_cf_area_all_nan(model_file, obs_file) -> None:
174
174
  [1, 1, 1],
175
175
  [1, 1, 1],
176
176
  [1, 1, 1],
177
- ], mask=True
177
+ ],
178
+ mask=True,
178
179
  )
179
180
  d = {"cf_A": ma.zeros((1, 1))}
180
181
  d = obj._regrid_cf(d, 0, 0, data)
@@ -413,7 +414,9 @@ def test_regrid_iwc_all_masked(model_file, obs_file) -> None:
413
414
  obs = ObservationManager(PRODUCT, str(obs_file))
414
415
  model = ModelManager(str(model_file), MODEL, OUTPUT_FILE, PRODUCT)
415
416
  obj = ProductGrid(model, obs)
416
- obj._obs_data = ma.array([[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3], [4, 4, 4, 4]], mask=True)
417
+ obj._obs_data = ma.array(
418
+ [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3], [4, 4, 4, 4]], mask=True
419
+ )
417
420
  d = {"iwc": ma.zeros((1, 1))}
418
421
  ind = ma.array([[0, 1, 1, 1]], dtype=bool)
419
422
  no_rain = ma.array(
@@ -5,21 +5,21 @@ import pytest
5
5
  from numpy import ma, testing
6
6
 
7
7
  from cloudnetpy.model_evaluation.products.product_resampling import ObservationManager
8
- from cloudnetpy.products.product_tools import CategorizeBits
8
+ from cloudnetpy.products.product_tools import CategorizeBits, CategoryBits
9
9
 
10
10
  PRODUCT = "iwc"
11
11
 
12
12
 
13
13
  class CatBits:
14
14
  def __init__(self) -> None:
15
- self.category_bits = {
16
- "droplet": np.asarray([[1, 0, 1, 1, 1, 1], [0, 1, 1, 1, 0, 0]], dtype=bool),
17
- "falling": np.asarray([[0, 0, 0, 0, 1, 0], [0, 0, 0, 1, 1, 1]], dtype=bool),
18
- "cold": np.asarray([[0, 0, 1, 1, 0, 0], [0, 1, 1, 1, 0, 1]], dtype=bool),
19
- "melting": np.asarray([[1, 0, 1, 0, 0, 0], [1, 1, 0, 0, 0, 0]], dtype=bool),
20
- "aerosol": np.asarray([[1, 0, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0]], dtype=bool),
21
- "insect": np.asarray([[1, 1, 0, 0, 0, 0], [0, 0, 1, 0, 0, 0]], dtype=bool),
22
- }
15
+ self.category_bits = CategoryBits(
16
+ droplet=np.asarray([[1, 0, 1, 1, 1, 1], [0, 1, 1, 1, 0, 0]], dtype=bool),
17
+ falling=np.asarray([[0, 0, 0, 0, 1, 0], [0, 0, 0, 1, 1, 1]], dtype=bool),
18
+ freezing=np.asarray([[0, 0, 1, 1, 0, 0], [0, 1, 1, 1, 0, 1]], dtype=bool),
19
+ melting=np.asarray([[1, 0, 1, 0, 0, 0], [1, 1, 0, 0, 0, 0]], dtype=bool),
20
+ aerosol=np.asarray([[1, 0, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0]], dtype=bool),
21
+ insect=np.asarray([[1, 1, 0, 0, 0, 0], [0, 0, 1, 0, 0, 0]], dtype=bool),
22
+ )
23
23
 
24
24
 
25
25
  def test_get_date(obs_file) -> None:
@@ -96,7 +96,7 @@ def test_mask_cloud_bits(obs_file) -> None:
96
96
  def test_basic_cloud_mask_all_values(obs_file) -> None:
97
97
  cat = CatBits()
98
98
  obj = ObservationManager("cf", str(obs_file))
99
- x = obj._classify_basic_mask(cat.category_bits)
99
+ x = obj._classify_basic_mask(cat.category_bits) # type: ignore
100
100
  compare = np.array([[8, 7, 6, 1, 3, 1], [0, 1, 7, 5, 2, 4]])
101
101
  testing.assert_array_almost_equal(x, compare)
102
102
 
@@ -104,7 +104,7 @@ def test_basic_cloud_mask_all_values(obs_file) -> None:
104
104
  def test_mask_cloud_bits_all_values(obs_file) -> None:
105
105
  cat = CatBits()
106
106
  obj = ObservationManager("cf", str(obs_file))
107
- mask = obj._classify_basic_mask(cat.category_bits)
107
+ mask = obj._classify_basic_mask(cat.category_bits) # type: ignore
108
108
  x = obj._mask_cloud_bits(mask)
109
109
  compare = np.array([[0, 0, 0, 1, 1, 1], [0, 1, 0, 1, 0, 1]])
110
110
  testing.assert_array_almost_equal(x, compare)