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
@@ -1,20 +1,21 @@
1
- """ This module has functions for liquid layer detection.
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: np.ndarray,
15
- is_freezing: np.ndarray,
15
+ is_liquid: npt.NDArray,
16
+ is_freezing: npt.NDArray,
16
17
  limit: float = 200,
17
- ) -> np.ndarray:
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 = cloudnetpy.categorize.atmos.find_cloud_tops(is_liquid)
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: np.ndarray, top_above: int) -> int:
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
- ) -> np.ndarray:
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] > min_lwp
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: np.ndarray, ind_peak: int, dist: int, lim: float) -> int:
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 also:
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(dprof: np.ndarray, ind_peak: int, nprof: int, dist: int, lim: float) -> int:
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 also:
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) -> np.ndarray:
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: np.ndarray, threshold: float) -> tuple:
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)
@@ -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 atmos
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, is_liquid: np.ndarray, is_insects: np.ndarray
12
- ) -> np.ndarray:
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, falling_from_radar, is_liquid
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: np.ndarray) -> np.ndarray:
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: np.ndarray) -> np.ndarray:
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. This method
52
- should be improved in the future if possible.
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
- temperature_limit = T0 - 15
56
- range_limit = 2000
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 < temperature_limit) & is_beta & ~is_liquid
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(obs.height + obs.altitude < range_limit)
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, falling_from_radar: np.ndarray, is_liquid: np.ndarray
70
- ) -> np.ndarray:
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 = atmos.find_cloud_bases(is_liquid)
75
- liquid_tops = atmos.find_cloud_tops(is_liquid)
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, base, top
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 is z is masked right above the liquid layer top."""
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
- return z.mask[ind_top + 1]
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
- z = z[ind_base : ind_top + 1].compressed()
99
- if len(z) > 1:
100
- return z[-1] > z[0]
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: np.ndarray) -> np.ndarray:
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 = _find_t0_alt(obs.tw, obs.height)
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
- ind = ~freezing_alt.mask
54
- f = interp1d(obs.time[ind], freezing_alt[ind])
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: np.ndarray, t0_alt: np.ndarray, height: np.ndarray
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(obs: ClassData, melting_layer: np.ndarray) -> ma.MaskedArray:
70
- assert melting_layer.dtype == bool
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 _find_t0_alt(temperature: np.ndarray, height: np.ndarray) -> np.ndarray:
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: np.ndarray = np.array([])
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 = prof[ind - 1 : ind + 1]
94
- y = height[ind - 1 : ind + 1]
95
- x, y = zip(*sorted(zip(x, y)))
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
@@ -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: np.ndarray,
14
- liquid_layers: np.ndarray,
15
+ melting_layer: npt.NDArray,
16
+ liquid_layers: npt.NDArray,
15
17
  prob_lim: float = 0.8,
16
- ) -> tuple[np.ndarray, np.ndarray]:
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[np.ndarray, np.ndarray]:
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, sigma: tuple[float, float] = (5, 5)
84
+ obs: ClassData,
85
+ sigma: tuple[float, float] = (5, 5),
83
86
  ) -> ma.MaskedArray:
84
- smoothed_v = gaussian_filter(obs.v, sigma)
85
- smoothed_v = ma.masked_where(obs.v.mask, smoothed_v)
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) -> np.ndarray:
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) -> np.ndarray:
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, prob: dict, prob_from_others: np.ndarray
111
- ) -> np.ndarray:
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: np.ndarray, prob_from_others: np.ndarray
120
- ) -> np.ndarray:
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(insect_prob, insect_prob_no_ldr, melting_layer, liquid_layers, obs):
128
- def _screen_liquid_layers():
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)