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
@@ -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
|
cloudnetpy/categorize/melting.py
CHANGED
@@ -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 = (
|
cloudnetpy/categorize/model.py
CHANGED
@@ -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
|
-
"
|
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
|
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
|
-
|
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)
|
cloudnetpy/categorize/radar.py
CHANGED
@@ -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:
|
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:
|
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
|
216
|
-
ind = ma.where(attenuations
|
217
|
-
z_corrected[ind] += attenuations[
|
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:
|
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
|
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
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
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,
|
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:
|
83
|
-
cloud_mask = bits
|
84
|
-
cloud_mask[bits
|
85
|
-
cloud_mask[bits
|
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
|
88
|
-
cloud_mask[bits
|
89
|
-
cloud_mask[bits
|
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
|
-
],
|
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(
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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)
|