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.
- cloudnetpy/categorize/__init__.py +1 -2
- cloudnetpy/categorize/atmos_utils.py +297 -67
- 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 +84 -0
- cloudnetpy/categorize/attenuations/melting_attenuation.py +78 -0
- cloudnetpy/categorize/attenuations/rain_attenuation.py +84 -0
- cloudnetpy/categorize/categorize.py +332 -156
- cloudnetpy/categorize/classify.py +127 -125
- cloudnetpy/categorize/containers.py +107 -76
- cloudnetpy/categorize/disdrometer.py +40 -0
- cloudnetpy/categorize/droplet.py +23 -21
- cloudnetpy/categorize/falling.py +53 -24
- cloudnetpy/categorize/freezing.py +25 -12
- cloudnetpy/categorize/insects.py +35 -23
- cloudnetpy/categorize/itu.py +243 -0
- cloudnetpy/categorize/lidar.py +36 -41
- cloudnetpy/categorize/melting.py +34 -26
- cloudnetpy/categorize/model.py +84 -37
- cloudnetpy/categorize/mwr.py +18 -14
- cloudnetpy/categorize/radar.py +215 -102
- cloudnetpy/cli.py +578 -0
- cloudnetpy/cloudnetarray.py +43 -89
- cloudnetpy/concat_lib.py +218 -78
- cloudnetpy/constants.py +28 -10
- cloudnetpy/datasource.py +61 -86
- cloudnetpy/exceptions.py +49 -20
- cloudnetpy/instruments/__init__.py +5 -0
- cloudnetpy/instruments/basta.py +29 -12
- cloudnetpy/instruments/bowtie.py +135 -0
- cloudnetpy/instruments/ceilo.py +138 -115
- cloudnetpy/instruments/ceilometer.py +164 -80
- cloudnetpy/instruments/cl61d.py +21 -5
- cloudnetpy/instruments/cloudnet_instrument.py +74 -36
- cloudnetpy/instruments/copernicus.py +108 -30
- cloudnetpy/instruments/da10.py +54 -0
- cloudnetpy/instruments/disdrometer/common.py +126 -223
- cloudnetpy/instruments/disdrometer/parsivel.py +453 -94
- cloudnetpy/instruments/disdrometer/thies.py +254 -87
- cloudnetpy/instruments/fd12p.py +201 -0
- cloudnetpy/instruments/galileo.py +65 -23
- cloudnetpy/instruments/hatpro.py +123 -49
- cloudnetpy/instruments/instruments.py +113 -1
- cloudnetpy/instruments/lufft.py +39 -17
- cloudnetpy/instruments/mira.py +268 -61
- cloudnetpy/instruments/mrr.py +187 -0
- cloudnetpy/instruments/nc_lidar.py +19 -8
- cloudnetpy/instruments/nc_radar.py +109 -55
- cloudnetpy/instruments/pollyxt.py +135 -51
- cloudnetpy/instruments/radiometrics.py +313 -59
- cloudnetpy/instruments/rain_e_h3.py +171 -0
- cloudnetpy/instruments/rpg.py +321 -189
- cloudnetpy/instruments/rpg_reader.py +74 -40
- cloudnetpy/instruments/toa5.py +49 -0
- cloudnetpy/instruments/vaisala.py +95 -343
- cloudnetpy/instruments/weather_station.py +774 -105
- cloudnetpy/metadata.py +90 -19
- cloudnetpy/model_evaluation/file_handler.py +55 -52
- cloudnetpy/model_evaluation/metadata.py +46 -20
- cloudnetpy/model_evaluation/model_metadata.py +1 -1
- cloudnetpy/model_evaluation/plotting/plot_tools.py +32 -37
- cloudnetpy/model_evaluation/plotting/plotting.py +327 -117
- cloudnetpy/model_evaluation/products/advance_methods.py +92 -83
- cloudnetpy/model_evaluation/products/grid_methods.py +88 -63
- cloudnetpy/model_evaluation/products/model_products.py +43 -35
- cloudnetpy/model_evaluation/products/observation_products.py +41 -35
- cloudnetpy/model_evaluation/products/product_resampling.py +17 -7
- cloudnetpy/model_evaluation/products/tools.py +29 -20
- cloudnetpy/model_evaluation/statistics/statistical_methods.py +30 -20
- cloudnetpy/model_evaluation/tests/e2e/conftest.py +3 -3
- cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +9 -5
- cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +15 -14
- cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +9 -5
- cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +15 -14
- cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +9 -5
- cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +15 -14
- cloudnetpy/model_evaluation/tests/unit/conftest.py +42 -41
- cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +41 -48
- cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +216 -194
- cloudnetpy/model_evaluation/tests/unit/test_model_products.py +23 -21
- cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +37 -38
- cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +43 -40
- cloudnetpy/model_evaluation/tests/unit/test_plotting.py +30 -36
- cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +68 -31
- cloudnetpy/model_evaluation/tests/unit/test_tools.py +33 -26
- cloudnetpy/model_evaluation/utils.py +2 -1
- cloudnetpy/output.py +170 -111
- cloudnetpy/plotting/__init__.py +2 -1
- cloudnetpy/plotting/plot_meta.py +562 -822
- cloudnetpy/plotting/plotting.py +1142 -704
- cloudnetpy/products/__init__.py +1 -0
- cloudnetpy/products/classification.py +370 -88
- cloudnetpy/products/der.py +85 -55
- cloudnetpy/products/drizzle.py +77 -34
- cloudnetpy/products/drizzle_error.py +15 -11
- cloudnetpy/products/drizzle_tools.py +79 -59
- cloudnetpy/products/epsilon.py +211 -0
- cloudnetpy/products/ier.py +27 -50
- cloudnetpy/products/iwc.py +55 -48
- cloudnetpy/products/lwc.py +96 -70
- cloudnetpy/products/mwr_tools.py +186 -0
- cloudnetpy/products/product_tools.py +170 -128
- cloudnetpy/utils.py +455 -240
- cloudnetpy/version.py +2 -2
- {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/METADATA +44 -40
- cloudnetpy-1.87.3.dist-info/RECORD +127 -0
- {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/WHEEL +1 -1
- cloudnetpy-1.87.3.dist-info/entry_points.txt +2 -0
- docs/source/conf.py +2 -2
- cloudnetpy/categorize/atmos.py +0 -361
- cloudnetpy/products/mwr_multi.py +0 -68
- cloudnetpy/products/mwr_single.py +0 -75
- cloudnetpy-1.49.9.dist-info/RECORD +0 -112
- {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info/licenses}/LICENSE +0 -0
- {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.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 | np.floating
|
|
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 | np.floating,
|
|
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 | np.floating,
|
|
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 | np.floating,
|
|
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 | np.floating,
|
|
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/lidar.py
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"""Lidar module, containing the :class:`Lidar` class."""
|
|
2
|
+
|
|
2
3
|
import logging
|
|
4
|
+
from os import PathLike
|
|
5
|
+
from typing import Literal
|
|
3
6
|
|
|
4
|
-
import numpy as
|
|
7
|
+
import numpy.typing as npt
|
|
5
8
|
from numpy import ma
|
|
6
9
|
|
|
7
10
|
from cloudnetpy.datasource import DataSource
|
|
8
|
-
from cloudnetpy.utils import interpolate_2d_nearest
|
|
11
|
+
from cloudnetpy.utils import get_gap_ind, interpolate_2d_nearest
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
class Lidar(DataSource):
|
|
@@ -16,61 +19,53 @@ class Lidar(DataSource):
|
|
|
16
19
|
|
|
17
20
|
"""
|
|
18
21
|
|
|
19
|
-
def __init__(self, full_path: str):
|
|
22
|
+
def __init__(self, full_path: str | PathLike) -> None:
|
|
20
23
|
super().__init__(full_path)
|
|
21
24
|
self.append_data(self.getvar("beta"), "beta")
|
|
22
25
|
self._add_meta()
|
|
23
26
|
|
|
24
|
-
def interpolate_to_grid(
|
|
27
|
+
def interpolate_to_grid(
|
|
28
|
+
self, time_new: npt.NDArray, height_new: npt.NDArray
|
|
29
|
+
) -> list[int]:
|
|
25
30
|
"""Interpolate beta using nearest neighbor."""
|
|
26
|
-
max_height = 100
|
|
27
|
-
max_time = 1
|
|
31
|
+
max_height = 100 # m
|
|
32
|
+
max_time = 1 / 60 # min -> fraction hour
|
|
33
|
+
|
|
34
|
+
if self.height is None:
|
|
35
|
+
msg = "Unable to interpolate lidar: no height information"
|
|
36
|
+
raise RuntimeError(msg)
|
|
28
37
|
|
|
29
|
-
#
|
|
38
|
+
# Interpolate beta to new grid but ignore profiles that are completely masked
|
|
30
39
|
beta = self.data["beta"][:]
|
|
31
|
-
indices = []
|
|
32
|
-
|
|
33
|
-
if not ma.all(b) is ma.masked:
|
|
34
|
-
indices.append(ind)
|
|
35
|
-
assert self.height is not None
|
|
36
|
-
beta_interpolated = interpolate_2d_nearest(
|
|
40
|
+
indices = [ind for ind, b in enumerate(beta) if ma.all(b) is not ma.masked]
|
|
41
|
+
beta_interp = interpolate_2d_nearest(
|
|
37
42
|
self.time[indices],
|
|
38
43
|
self.height,
|
|
39
44
|
beta[indices, :],
|
|
40
45
|
time_new,
|
|
41
46
|
height_new,
|
|
42
47
|
)
|
|
48
|
+
# Mask data points that are too far from the original grid
|
|
49
|
+
time_gap_ind = get_gap_ind(self.time[indices], time_new, max_time)
|
|
50
|
+
height_gap_ind = get_gap_ind(self.height, height_new, max_height)
|
|
51
|
+
self._mask_profiles(beta_interp, time_gap_ind, "time")
|
|
52
|
+
self._mask_profiles(beta_interp, height_gap_ind, "height")
|
|
53
|
+
self.data["beta"].data = beta_interp
|
|
54
|
+
return time_gap_ind
|
|
43
55
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
f"Unable to interpolate lidar for {len(bad_height_indices)} altitudes"
|
|
56
|
-
)
|
|
57
|
-
beta_interpolated[:, bad_height_indices] = ma.masked
|
|
58
|
-
self.data["beta"].data = beta_interpolated
|
|
59
|
-
return bad_time_indices
|
|
56
|
+
@staticmethod
|
|
57
|
+
def _mask_profiles(
|
|
58
|
+
data: ma.MaskedArray, ind: list[int], dim: Literal["time", "height"]
|
|
59
|
+
) -> None:
|
|
60
|
+
prefix = f"Unable to interpolate lidar for {len(ind)}"
|
|
61
|
+
if dim == "time" and ind:
|
|
62
|
+
logging.warning("%s time steps", prefix)
|
|
63
|
+
data[ind, :] = ma.masked
|
|
64
|
+
elif dim == "height" and ind:
|
|
65
|
+
logging.warning("%s altitudes", prefix)
|
|
66
|
+
data[:, ind] = ma.masked
|
|
60
67
|
|
|
61
68
|
def _add_meta(self) -> None:
|
|
62
69
|
self.append_data(float(self.getvar("wavelength")), "lidar_wavelength")
|
|
63
70
|
self.append_data(0.5, "beta_error")
|
|
64
71
|
self.append_data(3.0, "beta_bias")
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def _get_bad_indices(
|
|
68
|
-
original_grid: np.ndarray, new_grid: np.ndarray, threshold: float
|
|
69
|
-
) -> list:
|
|
70
|
-
indices = []
|
|
71
|
-
for ind, value in enumerate(new_grid):
|
|
72
|
-
diffu = np.abs(original_grid - value)
|
|
73
|
-
distance = diffu[diffu.argmin()]
|
|
74
|
-
if distance > threshold:
|
|
75
|
-
indices.append(ind)
|
|
76
|
-
return indices
|
cloudnetpy/categorize/melting.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Functions to find melting layer from data."""
|
|
2
|
+
|
|
2
3
|
import numpy as np
|
|
4
|
+
import numpy.typing as npt
|
|
3
5
|
from numpy import ma
|
|
4
6
|
from scipy.ndimage import gaussian_filter
|
|
5
7
|
|
|
@@ -9,7 +11,7 @@ from cloudnetpy.categorize.containers import ClassData
|
|
|
9
11
|
from cloudnetpy.constants import T0
|
|
10
12
|
|
|
11
13
|
|
|
12
|
-
def find_melting_layer(obs: ClassData, smooth: bool = True) ->
|
|
14
|
+
def find_melting_layer(obs: ClassData, *, smooth: bool = True) -> npt.NDArray:
|
|
13
15
|
"""Finds melting layer from model temperature, ldr, and velocity.
|
|
14
16
|
|
|
15
17
|
Melting layer is detected using linear depolarization ratio, *ldr*,
|
|
@@ -48,15 +50,14 @@ def find_melting_layer(obs: ClassData, smooth: bool = True) -> np.ndarray:
|
|
|
48
50
|
"""
|
|
49
51
|
melting_layer = np.zeros(obs.tw.shape, dtype=bool)
|
|
50
52
|
|
|
51
|
-
ldr_prof:
|
|
52
|
-
ldr_dprof:
|
|
53
|
-
ldr_diff:
|
|
53
|
+
ldr_prof: npt.NDArray | None = None
|
|
54
|
+
ldr_dprof: npt.NDArray | None = None
|
|
55
|
+
ldr_diff: npt.NDArray | None = None
|
|
54
56
|
width_prof = None
|
|
55
57
|
|
|
56
58
|
if hasattr(obs, "ldr"):
|
|
57
59
|
# Required for peak detection
|
|
58
|
-
diffu = np.diff(obs.ldr, axis=1)
|
|
59
|
-
assert isinstance(diffu, ma.MaskedArray)
|
|
60
|
+
diffu = ma.array(np.diff(obs.ldr, axis=1))
|
|
60
61
|
ldr_diff = diffu.filled(0)
|
|
61
62
|
|
|
62
63
|
t_range = _find_model_temperature_range(obs.model_type)
|
|
@@ -69,15 +70,24 @@ def find_melting_layer(obs: ClassData, smooth: bool = True) -> np.ndarray:
|
|
|
69
70
|
v_prof = obs.v[ind, temp_indices]
|
|
70
71
|
|
|
71
72
|
if ldr_diff is not None:
|
|
72
|
-
|
|
73
|
+
if not hasattr(obs, "ldr"):
|
|
74
|
+
msg = "ldr_diff is not None but obs.ldr does not exist"
|
|
75
|
+
raise RuntimeError(msg)
|
|
73
76
|
ldr_prof = obs.ldr[ind, temp_indices]
|
|
74
77
|
ldr_dprof = ldr_diff[ind, temp_indices]
|
|
75
78
|
|
|
76
|
-
if
|
|
79
|
+
if (ldr_prof is not None and ma.count(ldr_prof) > 3) or (
|
|
80
|
+
v_prof is not None and ma.count(v_prof) > 3
|
|
81
|
+
):
|
|
77
82
|
try:
|
|
78
|
-
|
|
83
|
+
if ldr_prof is None or ldr_dprof is None:
|
|
84
|
+
msg = "ldr_prof or ldr_dprof is None"
|
|
85
|
+
raise AssertionError(msg) # noqa: TRY301
|
|
79
86
|
indices = _find_melting_layer_from_ldr(
|
|
80
|
-
ldr_prof,
|
|
87
|
+
ldr_prof,
|
|
88
|
+
ldr_dprof,
|
|
89
|
+
v_prof,
|
|
90
|
+
z_prof,
|
|
81
91
|
)
|
|
82
92
|
except (ValueError, IndexError, AssertionError):
|
|
83
93
|
height = obs.height[temp_indices]
|
|
@@ -95,14 +105,11 @@ def find_melting_layer(obs: ClassData, smooth: bool = True) -> np.ndarray:
|
|
|
95
105
|
|
|
96
106
|
|
|
97
107
|
def _find_melting_layer_from_ldr(
|
|
98
|
-
ldr_prof:
|
|
99
|
-
ldr_dprof:
|
|
100
|
-
v_prof:
|
|
101
|
-
z_prof:
|
|
102
|
-
) ->
|
|
103
|
-
if ldr_prof is None:
|
|
104
|
-
raise ValueError
|
|
105
|
-
|
|
108
|
+
ldr_prof: npt.NDArray,
|
|
109
|
+
ldr_dprof: npt.NDArray,
|
|
110
|
+
v_prof: npt.NDArray,
|
|
111
|
+
z_prof: npt.NDArray,
|
|
112
|
+
) -> npt.NDArray | None:
|
|
106
113
|
peak = int(np.argmax(ldr_prof))
|
|
107
114
|
base, top = _basetop(ldr_dprof, peak)
|
|
108
115
|
conditions = (
|
|
@@ -114,14 +121,15 @@ def _find_melting_layer_from_ldr(
|
|
|
114
121
|
|
|
115
122
|
if all(conditions):
|
|
116
123
|
base = int(np.floor(base + (peak - base) / 2))
|
|
117
|
-
|
|
118
|
-
return indices
|
|
124
|
+
return np.arange(base, top)
|
|
119
125
|
return None
|
|
120
126
|
|
|
121
127
|
|
|
122
128
|
def _find_melting_layer_from_v(
|
|
123
|
-
v_prof:
|
|
124
|
-
|
|
129
|
+
v_prof: npt.NDArray,
|
|
130
|
+
width_prof: npt.NDArray | None,
|
|
131
|
+
height: npt.NDArray,
|
|
132
|
+
) -> npt.NDArray | None:
|
|
125
133
|
v = np.copy(v_prof[:-1])
|
|
126
134
|
v_diff = np.diff(v_prof)
|
|
127
135
|
v[v_diff < 0] = 0
|
|
@@ -146,22 +154,22 @@ def _find_melting_layer_from_v(
|
|
|
146
154
|
v_prof[base] < -2,
|
|
147
155
|
]
|
|
148
156
|
if all(conditions):
|
|
149
|
-
base =
|
|
157
|
+
base = round(top - (top - base) / 2)
|
|
150
158
|
return np.arange(base, top)
|
|
151
159
|
return None
|
|
152
160
|
|
|
153
161
|
|
|
154
|
-
def _basetop(dprof:
|
|
162
|
+
def _basetop(dprof: npt.NDArray, pind: int) -> tuple[int, int]:
|
|
155
163
|
"""Finds the base and top of ldr peak."""
|
|
156
164
|
top = droplet.ind_top(dprof, pind, len(dprof), 10, 2)
|
|
157
165
|
base = droplet.ind_base(dprof, pind, 10, 2)
|
|
158
166
|
return base, top
|
|
159
167
|
|
|
160
168
|
|
|
161
|
-
def _get_temp_indices(t_prof:
|
|
169
|
+
def _get_temp_indices(t_prof: npt.NDArray, t_range: tuple) -> npt.NDArray:
|
|
162
170
|
"""Finds indices of temperature profile covering the given range."""
|
|
163
171
|
ind = np.where((t_prof > min(t_range) + T0) & (t_prof < max(t_range) + T0))[0]
|
|
164
|
-
return np.array([]) if len(ind) == 0 else np.arange(min(ind), max(ind) + 1)
|
|
172
|
+
return np.array([]) if len(ind) == 0 else np.arange(np.min(ind), np.max(ind) + 1)
|
|
165
173
|
|
|
166
174
|
|
|
167
175
|
def _find_model_temperature_range(model_type: str) -> tuple[float, float]:
|