cloudnetpy 1.96.2__tar.gz → 1.96.4__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 (148) hide show
  1. {cloudnetpy-1.96.2/cloudnetpy.egg-info → cloudnetpy-1.96.4}/PKG-INFO +1 -1
  2. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/categorize/insects.py +24 -2
  3. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/categorize/melting.py +41 -5
  4. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/cli.py +8 -2
  5. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/disdronator/parsivel.py +17 -0
  6. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/products/advance_methods.py +14 -5
  7. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +17 -1
  8. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/version.py +1 -1
  9. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4/cloudnetpy.egg-info}/PKG-INFO +1 -1
  10. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/LICENSE +0 -0
  11. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/MANIFEST.in +0 -0
  12. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/README.md +0 -0
  13. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/__init__.py +0 -0
  14. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/arm.py +0 -0
  15. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/categorize/__init__.py +0 -0
  16. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/categorize/atmos_utils.py +0 -0
  17. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/categorize/attenuation.py +0 -0
  18. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/categorize/attenuations/__init__.py +0 -0
  19. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/categorize/attenuations/gas_attenuation.py +0 -0
  20. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/categorize/attenuations/liquid_attenuation.py +0 -0
  21. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/categorize/attenuations/melting_attenuation.py +0 -0
  22. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/categorize/attenuations/rain_attenuation.py +0 -0
  23. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/categorize/categorize.py +0 -0
  24. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/categorize/classify.py +0 -0
  25. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/categorize/containers.py +0 -0
  26. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/categorize/disdrometer.py +0 -0
  27. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/categorize/droplet.py +0 -0
  28. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/categorize/falling.py +0 -0
  29. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/categorize/freezing.py +0 -0
  30. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/categorize/lidar.py +0 -0
  31. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/categorize/model.py +0 -0
  32. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/categorize/mwr.py +0 -0
  33. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/categorize/radar.py +0 -0
  34. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/cloudnetarray.py +0 -0
  35. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/concat_lib.py +0 -0
  36. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/constants.py +0 -0
  37. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/datasource.py +0 -0
  38. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/disdronator/__init__.py +0 -0
  39. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/disdronator/lpm.py +0 -0
  40. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/disdronator/process.py +0 -0
  41. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/disdronator/rd80.py +0 -0
  42. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/disdronator/utils.py +0 -0
  43. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/exceptions.py +0 -0
  44. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/__init__.py +0 -0
  45. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/arm_ceilo.py +0 -0
  46. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/arm_ld.py +0 -0
  47. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/arm_utils.py +0 -0
  48. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/basta.py +0 -0
  49. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/bowtie.py +0 -0
  50. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/ceilo.py +0 -0
  51. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/ceilometer.py +0 -0
  52. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/cl61d.py +0 -0
  53. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/cloudnet_instrument.py +0 -0
  54. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/copernicus.py +0 -0
  55. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/da10.py +0 -0
  56. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/dealias.py +0 -0
  57. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/disdrometer.py +0 -0
  58. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/fd12p.py +0 -0
  59. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/galileo.py +0 -0
  60. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/hatpro.py +0 -0
  61. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/instruments.py +0 -0
  62. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/kazr.py +0 -0
  63. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/lufft.py +0 -0
  64. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/mira.py +0 -0
  65. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/mmcr.py +0 -0
  66. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/mrr.py +0 -0
  67. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/mwr3c.py +0 -0
  68. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/mwrlos.py +0 -0
  69. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/nc_lidar.py +0 -0
  70. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/nc_radar.py +0 -0
  71. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/pollyxt.py +0 -0
  72. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/radiometrics.py +0 -0
  73. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/rain_e_h3.py +0 -0
  74. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/rpg.py +0 -0
  75. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/rpg_reader.py +0 -0
  76. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/toa5.py +0 -0
  77. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/vaisala.py +0 -0
  78. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/weather_radar.py +0 -0
  79. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/instruments/weather_station.py +0 -0
  80. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/metadata.py +0 -0
  81. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/__init__.py +0 -0
  82. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/file_handler.py +0 -0
  83. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/metadata.py +0 -0
  84. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/model_metadata.py +0 -0
  85. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/plotting/__init__.py +0 -0
  86. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/plotting/plot_meta.py +0 -0
  87. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/plotting/plot_tools.py +0 -0
  88. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/plotting/plotting.py +0 -0
  89. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/products/__init__.py +0 -0
  90. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/products/grid_methods.py +0 -0
  91. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/products/model_products.py +0 -0
  92. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/products/observation_products.py +0 -0
  93. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/products/product_resampling.py +0 -0
  94. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/products/tools.py +0 -0
  95. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/statistics/__init__.py +0 -0
  96. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/statistics/statistical_methods.py +0 -0
  97. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/tests/__init__.py +0 -0
  98. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/tests/e2e/__init__.py +0 -0
  99. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/tests/e2e/conftest.py +0 -0
  100. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/tests/e2e/process_cf/__init__.py +0 -0
  101. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +0 -0
  102. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +0 -0
  103. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/__init__.py +0 -0
  104. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +0 -0
  105. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +0 -0
  106. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/__init__.py +0 -0
  107. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +0 -0
  108. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +0 -0
  109. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/tests/unit/__init__.py +0 -0
  110. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/tests/unit/conftest.py +0 -0
  111. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +0 -0
  112. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/tests/unit/test_model_products.py +0 -0
  113. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +0 -0
  114. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +0 -0
  115. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/tests/unit/test_plotting.py +0 -0
  116. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +0 -0
  117. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/tests/unit/test_tools.py +0 -0
  118. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/model_evaluation/utils.py +0 -0
  119. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/output.py +0 -0
  120. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/plotting/__init__.py +0 -0
  121. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/plotting/plot_meta.py +0 -0
  122. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/plotting/plotting.py +0 -0
  123. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/products/__init__.py +0 -0
  124. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/products/classification.py +0 -0
  125. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/products/cod.py +0 -0
  126. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/products/der.py +0 -0
  127. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/products/drizzle.py +0 -0
  128. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/products/drizzle_error.py +0 -0
  129. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/products/drizzle_tools.py +0 -0
  130. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/products/epsilon_lidar.py +0 -0
  131. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/products/epsilon_radar.py +0 -0
  132. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/products/ier.py +0 -0
  133. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/products/iwc.py +0 -0
  134. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/products/iwc_weather_radar.py +0 -0
  135. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/products/lwc.py +0 -0
  136. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/products/mie_lu_tables.nc +0 -0
  137. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/products/mwr_tools.py +0 -0
  138. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/products/product_tools.py +0 -0
  139. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/py.typed +0 -0
  140. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy/utils.py +0 -0
  141. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy.egg-info/SOURCES.txt +0 -0
  142. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy.egg-info/dependency_links.txt +0 -0
  143. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy.egg-info/entry_points.txt +0 -0
  144. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy.egg-info/requires.txt +0 -0
  145. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/cloudnetpy.egg-info/top_level.txt +0 -0
  146. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/docs/source/conf.py +0 -0
  147. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/pyproject.toml +0 -0
  148. {cloudnetpy-1.96.2 → cloudnetpy-1.96.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudnetpy
3
- Version: 1.96.2
3
+ Version: 1.96.4
4
4
  Summary: Python package for Cloudnet processing
5
5
  Author: Simo Tukiainen
6
6
  License: MIT License
@@ -58,7 +58,9 @@ def _insect_probability(obs: ClassData) -> tuple[npt.NDArray, npt.NDArray]:
58
58
  prob_from_ldr = _calc_prob_from_ldr(prob)
59
59
  prob_from_others = _calc_prob_from_all(prob)
60
60
  prob_from_others = _adjust_for_radar(obs, prob, prob_from_others)
61
- prob_combined = _fill_missing_pixels(prob_from_ldr, prob_from_others)
61
+ prob_from_others = _adjust_for_lidar(obs, prob_from_others)
62
+ is_ldr = ~ma.getmaskarray(obs.ldr) if hasattr(obs, "ldr") else None
63
+ prob_combined = _fill_missing_pixels(prob_from_ldr, prob_from_others, is_ldr)
62
64
  return prob_combined, prob_from_others
63
65
 
64
66
 
@@ -147,11 +149,31 @@ def _adjust_for_radar(
147
149
  return prob_from_others
148
150
 
149
151
 
152
+ def _adjust_for_lidar(
153
+ obs: ClassData,
154
+ prob_from_others: npt.NDArray,
155
+ weight: float = 0.8,
156
+ ) -> npt.NDArray:
157
+ """Reduces probability above the lidar signal, where liquid may be hidden."""
158
+ is_beta = ~ma.getmaskarray(obs.beta)
159
+ n_gates = is_beta.shape[1]
160
+ top = n_gates - np.argmax(is_beta[:, ::-1], axis=1)
161
+ top[~is_beta.any(axis=1)] = 0
162
+ above_lidar = np.arange(n_gates)[np.newaxis, :] >= top[:, np.newaxis]
163
+ prob_from_others[above_lidar] *= weight
164
+ return prob_from_others
165
+
166
+
150
167
  def _fill_missing_pixels(
151
168
  prob_from_ldr: npt.NDArray,
152
169
  prob_from_others: npt.NDArray,
170
+ is_ldr: npt.NDArray | None = None,
153
171
  ) -> npt.NDArray:
154
- return np.maximum(prob_from_ldr, prob_from_others)
172
+ """Combines probabilities trusting measured ldr over other parameters."""
173
+ prob = np.maximum(prob_from_ldr, prob_from_others)
174
+ if is_ldr is not None:
175
+ prob[is_ldr] = prob_from_ldr[is_ldr]
176
+ return prob
155
177
 
156
178
 
157
179
  def _screen_insects(
@@ -10,6 +10,10 @@ from cloudnetpy import utils
10
10
  from cloudnetpy.categorize import droplet
11
11
  from cloudnetpy.categorize.containers import ClassData
12
12
 
13
+ MIN_LAPSE_RATE = 0.004 # K m-1
14
+ MIN_LDR_COVERAGE = 0.5
15
+ T0_TOLERANCE = 1 # K
16
+
13
17
 
14
18
  def find_melting_layer(obs: ClassData, *, smooth: bool = True) -> npt.NDArray:
15
19
  """Finds melting layer from model temperature, ldr, and velocity.
@@ -27,11 +31,15 @@ def find_melting_layer(obs: ClassData, *, smooth: bool = True) -> npt.NDArray:
27
31
  The peak in *ldr* is the primary parameter we analyze. If
28
32
  *ldr* has a proper peak, and *v* < -1 m/s in the base, melting layer
29
33
  has been found. If *ldr* is missing we only analyze the behaviour
30
- of *v*, which is always present, to detect the melting layer.
34
+ of *v*, which is always present, to detect the melting layer. If *ldr*
35
+ covers the profile but shows no peak, there is no melting layer.
31
36
 
32
37
  Model temperature is used to limit the melting layer search to a certain
33
38
  temperature range around 0 C. For ECMWF the range is -4..+3, and for
34
- the rest -8..+6.
39
+ the rest -8..+6. Model temperature must also reach -1 C in the profile,
40
+ and the search is limited in altitude above the highest 0 C level,
41
+ because in temperature inversions the temperature range alone may
42
+ span several kilometers.
35
43
 
36
44
  Notes:
37
45
  This melting layer detection method is novel and needs to be validated.
@@ -63,7 +71,7 @@ def find_melting_layer(obs: ClassData, *, smooth: bool = True) -> npt.NDArray:
63
71
  t_range = _find_model_temperature_range(obs.model_type)
64
72
 
65
73
  for ind, t_prof in enumerate(obs.tw):
66
- temp_indices = _get_temp_indices(t_prof, t_range)
74
+ temp_indices = _get_temp_indices(t_prof, t_range, obs.height)
67
75
  if len(temp_indices) <= 1:
68
76
  continue
69
77
  z_prof = obs.z[ind, temp_indices]
@@ -90,6 +98,8 @@ def find_melting_layer(obs: ClassData, *, smooth: bool = True) -> npt.NDArray:
90
98
  z_prof,
91
99
  )
92
100
  except (ValueError, IndexError, AssertionError):
101
+ if _has_ldr_coverage(ldr_prof, v_prof):
102
+ continue
93
103
  height = obs.height[temp_indices]
94
104
  if hasattr(obs, "width"):
95
105
  width_prof = obs.width[ind, temp_indices]
@@ -104,6 +114,16 @@ def find_melting_layer(obs: ClassData, *, smooth: bool = True) -> npt.NDArray:
104
114
  return melting_layer
105
115
 
106
116
 
117
+ def _has_ldr_coverage(ldr_prof: npt.NDArray | None, v_prof: npt.NDArray) -> bool:
118
+ """Checks if ldr covers enough of the profile to rule out melting layer."""
119
+ if ldr_prof is None:
120
+ return False
121
+ is_v = ~ma.getmaskarray(v_prof)
122
+ is_ldr = ~ma.getmaskarray(ldr_prof)
123
+ n_ldr = np.count_nonzero(is_ldr & is_v)
124
+ return bool(n_ldr >= MIN_LDR_COVERAGE * np.count_nonzero(is_v))
125
+
126
+
107
127
  def _find_melting_layer_from_ldr(
108
128
  ldr_prof: npt.NDArray,
109
129
  ldr_dprof: npt.NDArray,
@@ -166,12 +186,28 @@ def _basetop(dprof: npt.NDArray, pind: int) -> tuple[int, int]:
166
186
  return base, top
167
187
 
168
188
 
169
- def _get_temp_indices(t_prof: npt.NDArray, t_range: tuple) -> npt.NDArray:
189
+ def _get_temp_indices(
190
+ t_prof: npt.NDArray, t_range: tuple, height: npt.NDArray | None = None
191
+ ) -> npt.NDArray:
170
192
  """Finds indices of temperature profile covering the given range."""
171
- ind = np.where((t_prof > min(t_range) + T0) & (t_prof < max(t_range) + T0))[0]
193
+ in_range = (t_prof > min(t_range) + T0) & (t_prof < max(t_range) + T0)
194
+ if height is not None:
195
+ in_range &= height <= _find_max_height(t_prof, t_range, height)
196
+ ind = np.where(in_range)[0]
172
197
  return np.array([]) if len(ind) == 0 else np.arange(np.min(ind), np.max(ind) + 1)
173
198
 
174
199
 
200
+ def _find_max_height(t_prof: npt.NDArray, t_range: tuple, height: npt.NDArray) -> float:
201
+ """Finds maximum melting layer height assuming realistic lapse rate."""
202
+ warm = np.where(t_prof >= T0)[0]
203
+ if len(warm) == 0:
204
+ # Model may be slightly too cold when melting layer is near the ground
205
+ warm = np.where(t_prof >= T0 - T0_TOLERANCE)[0]
206
+ if len(warm) == 0:
207
+ return -np.inf
208
+ return height[warm[-1]] - min(t_range) / MIN_LAPSE_RATE
209
+
210
+
175
211
  def _find_model_temperature_range(model_type: str) -> tuple[float, float]:
176
212
  """Returns temperature range around 0C for given model type."""
177
213
  if model_type == "gdas1":
@@ -928,9 +928,11 @@ def main() -> None:
928
928
  )
929
929
  parser.add_argument(
930
930
  "--show",
931
- help="Show plotted image",
931
+ nargs="?",
932
+ const=True,
932
933
  default=False,
933
- action=argparse.BooleanOptionalAction,
934
+ metavar="VARIABLES",
935
+ help="Show plotted image, optionally only given variables (comma-separated)",
934
936
  )
935
937
  parser.add_argument(
936
938
  "--dl",
@@ -980,6 +982,10 @@ def main() -> None:
980
982
  default=None,
981
983
  )
982
984
  args = parser.parse_args()
985
+ if isinstance(args.show, str):
986
+ if args.variables is None:
987
+ args.variables = args.show
988
+ args.show = True
983
989
 
984
990
  logger = logging.getLogger()
985
991
  logger.setLevel(logging.INFO)
@@ -475,6 +475,21 @@ def _read_leipzig(filename: str | PathLike) -> ParsivelOutput:
475
475
  return time, data
476
476
 
477
477
 
478
+ def _read_cloudlab(filename: str | PathLike) -> ParsivelOutput:
479
+ # Data acquired with ASDO and converted to daily netCDF files. The netCDF
480
+ # files are similar to parsivel2nc files but without "number_concentration"
481
+ # and "fall_velocity" variables.
482
+ with netCDF4.Dataset(filename) as nc:
483
+ time = cftime.num2pydate(nc["time"][:], units=nc["time"].units)
484
+ data = {
485
+ num: nc[key][:]
486
+ for key, num in PARSIVEL2NC_KEYS.items()
487
+ if key in nc.variables
488
+ }
489
+ data[13] = np.repeat(int(nc.Sensor_ID), len(time))
490
+ return time, data
491
+
492
+
478
493
  def _read_parsivel(
479
494
  filename: str | PathLike,
480
495
  telegram: Sequence[int | str | None] | None = None,
@@ -483,6 +498,8 @@ def _read_parsivel(
483
498
  ) -> ParsivelOutput:
484
499
  try:
485
500
  with netCDF4.Dataset(filename) as nc:
501
+ if hasattr(nc, "Title") and "CLOUDLAB" in nc.Title:
502
+ return _read_cloudlab(filename)
486
503
  if "number_concentration" in nc.variables:
487
504
  return _read_parsivel2nc(filename)
488
505
  if "fieldN" in nc.variables:
@@ -105,11 +105,20 @@ class AdvanceProductMethods:
105
105
  if self._obs_obj.height is None:
106
106
  msg = "No height in observation file"
107
107
  raise ValueError(msg)
108
- z_sen = [
109
- cl_tools.rebin_1d(self._obs_obj.height, self._obs_obj.z_sensitivity, h[i])
110
- for i in range(len(h))
111
- ]
112
- return np.asarray(z_sen)
108
+ # Interpolate at the model level heights: rebinning assumes evenly
109
+ # spaced levels, which maps stretched model levels to wrong heights.
110
+ obs_height = self._obs_obj.height_agl
111
+ if obs_height is None:
112
+ obs_height = self._obs_obj.height
113
+ obs_height = np.asarray(ma.filled(obs_height, np.nan), dtype=float)
114
+ z_obs = np.asarray(ma.filled(self._obs_obj.z_sensitivity, np.nan), dtype=float)
115
+ is_valid = np.isfinite(obs_height) & np.isfinite(z_obs)
116
+ model_height = np.asarray(ma.filled(h, np.nan), dtype=float)
117
+ z_sen = np.interp(
118
+ model_height, obs_height[is_valid], z_obs[is_valid], right=np.nan
119
+ )
120
+ z_sen[~np.isfinite(model_height)] = np.nan
121
+ return z_sen
113
122
 
114
123
  def filter_high_iwc_low_cf(
115
124
  self,
@@ -69,14 +69,30 @@ def test_fit_z_sensitivity(obs_file, model_file) -> None:
69
69
  obs = ObservationManager(PRODUCT, str(obs_file))
70
70
  model = ModelManager(str(model_file), MODEL, PRODUCT)
71
71
  advance = AdvanceProductMethods(model, obs)
72
+ # obs heights above ground 7, 8, 11 and 14 km with sensitivities
73
+ # 0.1, 0.2, 0.0, 1.0; levels above the radar range are NaN
72
74
  height = np.array(
73
75
  [[5000, 9000, 13000], [10000, 15000, 20000], [8000, 12000, 16000]]
74
76
  )
75
- expected = ma.masked_invalid([[np.nan, 0.15, 0.5], [0.1, 1, np.nan], [0.15, 0, 1]])
77
+ expected = np.array(
78
+ [[0.1, 0.4 / 3, 2 / 3], [0.2 / 3, np.nan, np.nan], [0.2, 1 / 3, np.nan]]
79
+ )
76
80
  result = advance.fit_z_sensitivity(height)
77
81
  testing.assert_array_almost_equal(result, expected)
78
82
 
79
83
 
84
+ def test_fit_z_sensitivity_stretched_grid(obs_file, model_file) -> None:
85
+ obs = ObservationManager(PRODUCT, str(obs_file))
86
+ model = ModelManager(str(model_file), MODEL, PRODUCT)
87
+ advance = AdvanceProductMethods(model, obs)
88
+ # unevenly spaced model levels within the radar range must all get a value
89
+ height = np.array([[7000, 7100, 7300, 7700, 8500, 10000, 13000, 14000]])
90
+ result = advance.fit_z_sensitivity(height)
91
+ assert np.isfinite(result).all()
92
+ assert result[0, 0] == pytest.approx(0.1)
93
+ assert result[0, -1] == pytest.approx(1.0)
94
+
95
+
80
96
  def test_filter_high_iwc_low_cf(obs_file, model_file) -> None:
81
97
  obs = ObservationManager(PRODUCT, str(obs_file))
82
98
  model = ModelManager(str(model_file), MODEL, PRODUCT)
@@ -1,4 +1,4 @@
1
1
  MAJOR = 1
2
2
  MINOR = 96
3
- PATCH = 2
3
+ PATCH = 4
4
4
  __version__ = f"{MAJOR}.{MINOR}.{PATCH}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudnetpy
3
- Version: 1.96.2
3
+ Version: 1.96.4
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