cloudnetpy 1.89.0__tar.gz → 1.89.2__tar.gz

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 (139) hide show
  1. {cloudnetpy-1.89.0/cloudnetpy.egg-info → cloudnetpy-1.89.2}/PKG-INFO +1 -1
  2. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/categorize/classify.py +80 -17
  3. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/categorize/insects.py +34 -7
  4. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/cli.py +67 -12
  5. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/version.py +1 -1
  6. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2/cloudnetpy.egg-info}/PKG-INFO +1 -1
  7. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/LICENSE +0 -0
  8. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/MANIFEST.in +0 -0
  9. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/README.md +0 -0
  10. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/__init__.py +0 -0
  11. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/categorize/__init__.py +0 -0
  12. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/categorize/atmos_utils.py +0 -0
  13. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/categorize/attenuation.py +0 -0
  14. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/categorize/attenuations/__init__.py +0 -0
  15. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/categorize/attenuations/gas_attenuation.py +0 -0
  16. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/categorize/attenuations/liquid_attenuation.py +0 -0
  17. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/categorize/attenuations/melting_attenuation.py +0 -0
  18. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/categorize/attenuations/rain_attenuation.py +0 -0
  19. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/categorize/categorize.py +0 -0
  20. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/categorize/containers.py +0 -0
  21. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/categorize/disdrometer.py +0 -0
  22. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/categorize/droplet.py +0 -0
  23. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/categorize/falling.py +0 -0
  24. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/categorize/freezing.py +0 -0
  25. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/categorize/itu.py +0 -0
  26. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/categorize/lidar.py +0 -0
  27. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/categorize/melting.py +0 -0
  28. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/categorize/model.py +0 -0
  29. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/categorize/mwr.py +0 -0
  30. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/categorize/radar.py +0 -0
  31. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/cloudnetarray.py +0 -0
  32. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/concat_lib.py +0 -0
  33. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/constants.py +0 -0
  34. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/datasource.py +0 -0
  35. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/disdronator/__init__.py +0 -0
  36. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/disdronator/lpm.py +0 -0
  37. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/disdronator/parsivel.py +0 -0
  38. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/disdronator/rd80.py +0 -0
  39. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/disdronator/utils.py +0 -0
  40. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/exceptions.py +0 -0
  41. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/__init__.py +0 -0
  42. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/basta.py +0 -0
  43. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/bowtie.py +0 -0
  44. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/ceilo.py +0 -0
  45. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/ceilometer.py +0 -0
  46. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/cl61d.py +0 -0
  47. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/cloudnet_instrument.py +0 -0
  48. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/copernicus.py +0 -0
  49. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/da10.py +0 -0
  50. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/disdrometer/__init__.py +0 -0
  51. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/disdrometer/common.py +0 -0
  52. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/disdrometer/parsivel.py +0 -0
  53. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/disdrometer/rd80.py +0 -0
  54. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/disdrometer/thies.py +0 -0
  55. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/fd12p.py +0 -0
  56. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/galileo.py +0 -0
  57. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/hatpro.py +0 -0
  58. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/instruments.py +0 -0
  59. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/lufft.py +0 -0
  60. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/mira.py +0 -0
  61. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/mrr.py +0 -0
  62. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/nc_lidar.py +0 -0
  63. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/nc_radar.py +0 -0
  64. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/pollyxt.py +0 -0
  65. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/radiometrics.py +0 -0
  66. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/rain_e_h3.py +0 -0
  67. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/rpg.py +0 -0
  68. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/rpg_reader.py +0 -0
  69. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/toa5.py +0 -0
  70. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/vaisala.py +0 -0
  71. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/instruments/weather_station.py +0 -0
  72. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/metadata.py +0 -0
  73. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/__init__.py +0 -0
  74. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/file_handler.py +0 -0
  75. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/metadata.py +0 -0
  76. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/model_metadata.py +0 -0
  77. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/plotting/__init__.py +0 -0
  78. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/plotting/plot_meta.py +0 -0
  79. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/plotting/plot_tools.py +0 -0
  80. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/plotting/plotting.py +0 -0
  81. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/products/__init__.py +0 -0
  82. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/products/advance_methods.py +0 -0
  83. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/products/grid_methods.py +0 -0
  84. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/products/model_products.py +0 -0
  85. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/products/observation_products.py +0 -0
  86. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/products/product_resampling.py +0 -0
  87. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/products/tools.py +0 -0
  88. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/statistics/__init__.py +0 -0
  89. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/statistics/statistical_methods.py +0 -0
  90. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/tests/__init__.py +0 -0
  91. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/tests/e2e/__init__.py +0 -0
  92. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/tests/e2e/conftest.py +0 -0
  93. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/tests/e2e/process_cf/__init__.py +0 -0
  94. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +0 -0
  95. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +0 -0
  96. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/__init__.py +0 -0
  97. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +0 -0
  98. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +0 -0
  99. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/__init__.py +0 -0
  100. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +0 -0
  101. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +0 -0
  102. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/tests/unit/__init__.py +0 -0
  103. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/tests/unit/conftest.py +0 -0
  104. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +0 -0
  105. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +0 -0
  106. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/tests/unit/test_model_products.py +0 -0
  107. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +0 -0
  108. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +0 -0
  109. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/tests/unit/test_plotting.py +0 -0
  110. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +0 -0
  111. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/tests/unit/test_tools.py +0 -0
  112. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/model_evaluation/utils.py +0 -0
  113. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/output.py +0 -0
  114. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/plotting/__init__.py +0 -0
  115. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/plotting/plot_meta.py +0 -0
  116. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/plotting/plotting.py +0 -0
  117. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/products/__init__.py +0 -0
  118. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/products/classification.py +0 -0
  119. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/products/der.py +0 -0
  120. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/products/drizzle.py +0 -0
  121. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/products/drizzle_error.py +0 -0
  122. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/products/drizzle_tools.py +0 -0
  123. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/products/epsilon.py +0 -0
  124. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/products/ier.py +0 -0
  125. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/products/iwc.py +0 -0
  126. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/products/lwc.py +0 -0
  127. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/products/mie_lu_tables.nc +0 -0
  128. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/products/mwr_tools.py +0 -0
  129. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/products/product_tools.py +0 -0
  130. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/py.typed +0 -0
  131. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy/utils.py +0 -0
  132. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy.egg-info/SOURCES.txt +0 -0
  133. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy.egg-info/dependency_links.txt +0 -0
  134. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy.egg-info/entry_points.txt +0 -0
  135. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy.egg-info/requires.txt +0 -0
  136. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/cloudnetpy.egg-info/top_level.txt +0 -0
  137. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/docs/source/conf.py +0 -0
  138. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/pyproject.toml +0 -0
  139. {cloudnetpy-1.89.0 → cloudnetpy-1.89.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudnetpy
3
- Version: 1.89.0
3
+ Version: 1.89.2
4
4
  Summary: Python package for Cloudnet processing
5
5
  Author: Simo Tukiainen
6
6
  License: MIT License
@@ -1,6 +1,7 @@
1
1
  import numpy as np
2
2
  import numpy.typing as npt
3
3
  from numpy import ma
4
+ from scipy import ndimage
4
5
 
5
6
  from cloudnetpy import utils
6
7
  from cloudnetpy.categorize import (
@@ -84,19 +85,21 @@ def classify_measurements(data: Observations) -> ClassificationResult:
84
85
  bits.droplet = droplet.correct_liquid_top(obs, is_liquid, bits.freezing, limit=500)
85
86
  bits.insect, insect_prob = insects.find_insects(obs, bits.melting, bits.droplet)
86
87
  bits.falling = falling.find_falling_hydrometeors(obs, bits.droplet, bits.insect)
87
- filtered_ice = _filter_falling(bits)
88
88
  for _ in range(5):
89
89
  _fix_undetected_melting_layer(bits)
90
90
  _filter_insects(bits)
91
91
  bits.aerosol = _find_aerosols(obs, bits)
92
- bits.aerosol[filtered_ice] = False
93
92
  _fix_super_cold_liquid(obs, bits)
94
93
  _reclassify_ice_with_high_ldr(obs, bits)
94
+ _filter_insects_in_falling(bits)
95
+ _filter_falling(bits, obs)
96
+
97
+ is_clutter = _remove_clutter_in_liquid(obs.is_clutter, bits)
95
98
 
96
99
  return ClassificationResult(
97
100
  category_bits=bits,
98
101
  is_rain=obs.is_rain,
99
- is_clutter=obs.is_clutter,
102
+ is_clutter=is_clutter,
100
103
  insect_prob=insect_prob,
101
104
  liquid_prob=liquid_prob,
102
105
  )
@@ -137,13 +140,50 @@ def _reclassify_ice_with_high_ldr(obs: ClassData, bits: CategoryBits) -> None:
137
140
  is_ice = bits.falling & bits.freezing & ~bits.droplet & ~bits.melting
138
141
  high_ldr = ~ma.getmaskarray(obs.ldr) & (obs.ldr > ldr_limit)
139
142
  warm_enough = obs.tw > temp_limit
140
- reclassify = is_ice & high_ldr & warm_enough
143
+ above_melting = utils.ffill(bits.melting)
144
+ reclassify = is_ice & high_ldr & warm_enough & ~above_melting
141
145
  bits.insect[reclassify] = True
142
146
  bits.falling[reclassify] = False
143
147
  has_lidar = ~obs.beta.mask
144
148
  bits.aerosol[reclassify & has_lidar] = True
145
149
 
146
150
 
151
+ def _filter_insects_in_falling(
152
+ bits: CategoryBits,
153
+ min_falling_size: int = 100,
154
+ dilation: int = 2,
155
+ ) -> None:
156
+ """Reclassifies insect pixels in freezing regions near large falling ice.
157
+
158
+ Insect pixels in freezing temperatures near sufficiently large falling
159
+ hydrometeor regions are almost certainly false positives. Real insects
160
+ cannot survive in freezing conditions, so the freezing constraint
161
+ protects legitimate warm-region insects from being reclassified.
162
+
163
+ Args:
164
+ bits: A :class:`CategoryBits` instance.
165
+ min_falling_size: Minimum size of nearby falling regions required
166
+ to trigger reclassification.
167
+ dilation: Number of pixels to dilate around falling regions.
168
+ """
169
+ structure = ndimage.generate_binary_structure(2, 1)
170
+ falling_labels, n_falling = ndimage.label(bits.falling, structure=structure)
171
+ if n_falling == 0:
172
+ return
173
+ falling_ids = np.arange(1, n_falling + 1)
174
+ falling_sizes = ndimage.sum(bits.falling, falling_labels, falling_ids)
175
+ large_falling = np.isin(
176
+ falling_labels, falling_ids[falling_sizes >= min_falling_size]
177
+ )
178
+ near_large_falling = ndimage.binary_dilation(
179
+ large_falling, structure, iterations=dilation
180
+ )
181
+ reclassify = bits.insect & bits.freezing & near_large_falling
182
+ bits.falling[reclassify] = True
183
+ bits.insect[reclassify] = False
184
+ bits.aerosol[reclassify] = False
185
+
186
+
147
187
  def _fix_super_cold_liquid(obs: ClassData, bits: CategoryBits) -> None:
148
188
  """Supercooled liquid droplets do not exist in atmosphere below around -38 C."""
149
189
  t_limit = T0 - 38
@@ -246,21 +286,44 @@ def _filter_insects(bits: CategoryBits) -> None:
246
286
  bits.insect = is_insects
247
287
 
248
288
 
249
- def _filter_falling(bits: CategoryBits) -> tuple:
289
+ def _remove_clutter_in_liquid(
290
+ is_clutter: npt.NDArray,
291
+ bits: CategoryBits,
292
+ ) -> npt.NDArray:
293
+ """Removes clutter that is inside liquid or falling hydrometeor layers.
294
+
295
+ Clutter detection based on near-zero velocity can produce false positives
296
+ inside cloud and drizzle layers. This function removes clutter flags from
297
+ pixels that are adjacent to droplet or falling hydrometeor pixels, and
298
+ reclassifies them as falling hydrometeors.
299
+ """
300
+ is_clutter = is_clutter.copy()
301
+ liquid_or_falling = bits.droplet | bits.falling
302
+ structure = ndimage.generate_binary_structure(2, 1)
303
+ near_liquid = ndimage.binary_dilation(liquid_or_falling, structure)
304
+ false_clutter = is_clutter & near_liquid
305
+ is_clutter[false_clutter] = False
306
+ bits.falling[false_clutter] = True
307
+ return is_clutter
308
+
309
+
310
+ def _filter_falling(bits: CategoryBits, obs: ClassData) -> None:
250
311
  # filter falling ice speckle noise
312
+ is_beta = ~obs.beta.mask
251
313
  is_freezing = bits.freezing
252
314
  is_falling = bits.falling
253
- is_falling_filtered = utils.remove_small_objects(
254
- is_falling,
255
- max_size=10,
256
- connectivity=1,
315
+ filtered_out = is_falling & ~np.asarray(
316
+ utils.remove_small_objects(
317
+ is_falling,
318
+ max_size=10,
319
+ connectivity=1,
320
+ ),
321
+ dtype=bool,
257
322
  )
258
- is_filtered = is_falling & ~np.array(is_falling_filtered)
259
- ice_ind = np.where(is_freezing & is_filtered)
260
- is_falling[ice_ind] = False
261
- # in warm these are (probably) insects
262
- insect_ind = np.where(~is_freezing & is_filtered)
263
- is_falling[insect_ind] = False
323
+ is_falling[filtered_out] = False
324
+ # In warm conditions, these are likely insects
325
+ bits.insect[filtered_out & ~is_freezing] = True
326
+ bits.aerosol[filtered_out & ~is_freezing & is_beta] = True
327
+ # In cold conditions, classify as aerosol if lidar signal is present
328
+ bits.aerosol[filtered_out & is_freezing & is_beta] = True
264
329
  bits.falling = is_falling
265
- bits.insect[insect_ind] = True
266
- return ice_ind
@@ -66,7 +66,7 @@ def _get_probabilities(obs: ClassData) -> dict:
66
66
  smooth_v = _get_smoothed_v(obs)
67
67
  lwp_interp = droplet.interpolate_lwp(obs)
68
68
  fun = utils.array_to_probability
69
- return {
69
+ prob: dict = {
70
70
  "width": fun(obs.width, 1, 0.3, invert=True) if hasattr(obs, "width") else 1,
71
71
  "z_strong": fun(obs.z, 0, 8, invert=True),
72
72
  "z_weak": fun(obs.z, -20, 8, invert=True),
@@ -78,6 +78,19 @@ def _get_probabilities(obs: ClassData) -> dict:
78
78
  "lwp": utils.transpose(fun(lwp_interp, 0.15, 0.05, invert=True)),
79
79
  "v_sigma": fun(obs.v_sigma, 0.01, 0.1),
80
80
  }
81
+ if hasattr(obs, "sldr"):
82
+ prob.update(
83
+ {
84
+ "width_narrow": fun(obs.width, 0.3, 0.1, invert=True)
85
+ if hasattr(obs, "width")
86
+ else 1,
87
+ "z_insect": fun(obs.z, -30, 5, invert=True),
88
+ "temp_sldr": fun(obs.tw, 265, 3),
89
+ "v_sigma_sldr": fun(obs.v_sigma, 0.2, 0.15),
90
+ "v_sigma_mask": ma.getmaskarray(obs.v_sigma),
91
+ }
92
+ )
93
+ return prob
81
94
 
82
95
 
83
96
  def _get_smoothed_v(
@@ -92,12 +105,26 @@ def _calc_prob_from_ldr(prob: dict) -> npt.NDArray:
92
105
  """This is the most reliable proxy for insects."""
93
106
  if prob["ldr"] is not None:
94
107
  return prob["ldr"] * prob["temp_loose"]
95
- if (
96
- prob["sldr"] is not None
97
- ): # Strong SLDR values are probably insects, weak CAN be but not necessarily
98
- p = prob["sldr"]
99
- p[p < 0.9] = ma.masked
100
- return p * prob["temp_loose"]
108
+ if prob["sldr"] is not None:
109
+ # SLDR alone is unreliable at low SNR. Use v_sigma * width as
110
+ # alternative proxy (insects show erratic motion + narrow width).
111
+ # When v_sigma is also unavailable, fall back to weak echo * narrow
112
+ # width -- temperature provides the main discrimination.
113
+ p_sldr = prob["sldr"]
114
+ p_vsig = prob["v_sigma_sldr"]
115
+ p_wn = prob["width_narrow"]
116
+ p_zi = prob["z_insect"]
117
+ # Echo fallback only where v_sigma is unavailable -- if v_sigma
118
+ # exists and is low, that's evidence against insects.
119
+ p_echo = np.where(prob["v_sigma_mask"], p_zi, 0)
120
+ p_proxy = np.maximum(p_sldr, np.maximum(p_vsig, p_echo))
121
+ p_total = p_proxy * p_wn * prob["temp_sldr"]
122
+ # High SLDR at warm temperatures are reliable as is
123
+ p_high_sldr = p_sldr * prob["temp_loose"]
124
+ high = p_high_sldr > 0.9
125
+ p_total[high] = p_high_sldr[high]
126
+ return p_total
127
+
101
128
  return np.zeros(prob["z_strong"].shape)
102
129
 
103
130
 
@@ -1,5 +1,6 @@
1
1
  import argparse
2
2
  import base64
3
+ import concurrent.futures
3
4
  import datetime
4
5
  import gzip
5
6
  import hashlib
@@ -88,21 +89,51 @@ def _process_categorize(
88
89
  input_files: dict, args: argparse.Namespace, client: APIClient
89
90
  ) -> str | None:
90
91
  cat_filepath = _create_categorize_filepath(args)
92
+ instrument_prefs = _parse_instrument_preferences(args.instrument)
93
+
94
+ def fetch_instrument(product: str) -> tuple[str, str | None]:
95
+ source = instrument_prefs.get(product)
96
+ filepath = _fetch_product(args, product, client, source=source)
97
+ if filepath is None and source:
98
+ logging.info(
99
+ "Preferred instrument '%s' not found for %s, using available",
100
+ source,
101
+ product,
102
+ )
103
+ filepath = _fetch_product(args, product, client)
104
+ return product, filepath
91
105
 
92
- input_files["model"] = _fetch_model(args, client)
93
- if input_files["model"] is None:
106
+ def fetch_mwr() -> tuple[str, str | None]:
107
+ try:
108
+ return "mwr", _fetch_mwr(args, client)
109
+ except (CloudnetAPIError, KeyError, ValueError):
110
+ logging.info("LWP data not available, continuing without it")
111
+ return "mwr", None
112
+
113
+ def fetch_model() -> tuple[str, str | None]:
114
+ return "model", _fetch_model(args, client)
115
+
116
+ with concurrent.futures.ThreadPoolExecutor() as executor:
117
+ futures = [executor.submit(fetch_model)]
118
+ for product in ("radar", "lidar", "disdrometer"):
119
+ if product not in input_files:
120
+ futures.append(executor.submit(fetch_instrument, product))
121
+
122
+ for future in concurrent.futures.as_completed(futures):
123
+ product, filepath = future.result()
124
+ if filepath:
125
+ input_files[product] = filepath
126
+
127
+ # MWR runs after other downloads because its fallback chain may
128
+ # try to download the same radar file fetched above.
129
+ mwr_key, mwr_path = fetch_mwr()
130
+ if mwr_path:
131
+ input_files[mwr_key] = mwr_path
132
+
133
+ if "model" not in input_files:
94
134
  logging.info("No model data available for this date.")
95
135
  return None
96
136
 
97
- for product in ("radar", "lidar", "disdrometer"):
98
- if product not in input_files and (
99
- filepath := _fetch_product(args, product, client)
100
- ):
101
- input_files[product] = filepath
102
-
103
- if mwr := _fetch_mwr(args, client):
104
- input_files["mwr"] = mwr
105
-
106
137
  try:
107
138
  logging.info("Processing categorize...")
108
139
  generate_categorize(cast("CategorizeInput", input_files), cat_filepath)
@@ -338,6 +369,20 @@ def _get_product_sources(
338
369
  return source_products
339
370
 
340
371
 
372
+ def _parse_instrument_preferences(args: list[str] | None) -> dict[str, str]:
373
+ """Parses instrument preferences like 'radar:mira-35' into a dict."""
374
+ if not args:
375
+ return {}
376
+ prefs = {}
377
+ for arg in args:
378
+ if ":" not in arg:
379
+ msg = f"Invalid instrument format '{arg}', expected 'product:instrument_id'"
380
+ raise argparse.ArgumentTypeError(msg)
381
+ product, instrument_id = arg.split(":", 1)
382
+ prefs[product] = instrument_id
383
+ return prefs
384
+
385
+
341
386
  def _parse_instrument(s: str) -> tuple[str, str | None]:
342
387
  if "[" in s and s.endswith("]"):
343
388
  name = s[: s.index("[")]
@@ -382,7 +427,6 @@ def _fetch_product(
382
427
  if m.instrument is not None and m.instrument.instrument_id == source
383
428
  ]
384
429
  if not meta:
385
- logging.info("No data available for %s", product)
386
430
  return None
387
431
  if len(meta) > 1:
388
432
  logging.info(
@@ -560,6 +604,17 @@ def main() -> None:
560
604
  type=lambda arg: _parse_model(arg, client),
561
605
  help="Model to use in categorize.",
562
606
  )
607
+ parser.add_argument(
608
+ "-i",
609
+ "--instrument",
610
+ type=str,
611
+ action="append",
612
+ help=(
613
+ "Preferred instrument for categorize input, e.g. "
614
+ "'radar:mira-35' or 'lidar:cl61d'. Can be specified multiple times."
615
+ ),
616
+ default=None,
617
+ )
563
618
  parser.add_argument("--input", type=Path, help="Input path", default="input/")
564
619
  parser.add_argument("--output", type=Path, help="Output path", default="output/")
565
620
  parser.add_argument(
@@ -1,4 +1,4 @@
1
1
  MAJOR = 1
2
2
  MINOR = 89
3
- PATCH = 0
3
+ PATCH = 2
4
4
  __version__ = f"{MAJOR}.{MINOR}.{PATCH}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudnetpy
3
- Version: 1.89.0
3
+ Version: 1.89.2
4
4
  Summary: Python package for Cloudnet processing
5
5
  Author: Simo Tukiainen
6
6
  License: MIT License
File without changes
File without changes
File without changes
File without changes
File without changes