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
@@ -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
@@ -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 np
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(self, time_new: np.ndarray, height_new: np.ndarray) -> list:
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.0 # m
27
- max_time = 1.0 # min
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
- # Remove completely masked profiles from the interpolation
38
+ # Interpolate beta to new grid but ignore profiles that are completely masked
30
39
  beta = self.data["beta"][:]
31
- indices = []
32
- for ind, b in enumerate(beta):
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
- # Filter profiles and range gates having data gap
45
- max_time /= 60 # to fraction hour
46
- bad_time_indices = _get_bad_indices(self.time[indices], time_new, max_time)
47
- bad_height_indices = _get_bad_indices(self.height, height_new, max_height)
48
- if bad_time_indices:
49
- logging.warning(
50
- f"Unable to interpolate lidar for {len(bad_time_indices)} time steps"
51
- )
52
- beta_interpolated[bad_time_indices, :] = ma.masked
53
- if bad_height_indices:
54
- logging.warning(
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
@@ -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) -> np.ndarray:
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: np.ndarray | None = None
52
- ldr_dprof: np.ndarray | None = None
53
- ldr_diff: np.ndarray | None = None
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
- assert hasattr(obs, "ldr")
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 ma.count(ldr_prof) > 3 or ma.count(v_prof) > 3:
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
- assert ldr_prof is not None and ldr_dprof is not None
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, ldr_dprof, v_prof, z_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: np.ndarray,
99
- ldr_dprof: np.ndarray,
100
- v_prof: np.ndarray,
101
- z_prof: np.ndarray,
102
- ) -> np.ndarray | None:
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
- indices = np.arange(base, top)
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: np.ndarray, width_prof: np.ndarray | None, height: np.ndarray
124
- ) -> np.ndarray | None:
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 = int(round(top - (top - base) / 2))
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: np.ndarray, pind: int) -> tuple[int, int]:
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: np.ndarray, t_range: tuple) -> np.ndarray:
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]: