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
cloudnetpy/categorize/droplet.py
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
1
|
+
"""This module has functions for liquid layer detection."""
|
|
2
|
+
|
|
3
3
|
import numpy as np
|
|
4
|
+
import numpy.typing as npt
|
|
4
5
|
import scipy.signal
|
|
5
6
|
from numpy import ma
|
|
6
7
|
|
|
7
|
-
import cloudnetpy.categorize.atmos
|
|
8
8
|
from cloudnetpy import utils
|
|
9
|
+
from cloudnetpy.categorize import atmos_utils
|
|
9
10
|
from cloudnetpy.categorize.containers import ClassData
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
def correct_liquid_top(
|
|
13
14
|
obs: ClassData,
|
|
14
|
-
is_liquid:
|
|
15
|
-
is_freezing:
|
|
15
|
+
is_liquid: npt.NDArray,
|
|
16
|
+
is_freezing: npt.NDArray,
|
|
16
17
|
limit: float = 200,
|
|
17
|
-
) ->
|
|
18
|
+
) -> npt.NDArray:
|
|
18
19
|
"""Corrects lidar detected liquid cloud top using radar data.
|
|
19
20
|
|
|
20
21
|
Args:
|
|
@@ -32,9 +33,9 @@ def correct_liquid_top(
|
|
|
32
33
|
|
|
33
34
|
"""
|
|
34
35
|
is_liquid_corrected = np.copy(is_liquid)
|
|
35
|
-
liquid_tops =
|
|
36
|
+
liquid_tops = atmos_utils.find_cloud_tops(is_liquid)
|
|
36
37
|
top_above = utils.n_elements(obs.height, limit)
|
|
37
|
-
for prof, top in zip(*np.where(liquid_tops)):
|
|
38
|
+
for prof, top in zip(*np.where(liquid_tops), strict=True):
|
|
38
39
|
ind = _find_ind_above_top(is_freezing[prof, top:], top_above)
|
|
39
40
|
rad = obs.z[prof, top : top + ind + 1]
|
|
40
41
|
if not (rad.mask.all() or ~rad.mask.any()):
|
|
@@ -43,7 +44,7 @@ def correct_liquid_top(
|
|
|
43
44
|
return is_liquid_corrected
|
|
44
45
|
|
|
45
46
|
|
|
46
|
-
def _find_ind_above_top(is_freezing_from_peak:
|
|
47
|
+
def _find_ind_above_top(is_freezing_from_peak: npt.NDArray, top_above: int) -> int:
|
|
47
48
|
first_point_below_zero = np.where(is_freezing_from_peak)[0][0]
|
|
48
49
|
ind = first_point_below_zero + top_above
|
|
49
50
|
return min(len(is_freezing_from_peak) - 1, ind)
|
|
@@ -57,7 +58,7 @@ def find_liquid(
|
|
|
57
58
|
min_top_der: float = 1e-7,
|
|
58
59
|
min_lwp: float = 0,
|
|
59
60
|
min_alt: float = 100,
|
|
60
|
-
) ->
|
|
61
|
+
) -> npt.NDArray:
|
|
61
62
|
"""Estimate liquid layers from SNR-screened attenuated backscatter.
|
|
62
63
|
|
|
63
64
|
Args:
|
|
@@ -80,7 +81,7 @@ def find_liquid(
|
|
|
80
81
|
|
|
81
82
|
"""
|
|
82
83
|
|
|
83
|
-
def _is_proper_peak():
|
|
84
|
+
def _is_proper_peak() -> bool:
|
|
84
85
|
conditions = (
|
|
85
86
|
npoints >= min_points,
|
|
86
87
|
peak_width < max_width,
|
|
@@ -97,13 +98,12 @@ def find_liquid(
|
|
|
97
98
|
is_liquid = np.zeros(beta.shape, dtype=bool)
|
|
98
99
|
base_below_peak = utils.n_elements(height, 200)
|
|
99
100
|
top_above_peak = utils.n_elements(height, 150)
|
|
100
|
-
difference = np.diff(beta, axis=1)
|
|
101
|
-
assert isinstance(difference, ma.MaskedArray)
|
|
101
|
+
difference = ma.array(np.diff(beta, axis=1))
|
|
102
102
|
beta_diff = difference.filled(0)
|
|
103
103
|
beta = beta.filled(0)
|
|
104
104
|
peak_indices = _find_strong_peaks(beta, peak_amp)
|
|
105
105
|
|
|
106
|
-
for n, peak in zip(*peak_indices):
|
|
106
|
+
for n, peak in zip(*peak_indices, strict=True):
|
|
107
107
|
lprof = beta[n, :]
|
|
108
108
|
dprof = beta_diff[n, :]
|
|
109
109
|
try:
|
|
@@ -115,14 +115,14 @@ def find_liquid(
|
|
|
115
115
|
peak_width = height[top] - height[base]
|
|
116
116
|
peak_alt = height[peak] - height[0]
|
|
117
117
|
top_der = (lprof[peak] - lprof[top]) / (height[top] - height[peak])
|
|
118
|
-
is_positive_lwp = lwp_int[n]
|
|
118
|
+
is_positive_lwp = lwp_int[n] >= min_lwp
|
|
119
119
|
if _is_proper_peak():
|
|
120
120
|
is_liquid[n, base : top + 1] = True
|
|
121
121
|
|
|
122
122
|
return is_liquid
|
|
123
123
|
|
|
124
124
|
|
|
125
|
-
def ind_base(dprof:
|
|
125
|
+
def ind_base(dprof: npt.NDArray, ind_peak: int, dist: int, lim: float) -> int:
|
|
126
126
|
"""Finds base index of a peak in profile.
|
|
127
127
|
|
|
128
128
|
Return the lowermost index of profile where 1st order differences
|
|
@@ -177,7 +177,7 @@ def ind_base(dprof: np.ndarray, ind_peak: int, dist: int, lim: float) -> int:
|
|
|
177
177
|
>>> ind_base(dx, 5, 4, 10)
|
|
178
178
|
1
|
|
179
179
|
|
|
180
|
-
See
|
|
180
|
+
See Also:
|
|
181
181
|
droplet.ind_top()
|
|
182
182
|
|
|
183
183
|
"""
|
|
@@ -187,7 +187,9 @@ def ind_base(dprof: np.ndarray, ind_peak: int, dist: int, lim: float) -> int:
|
|
|
187
187
|
return start + np.where(diffs > diffs[mind] / lim)[0][0]
|
|
188
188
|
|
|
189
189
|
|
|
190
|
-
def ind_top(
|
|
190
|
+
def ind_top(
|
|
191
|
+
dprof: npt.NDArray, ind_peak: int, nprof: int, dist: int, lim: float
|
|
192
|
+
) -> int:
|
|
191
193
|
"""Finds top index of a peak in profile.
|
|
192
194
|
|
|
193
195
|
Return the uppermost index of profile where 1st order differences
|
|
@@ -213,7 +215,7 @@ def ind_top(dprof: np.ndarray, ind_peak: int, nprof: int, dist: int, lim: float)
|
|
|
213
215
|
IndexError: Can not find proper top index (probably too many masked
|
|
214
216
|
values in the profile).
|
|
215
217
|
|
|
216
|
-
See
|
|
218
|
+
See Also:
|
|
217
219
|
droplet.ind_base()
|
|
218
220
|
|
|
219
221
|
"""
|
|
@@ -223,7 +225,7 @@ def ind_top(dprof: np.ndarray, ind_peak: int, nprof: int, dist: int, lim: float)
|
|
|
223
225
|
return ind_peak + np.where(diffs < diffs[mind] / lim)[0][-1] + 1
|
|
224
226
|
|
|
225
227
|
|
|
226
|
-
def interpolate_lwp(obs: ClassData) ->
|
|
228
|
+
def interpolate_lwp(obs: ClassData) -> npt.NDArray:
|
|
227
229
|
"""Linear interpolation of liquid water path to fill masked values.
|
|
228
230
|
|
|
229
231
|
Args:
|
|
@@ -239,7 +241,7 @@ def interpolate_lwp(obs: ClassData) -> np.ndarray:
|
|
|
239
241
|
return np.interp(obs.time, obs.time[ind], obs.lwp[ind])
|
|
240
242
|
|
|
241
243
|
|
|
242
|
-
def _find_strong_peaks(data:
|
|
244
|
+
def _find_strong_peaks(data: npt.NDArray, threshold: float) -> tuple:
|
|
243
245
|
"""Finds local maximums from data (greater than *threshold*)."""
|
|
244
246
|
peaks = scipy.signal.argrelextrema(data, np.greater, order=4, axis=1)
|
|
245
247
|
strong_peaks = np.where(data[peaks] > threshold)
|
cloudnetpy/categorize/falling.py
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
"""Module to find falling hydrometeors from data."""
|
|
2
|
+
|
|
2
3
|
import numpy as np
|
|
4
|
+
import numpy.typing as npt
|
|
3
5
|
from numpy import ma
|
|
4
6
|
|
|
5
|
-
from cloudnetpy.categorize import
|
|
7
|
+
from cloudnetpy.categorize import atmos_utils
|
|
6
8
|
from cloudnetpy.categorize.containers import ClassData
|
|
7
9
|
from cloudnetpy.constants import T0
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
def find_falling_hydrometeors(
|
|
11
|
-
obs: ClassData,
|
|
12
|
-
|
|
13
|
+
obs: ClassData,
|
|
14
|
+
is_liquid: npt.NDArray,
|
|
15
|
+
is_insects: npt.NDArray,
|
|
16
|
+
) -> npt.NDArray:
|
|
13
17
|
"""Finds falling hydrometeors.
|
|
14
18
|
|
|
15
19
|
Falling hydrometeors are radar signals that are
|
|
@@ -30,56 +34,80 @@ def find_falling_hydrometeors(
|
|
|
30
34
|
Hogan R. and O'Connor E., 2004, https://bit.ly/2Yjz9DZ.
|
|
31
35
|
|
|
32
36
|
"""
|
|
33
|
-
|
|
34
37
|
falling_from_radar = _find_falling_from_radar(obs, is_insects)
|
|
35
38
|
falling_from_radar_fixed = _fix_liquid_dominated_radar(
|
|
36
|
-
obs,
|
|
39
|
+
obs,
|
|
40
|
+
falling_from_radar,
|
|
41
|
+
is_liquid,
|
|
37
42
|
)
|
|
38
43
|
cold_aerosols = _find_cold_aerosols(obs, is_liquid)
|
|
39
44
|
return falling_from_radar_fixed | cold_aerosols
|
|
40
45
|
|
|
41
46
|
|
|
42
|
-
def _find_falling_from_radar(obs: ClassData, is_insects:
|
|
47
|
+
def _find_falling_from_radar(obs: ClassData, is_insects: npt.NDArray) -> npt.NDArray:
|
|
43
48
|
is_z = ~obs.z.mask
|
|
44
49
|
no_clutter = ~obs.is_clutter
|
|
45
50
|
no_insects = ~is_insects
|
|
46
51
|
return is_z & no_clutter & no_insects
|
|
47
52
|
|
|
48
53
|
|
|
49
|
-
def _find_cold_aerosols(obs: ClassData, is_liquid:
|
|
54
|
+
def _find_cold_aerosols(obs: ClassData, is_liquid: npt.NDArray) -> npt.NDArray:
|
|
50
55
|
"""Lidar signals which are in colder than the threshold temperature
|
|
51
|
-
and threshold altitude from the ground are assumed ice.
|
|
52
|
-
|
|
56
|
+
and threshold altitude from the ground are assumed ice.
|
|
57
|
+
|
|
58
|
+
These pixels are easily mixed with aerosols at lower altitudes,
|
|
59
|
+
and at higher altitudes they could be supercooled liquid, actually.
|
|
60
|
+
This should be investigated and fixed in the future.
|
|
53
61
|
"""
|
|
54
62
|
cold_aerosols = np.zeros(is_liquid.shape, dtype=bool)
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
lidar_range = obs.height - obs.altitude
|
|
64
|
+
cold_aerosol_temperature_limit = T0 - 15
|
|
65
|
+
cold_aerosol_min_altitude = 2000
|
|
57
66
|
is_beta = ~obs.beta.mask
|
|
58
67
|
lidar_ice_indices = np.where(
|
|
59
|
-
(obs.tw.data <
|
|
68
|
+
(obs.tw.data < cold_aerosol_temperature_limit) & is_beta & ~is_liquid,
|
|
60
69
|
)
|
|
61
70
|
cold_aerosols[lidar_ice_indices] = True
|
|
62
|
-
low_range_indices = np.where(
|
|
71
|
+
low_range_indices = np.where(lidar_range < cold_aerosol_min_altitude)
|
|
63
72
|
if low_range_indices:
|
|
64
73
|
cold_aerosols[:, low_range_indices] = False
|
|
74
|
+
|
|
75
|
+
# Further investigate range gates between 2000 and 4000 m
|
|
76
|
+
# to avoid abrupt transitions from aerosol to ice.
|
|
77
|
+
altitude_limit = 4000
|
|
78
|
+
window_size = 6
|
|
79
|
+
n_beta_in_window = 2
|
|
80
|
+
for time_ind, profile in enumerate(cold_aerosols):
|
|
81
|
+
for alt_ind, is_cold_aerosol in enumerate(profile):
|
|
82
|
+
if is_cold_aerosol and lidar_range[alt_ind] < altitude_limit:
|
|
83
|
+
start_ind = max(0, alt_ind - window_size + 1)
|
|
84
|
+
end_ind = alt_ind + 1
|
|
85
|
+
n_beta_below = np.sum(is_beta[time_ind, start_ind:end_ind])
|
|
86
|
+
if n_beta_below > n_beta_in_window:
|
|
87
|
+
cold_aerosols[time_ind, alt_ind] = False
|
|
88
|
+
|
|
65
89
|
return cold_aerosols
|
|
66
90
|
|
|
67
91
|
|
|
68
92
|
def _fix_liquid_dominated_radar(
|
|
69
|
-
obs: ClassData,
|
|
70
|
-
|
|
93
|
+
obs: ClassData,
|
|
94
|
+
falling_from_radar: npt.NDArray,
|
|
95
|
+
is_liquid: npt.NDArray,
|
|
96
|
+
) -> npt.NDArray:
|
|
71
97
|
"""Radar signals inside liquid clouds are NOT ice if Z is
|
|
72
98
|
increasing in height inside the cloud.
|
|
73
99
|
"""
|
|
74
|
-
liquid_bases =
|
|
75
|
-
liquid_tops =
|
|
100
|
+
liquid_bases = atmos_utils.find_cloud_bases(is_liquid)
|
|
101
|
+
liquid_tops = atmos_utils.find_cloud_tops(is_liquid)
|
|
76
102
|
base_indices = np.where(liquid_bases)
|
|
77
103
|
top_indices = np.where(liquid_tops)
|
|
78
104
|
|
|
79
|
-
for n, base, _, top in zip(*base_indices, *top_indices):
|
|
105
|
+
for n, base, _, top in zip(*base_indices, *top_indices, strict=True):
|
|
80
106
|
z_prof = obs.z[n, :]
|
|
81
107
|
if _is_z_missing_above_liquid(z_prof, top) and _is_z_increasing(
|
|
82
|
-
z_prof,
|
|
108
|
+
z_prof,
|
|
109
|
+
base,
|
|
110
|
+
top,
|
|
83
111
|
):
|
|
84
112
|
falling_from_radar[n, base : top + 1] = False
|
|
85
113
|
|
|
@@ -87,15 +115,16 @@ def _fix_liquid_dominated_radar(
|
|
|
87
115
|
|
|
88
116
|
|
|
89
117
|
def _is_z_missing_above_liquid(z: ma.MaskedArray, ind_top: int) -> bool:
|
|
90
|
-
"""Checks
|
|
118
|
+
"""Checks if z is masked right above the liquid layer top."""
|
|
91
119
|
if ind_top == len(z) - 1:
|
|
92
120
|
return False
|
|
93
|
-
|
|
121
|
+
mask = ma.getmaskarray(z)
|
|
122
|
+
return bool(mask[ind_top + 1])
|
|
94
123
|
|
|
95
124
|
|
|
96
125
|
def _is_z_increasing(z: ma.MaskedArray, ind_base: int, ind_top: int) -> bool:
|
|
97
126
|
"""Checks is z is increasing inside the liquid cloud."""
|
|
98
|
-
|
|
99
|
-
if len(
|
|
100
|
-
return
|
|
127
|
+
z_valid = z[ind_base : ind_top + 1].compressed()
|
|
128
|
+
if len(z_valid) > 1:
|
|
129
|
+
return z_valid[-1] > z_valid[0]
|
|
101
130
|
return False
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"""Module to find freezing region from data."""
|
|
2
|
+
|
|
2
3
|
import logging
|
|
3
4
|
|
|
4
5
|
import numpy as np
|
|
6
|
+
import numpy.typing as npt
|
|
5
7
|
from numpy import ma
|
|
6
8
|
from scipy.interpolate import interp1d
|
|
7
9
|
|
|
@@ -10,7 +12,7 @@ from cloudnetpy.categorize.containers import ClassData
|
|
|
10
12
|
from cloudnetpy.constants import T0
|
|
11
13
|
|
|
12
14
|
|
|
13
|
-
def find_freezing_region(obs: ClassData, melting_layer:
|
|
15
|
+
def find_freezing_region(obs: ClassData, melting_layer: npt.NDArray) -> npt.NDArray:
|
|
14
16
|
"""Finds freezing region using the model temperature and melting layer.
|
|
15
17
|
|
|
16
18
|
Every profile that contains melting layer, subzero region starts from
|
|
@@ -34,7 +36,7 @@ def find_freezing_region(obs: ClassData, melting_layer: np.ndarray) -> np.ndarra
|
|
|
34
36
|
|
|
35
37
|
"""
|
|
36
38
|
is_freezing = np.zeros(obs.tw.shape, dtype=bool)
|
|
37
|
-
t0_alt =
|
|
39
|
+
t0_alt = find_t0_alt(obs.tw, obs.height)
|
|
38
40
|
mean_melting_alt = _find_mean_melting_alt(obs, melting_layer)
|
|
39
41
|
|
|
40
42
|
if _is_all_freezing(mean_melting_alt, t0_alt, obs.height):
|
|
@@ -50,8 +52,8 @@ def find_freezing_region(obs: ClassData, melting_layer: np.ndarray) -> np.ndarra
|
|
|
50
52
|
for n in range(len(obs.time) - win):
|
|
51
53
|
if mean_melting_alt[n : n + win].mask.all():
|
|
52
54
|
freezing_alt[n + mid_win] = t0_alt[n + mid_win]
|
|
53
|
-
|
|
54
|
-
f = interp1d(obs.time[
|
|
55
|
+
ind_valid = ~freezing_alt.mask
|
|
56
|
+
f = interp1d(obs.time[ind_valid], freezing_alt[ind_valid])
|
|
55
57
|
freezing_alt_interpolated = f(obs.time) - 1
|
|
56
58
|
for ii, alt in enumerate(freezing_alt_interpolated):
|
|
57
59
|
is_freezing[ii, obs.height > alt] = True
|
|
@@ -59,21 +61,27 @@ def find_freezing_region(obs: ClassData, melting_layer: np.ndarray) -> np.ndarra
|
|
|
59
61
|
|
|
60
62
|
|
|
61
63
|
def _is_all_freezing(
|
|
62
|
-
mean_melting_alt:
|
|
64
|
+
mean_melting_alt: npt.NDArray,
|
|
65
|
+
t0_alt: npt.NDArray,
|
|
66
|
+
height: npt.NDArray,
|
|
63
67
|
) -> bool:
|
|
64
68
|
no_detected_melting = mean_melting_alt.all() is ma.masked
|
|
65
69
|
all_temperatures_below_freezing = (t0_alt <= height[0]).all()
|
|
66
70
|
return no_detected_melting and all_temperatures_below_freezing
|
|
67
71
|
|
|
68
72
|
|
|
69
|
-
def _find_mean_melting_alt(
|
|
70
|
-
|
|
73
|
+
def _find_mean_melting_alt(
|
|
74
|
+
obs: ClassData, melting_layer: npt.NDArray
|
|
75
|
+
) -> ma.MaskedArray:
|
|
76
|
+
if melting_layer.dtype != bool:
|
|
77
|
+
msg = "melting_layer data type should be boolean"
|
|
78
|
+
raise ValueError(msg)
|
|
71
79
|
alt_array = np.tile(obs.height, (len(obs.time), 1))
|
|
72
80
|
melting_alts = ma.array(alt_array, mask=~melting_layer)
|
|
73
81
|
return ma.median(melting_alts, axis=1)
|
|
74
82
|
|
|
75
83
|
|
|
76
|
-
def
|
|
84
|
+
def find_t0_alt(temperature: npt.NDArray, height: npt.NDArray) -> npt.NDArray:
|
|
77
85
|
"""Interpolates altitudes where temperature goes below freezing.
|
|
78
86
|
|
|
79
87
|
Args:
|
|
@@ -84,14 +92,19 @@ def _find_t0_alt(temperature: np.ndarray, height: np.ndarray) -> np.ndarray:
|
|
|
84
92
|
1-D array denoting altitudes where the temperature drops below 0 deg C.
|
|
85
93
|
|
|
86
94
|
"""
|
|
87
|
-
alt:
|
|
95
|
+
alt: npt.NDArray = np.array([])
|
|
88
96
|
for prof in temperature:
|
|
89
97
|
ind = np.where(prof < T0)[0][0]
|
|
90
98
|
if ind == 0:
|
|
91
99
|
alt = np.append(alt, height[0])
|
|
92
100
|
else:
|
|
93
|
-
x =
|
|
94
|
-
|
|
95
|
-
|
|
101
|
+
x, y = zip(
|
|
102
|
+
*sorted(
|
|
103
|
+
zip(
|
|
104
|
+
prof[ind - 1 : ind + 1], height[ind - 1 : ind + 1], strict=True
|
|
105
|
+
),
|
|
106
|
+
),
|
|
107
|
+
strict=True,
|
|
108
|
+
)
|
|
96
109
|
alt = np.append(alt, np.interp(T0, x, y))
|
|
97
110
|
return alt
|
cloudnetpy/categorize/insects.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Module to find insects 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
|
|
|
@@ -10,10 +12,10 @@ from cloudnetpy.categorize.containers import ClassData
|
|
|
10
12
|
|
|
11
13
|
def find_insects(
|
|
12
14
|
obs: ClassData,
|
|
13
|
-
melting_layer:
|
|
14
|
-
liquid_layers:
|
|
15
|
+
melting_layer: npt.NDArray,
|
|
16
|
+
liquid_layers: npt.NDArray,
|
|
15
17
|
prob_lim: float = 0.8,
|
|
16
|
-
) -> tuple[
|
|
18
|
+
) -> tuple[npt.NDArray, npt.NDArray]:
|
|
17
19
|
"""Returns insect probability and boolean array of insect presence.
|
|
18
20
|
|
|
19
21
|
Insects are classified by estimating heuristic probability
|
|
@@ -51,7 +53,7 @@ def find_insects(
|
|
|
51
53
|
return is_insects, ma.masked_where(insect_prob == 0, insect_prob)
|
|
52
54
|
|
|
53
55
|
|
|
54
|
-
def _insect_probability(obs: ClassData) -> tuple[
|
|
56
|
+
def _insect_probability(obs: ClassData) -> tuple[npt.NDArray, npt.NDArray]:
|
|
55
57
|
prob = _get_probabilities(obs)
|
|
56
58
|
prob_from_ldr = _calc_prob_from_ldr(prob)
|
|
57
59
|
prob_from_others = _calc_prob_from_all(prob)
|
|
@@ -65,9 +67,9 @@ def _get_probabilities(obs: ClassData) -> dict:
|
|
|
65
67
|
lwp_interp = droplet.interpolate_lwp(obs)
|
|
66
68
|
fun = utils.array_to_probability
|
|
67
69
|
return {
|
|
68
|
-
"width": fun(obs.width, 1, 0.3, True) if hasattr(obs, "width") else 1,
|
|
69
|
-
"z_strong": fun(obs.z, 0, 8, True),
|
|
70
|
-
"z_weak": fun(obs.z, -20, 8, True),
|
|
70
|
+
"width": fun(obs.width, 1, 0.3, invert=True) if hasattr(obs, "width") else 1,
|
|
71
|
+
"z_strong": fun(obs.z, 0, 8, invert=True),
|
|
72
|
+
"z_weak": fun(obs.z, -20, 8, invert=True),
|
|
71
73
|
"ldr": fun(obs.ldr, -25, 5) if hasattr(obs, "ldr") else None,
|
|
72
74
|
"sldr": fun(obs.sldr, -25, 5) if hasattr(obs, "sldr") else None,
|
|
73
75
|
"temp_loose": fun(obs.tw, 268, 2),
|
|
@@ -79,14 +81,14 @@ def _get_probabilities(obs: ClassData) -> dict:
|
|
|
79
81
|
|
|
80
82
|
|
|
81
83
|
def _get_smoothed_v(
|
|
82
|
-
obs: ClassData,
|
|
84
|
+
obs: ClassData,
|
|
85
|
+
sigma: tuple[float, float] = (5, 5),
|
|
83
86
|
) -> ma.MaskedArray:
|
|
84
|
-
smoothed_v = gaussian_filter(obs.v, sigma)
|
|
85
|
-
|
|
86
|
-
return smoothed_v
|
|
87
|
+
smoothed_v = gaussian_filter(obs.v.filled(0), sigma)
|
|
88
|
+
return ma.masked_where(obs.v.mask, smoothed_v)
|
|
87
89
|
|
|
88
90
|
|
|
89
|
-
def _calc_prob_from_ldr(prob: dict) ->
|
|
91
|
+
def _calc_prob_from_ldr(prob: dict) -> npt.NDArray:
|
|
90
92
|
"""This is the most reliable proxy for insects."""
|
|
91
93
|
if prob["ldr"] is not None:
|
|
92
94
|
return prob["ldr"] * prob["temp_loose"]
|
|
@@ -99,16 +101,19 @@ def _calc_prob_from_ldr(prob: dict) -> np.ndarray:
|
|
|
99
101
|
return np.zeros(prob["z_strong"].shape)
|
|
100
102
|
|
|
101
103
|
|
|
102
|
-
def _calc_prob_from_all(prob: dict) ->
|
|
104
|
+
def _calc_prob_from_all(prob: dict) -> npt.NDArray:
|
|
103
105
|
"""This can be tried when LDR is not available. To detect insects without LDR
|
|
104
106
|
unambiguously is difficult and might result in many false positives and/or false
|
|
105
|
-
negatives.
|
|
107
|
+
negatives.
|
|
108
|
+
"""
|
|
106
109
|
return prob["z_weak"] * prob["temp_strict"] * prob["width"] * prob["v"]
|
|
107
110
|
|
|
108
111
|
|
|
109
112
|
def _adjust_for_radar(
|
|
110
|
-
obs: ClassData,
|
|
111
|
-
|
|
113
|
+
obs: ClassData,
|
|
114
|
+
prob: dict,
|
|
115
|
+
prob_from_others: npt.NDArray,
|
|
116
|
+
) -> npt.NDArray:
|
|
112
117
|
"""Adds radar-specific weighting to insect probabilities."""
|
|
113
118
|
if "mira" in obs.radar_type.lower():
|
|
114
119
|
prob_from_others *= prob["lwp"]
|
|
@@ -116,27 +121,34 @@ def _adjust_for_radar(
|
|
|
116
121
|
|
|
117
122
|
|
|
118
123
|
def _fill_missing_pixels(
|
|
119
|
-
prob_from_ldr:
|
|
120
|
-
|
|
124
|
+
prob_from_ldr: npt.NDArray,
|
|
125
|
+
prob_from_others: npt.NDArray,
|
|
126
|
+
) -> npt.NDArray:
|
|
121
127
|
prob_combined = np.copy(prob_from_ldr)
|
|
122
128
|
no_ldr = np.where(prob_from_ldr == 0)
|
|
123
129
|
prob_combined[no_ldr] = prob_from_others[no_ldr]
|
|
124
130
|
return prob_combined
|
|
125
131
|
|
|
126
132
|
|
|
127
|
-
def _screen_insects(
|
|
128
|
-
|
|
133
|
+
def _screen_insects(
|
|
134
|
+
insect_prob: npt.NDArray,
|
|
135
|
+
insect_prob_no_ldr: npt.NDArray,
|
|
136
|
+
melting_layer: npt.NDArray,
|
|
137
|
+
liquid_layers: npt.NDArray,
|
|
138
|
+
obs: ClassData,
|
|
139
|
+
) -> npt.NDArray:
|
|
140
|
+
def _screen_liquid_layers() -> None:
|
|
129
141
|
prob[liquid_layers == 1] = 0
|
|
130
142
|
|
|
131
|
-
def _screen_above_melting():
|
|
143
|
+
def _screen_above_melting() -> None:
|
|
132
144
|
above_melting = utils.ffill(melting_layer)
|
|
133
145
|
prob[above_melting == 1] = 0
|
|
134
146
|
|
|
135
|
-
def _screen_above_liquid():
|
|
147
|
+
def _screen_above_liquid() -> None:
|
|
136
148
|
above_liquid = utils.ffill(liquid_layers)
|
|
137
149
|
prob[(above_liquid == 1) & (insect_prob_no_ldr > 0)] = 0
|
|
138
150
|
|
|
139
|
-
def _screen_rainy_profiles():
|
|
151
|
+
def _screen_rainy_profiles() -> None:
|
|
140
152
|
prob[obs.is_rain == 1, :] = 0
|
|
141
153
|
|
|
142
154
|
prob = np.copy(insect_prob)
|