cloudnetpy 1.57.0__tar.gz → 1.58.1__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 (120) hide show
  1. {cloudnetpy-1.57.0/cloudnetpy.egg-info → cloudnetpy-1.58.1}/PKG-INFO +1 -1
  2. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/categorize/atmos.py +1 -1
  3. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/categorize/categorize.py +26 -15
  4. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/categorize/classify.py +5 -6
  5. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/categorize/containers.py +30 -54
  6. cloudnetpy-1.58.1/cloudnetpy/categorize/disdrometer.py +53 -0
  7. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/categorize/insects.py +14 -1
  8. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/categorize/lidar.py +3 -3
  9. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/categorize/radar.py +34 -20
  10. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/cloudnetarray.py +7 -3
  11. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/constants.py +4 -0
  12. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/instruments/disdrometer/parsivel.py +42 -6
  13. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/output.py +4 -1
  14. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/plotting/plotting.py +3 -0
  15. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/products/der.py +1 -1
  16. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/products/drizzle_tools.py +6 -5
  17. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/products/product_tools.py +16 -13
  18. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/utils.py +11 -2
  19. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/version.py +2 -2
  20. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1/cloudnetpy.egg-info}/PKG-INFO +1 -1
  21. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy.egg-info/SOURCES.txt +1 -0
  22. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/LICENSE +0 -0
  23. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/MANIFEST.in +0 -0
  24. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/README.md +0 -0
  25. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/__init__.py +0 -0
  26. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/categorize/__init__.py +0 -0
  27. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/categorize/atmos_utils.py +0 -0
  28. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/categorize/droplet.py +0 -0
  29. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/categorize/falling.py +0 -0
  30. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/categorize/freezing.py +0 -0
  31. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/categorize/melting.py +0 -0
  32. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/categorize/model.py +0 -0
  33. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/categorize/mwr.py +0 -0
  34. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/concat_lib.py +0 -0
  35. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/datasource.py +0 -0
  36. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/exceptions.py +0 -0
  37. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/instruments/__init__.py +0 -0
  38. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/instruments/basta.py +0 -0
  39. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/instruments/campbell_scientific.py +0 -0
  40. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/instruments/ceilo.py +0 -0
  41. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/instruments/ceilometer.py +0 -0
  42. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/instruments/cl61d.py +0 -0
  43. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/instruments/cloudnet_instrument.py +0 -0
  44. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/instruments/copernicus.py +0 -0
  45. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/instruments/disdrometer/__init__.py +0 -0
  46. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/instruments/disdrometer/common.py +0 -0
  47. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/instruments/disdrometer/thies.py +0 -0
  48. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/instruments/galileo.py +0 -0
  49. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/instruments/hatpro.py +0 -0
  50. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/instruments/instruments.py +0 -0
  51. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/instruments/lufft.py +0 -0
  52. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/instruments/mira.py +0 -0
  53. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/instruments/mrr.py +0 -0
  54. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/instruments/nc_lidar.py +0 -0
  55. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/instruments/nc_radar.py +0 -0
  56. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/instruments/pollyxt.py +0 -0
  57. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/instruments/radiometrics.py +0 -0
  58. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/instruments/rpg.py +0 -0
  59. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/instruments/rpg_reader.py +0 -0
  60. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/instruments/vaisala.py +0 -0
  61. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/instruments/weather_station.py +0 -0
  62. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/metadata.py +0 -0
  63. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/__init__.py +0 -0
  64. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/file_handler.py +0 -0
  65. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/metadata.py +0 -0
  66. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/model_metadata.py +0 -0
  67. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/plotting/__init__.py +0 -0
  68. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/plotting/plot_meta.py +0 -0
  69. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/plotting/plot_tools.py +0 -0
  70. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/plotting/plotting.py +0 -0
  71. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/products/__init__.py +0 -0
  72. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/products/advance_methods.py +0 -0
  73. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/products/grid_methods.py +0 -0
  74. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/products/model_products.py +0 -0
  75. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/products/observation_products.py +0 -0
  76. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/products/product_resampling.py +0 -0
  77. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/products/tools.py +0 -0
  78. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/statistics/__init__.py +0 -0
  79. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/statistics/statistical_methods.py +0 -0
  80. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/tests/__init__.py +0 -0
  81. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/tests/e2e/__init__.py +0 -0
  82. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/tests/e2e/conftest.py +0 -0
  83. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/tests/e2e/process_cf/__init__.py +0 -0
  84. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +0 -0
  85. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +0 -0
  86. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/__init__.py +0 -0
  87. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +0 -0
  88. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +0 -0
  89. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/__init__.py +0 -0
  90. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +0 -0
  91. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +0 -0
  92. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/tests/unit/__init__.py +0 -0
  93. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/tests/unit/conftest.py +0 -0
  94. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +0 -0
  95. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +0 -0
  96. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/tests/unit/test_model_products.py +0 -0
  97. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +0 -0
  98. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +0 -0
  99. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/tests/unit/test_plotting.py +0 -0
  100. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +0 -0
  101. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/tests/unit/test_tools.py +0 -0
  102. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/model_evaluation/utils.py +0 -0
  103. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/plotting/__init__.py +0 -0
  104. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/plotting/plot_meta.py +0 -0
  105. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/products/__init__.py +0 -0
  106. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/products/classification.py +0 -0
  107. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/products/drizzle.py +0 -0
  108. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/products/drizzle_error.py +0 -0
  109. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/products/ier.py +0 -0
  110. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/products/iwc.py +0 -0
  111. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/products/lwc.py +0 -0
  112. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/products/mie_lu_tables.nc +0 -0
  113. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/products/mwr_tools.py +0 -0
  114. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy/py.typed +0 -0
  115. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy.egg-info/dependency_links.txt +0 -0
  116. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy.egg-info/requires.txt +0 -0
  117. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/cloudnetpy.egg-info/top_level.txt +0 -0
  118. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/docs/source/conf.py +0 -0
  119. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/pyproject.toml +0 -0
  120. {cloudnetpy-1.57.0 → cloudnetpy-1.58.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cloudnetpy
3
- Version: 1.57.0
3
+ Version: 1.58.1
4
4
  Summary: Python package for Cloudnet processing
5
5
  Author: Simo Tukiainen
6
6
  License: MIT License
@@ -219,7 +219,7 @@ class LiquidAttenuation(Attenuation):
219
219
  def _find_pixels_hard_to_correct(self) -> np.ndarray:
220
220
  melting_layer = utils.isbit(self.classification.category_bits, 3)
221
221
  hard_to_correct = np.cumsum(melting_layer, axis=1) >= 1
222
- hard_to_correct[self.classification.is_rain, :] = True
222
+ hard_to_correct[self.classification.is_rain == 1, :] = True
223
223
  attenuated = self._find_attenuated_part_of_atmosphere()
224
224
  hard_to_correct[attenuated & self.atten.mask] = True
225
225
  return hard_to_correct
@@ -1,10 +1,12 @@
1
1
  """Module that generates Cloudnet categorize file."""
2
2
  from cloudnetpy import output, utils
3
3
  from cloudnetpy.categorize import atmos, classify
4
+ from cloudnetpy.categorize.disdrometer import Disdrometer
4
5
  from cloudnetpy.categorize.lidar import Lidar
5
6
  from cloudnetpy.categorize.model import Model
6
7
  from cloudnetpy.categorize.mwr import Mwr
7
8
  from cloudnetpy.categorize.radar import Radar
9
+ from cloudnetpy.datasource import DataSource
8
10
  from cloudnetpy.exceptions import ValidTimeStampError
9
11
  from cloudnetpy.metadata import MetaData
10
12
 
@@ -59,6 +61,8 @@ def generate_categorize(
59
61
  def _interpolate_to_cloudnet_grid() -> list:
60
62
  wl_band = utils.get_wl_band(data["radar"].radar_frequency)
61
63
  data["mwr"].rebin_to_grid(time)
64
+ if is_disdrometer:
65
+ data["disdrometer"].interpolate_to_grid(time)
62
66
  data["model"].interpolate_to_common_height(wl_band)
63
67
  model_gap_ind = data["model"].interpolate_to_grid(time, height)
64
68
  radar_gap_ind = data["radar"].rebin_to_grid(time)
@@ -69,8 +73,10 @@ def generate_categorize(
69
73
  def _screen_bad_time_indices(valid_indices: list) -> None:
70
74
  n_time_full = len(time)
71
75
  data["radar"].time = time[valid_indices]
72
- for var in ("radar", "lidar", "mwr", "model"):
73
- for key, item in data[var].data.items():
76
+ for data_key, obj in data.items():
77
+ if obj is None or data_key == "lv0_files":
78
+ continue
79
+ for key, item in obj.data.items():
74
80
  if utils.isscalar(item.data):
75
81
  continue
76
82
  array = item[:]
@@ -81,26 +87,31 @@ def generate_categorize(
81
87
  array = array[valid_indices, :]
82
88
  else:
83
89
  continue
84
- data[var].data[key].data = array
90
+ obj.data[key].data = array
85
91
  for key, item in data["model"].data_dense.items():
86
92
  data["model"].data_dense[key] = item[valid_indices, :]
87
93
 
88
94
  def _prepare_output() -> dict:
89
95
  data["radar"].add_meta()
90
96
  data["model"].screen_sparse_fields()
91
- for key in ("category_bits", "rainfall_rate", "insect_prob"):
97
+ if is_disdrometer:
98
+ data["radar"].data.pop("rainfall_rate", None)
99
+ data["disdrometer"].data.pop("n_particles", None)
100
+ for key in ("category_bits", "insect_prob"):
92
101
  data["radar"].append_data(getattr(classification, key), key)
93
102
  if classification.liquid_prob is not None:
94
103
  data["radar"].append_data(classification.liquid_prob, "liquid_prob")
95
104
  for key in ("radar_liquid_atten", "radar_gas_atten"):
96
105
  data["radar"].append_data(attenuations[key], key)
97
106
  data["radar"].append_data(quality["quality_bits"], "quality_bits")
107
+ data["radar"].append_data(classification.is_rain, "rain_detected")
98
108
  return {
99
109
  **data["radar"].data,
100
110
  **data["lidar"].data,
101
111
  **data["model"].data,
102
112
  **data["model"].data_sparse,
103
113
  **data["mwr"].data,
114
+ **(data["disdrometer"].data if is_disdrometer else {}),
104
115
  }
105
116
 
106
117
  def _define_dense_grid() -> tuple:
@@ -108,19 +119,20 @@ def generate_categorize(
108
119
 
109
120
  def _close_all() -> None:
110
121
  for obj in data.values():
111
- if isinstance(obj, Radar | Lidar | Mwr | Model):
122
+ if isinstance(obj, DataSource):
112
123
  obj.close()
113
124
 
114
125
  try:
126
+ is_disdrometer = "disdrometer" in input_files
115
127
  data = {
116
128
  "radar": Radar(input_files["radar"]),
117
129
  "lidar": Lidar(input_files["lidar"]),
118
130
  "mwr": Mwr(input_files["mwr"]),
119
131
  "lv0_files": input_files.get("lv0_files", None),
132
+ "disdrometer": Disdrometer(input_files["disdrometer"])
133
+ if is_disdrometer
134
+ else None,
120
135
  }
121
- if data["radar"].altitude is None:
122
- msg = "Radar altitude not defined"
123
- raise RuntimeError(msg)
124
136
  data["model"] = Model(input_files["model"], data["radar"].altitude)
125
137
  time, height = _define_dense_grid()
126
138
  valid_ind = _interpolate_to_cloudnet_grid()
@@ -141,7 +153,7 @@ def generate_categorize(
141
153
  classification = classify.classify_measurements(data)
142
154
  attenuations = atmos.get_attenuations(data, classification)
143
155
  data["radar"].correct_atten(attenuations)
144
- data["radar"].calc_errors(attenuations, classification)
156
+ data["radar"].calc_errors(attenuations, classification.is_clutter)
145
157
  quality = classify.fetch_quality(data, classification, attenuations)
146
158
  cloudnet_arrays = _prepare_output()
147
159
  date = data["radar"].get_date()
@@ -382,12 +394,6 @@ CATEGORIZE_ATTRIBUTES = {
382
394
  definition=DEFINITIONS["quality_bits"],
383
395
  units="1",
384
396
  ),
385
- "rainfall_rate": MetaData(
386
- long_name="Rainfall rate",
387
- standard_name="rainfall_rate",
388
- units="m s-1",
389
- comment="Fill values denote rain with undefined intensity.",
390
- ),
391
397
  "radar_liquid_atten": MetaData(
392
398
  long_name="Two-way radar attenuation due to liquid water",
393
399
  units="dB",
@@ -409,4 +415,9 @@ CATEGORIZE_ATTRIBUTES = {
409
415
  units="1",
410
416
  comment=COMMENTS["liquid_prob"],
411
417
  ),
418
+ "rain_detected": MetaData(
419
+ long_name="Rain detected",
420
+ units="1",
421
+ comment="1 = rain detected, 0 = no rain detected",
422
+ ),
412
423
  }
@@ -66,12 +66,11 @@ def classify_measurements(data: dict) -> ClassificationResult:
66
66
  bits[4] = _find_aerosols(obs, bits[1], bits[0])
67
67
  bits[4][filtered_ice] = False
68
68
  return ClassificationResult(
69
- _bits_to_integer(bits),
70
- obs.is_rain,
71
- obs.is_clutter,
72
- obs.rainfall_rate,
73
- insect_prob,
74
- liquid_prob,
69
+ category_bits=_bits_to_integer(bits),
70
+ is_rain=obs.is_rain,
71
+ is_clutter=obs.is_clutter,
72
+ insect_prob=insect_prob,
73
+ liquid_prob=liquid_prob,
75
74
  )
76
75
 
77
76
 
@@ -1,11 +1,10 @@
1
- import logging
2
1
  from dataclasses import dataclass
3
2
 
4
3
  import numpy as np
5
- import skimage
6
4
  from numpy import ma
7
5
 
8
6
  from cloudnetpy import utils
7
+ from cloudnetpy.constants import MM_H_TO_M_S
9
8
 
10
9
 
11
10
  @dataclass
@@ -15,7 +14,6 @@ class ClassificationResult:
15
14
  category_bits: np.ndarray
16
15
  is_rain: np.ndarray
17
16
  is_clutter: np.ndarray
18
- rainfall_rate: np.ndarray
19
17
  insect_prob: np.ndarray
20
18
  liquid_prob: np.ndarray | None
21
19
 
@@ -42,12 +40,12 @@ class ClassData:
42
40
  radar_type (str): Radar identifier.
43
41
  is_rain (ndarray): 2D boolean array denoting rain.
44
42
  is_clutter (ndarray): 2D boolean array denoting clutter.
45
- rainfall_rate: 1D rain rate.
46
43
  altitude: site altitude.
47
44
 
48
45
  """
49
46
 
50
47
  def __init__(self, data: dict):
48
+ self.data = data
51
49
  self.z = data["radar"].data["Z"][:]
52
50
  self.v = data["radar"].data["v"][:]
53
51
  self.v_sigma = data["radar"].data["v_sigma"][:]
@@ -61,61 +59,39 @@ class ClassData:
61
59
  self.model_type = data["model"].source_type
62
60
  self.beta = data["lidar"].data["beta"][:]
63
61
  self.lwp = data["mwr"].data["lwp"][:]
64
- self.is_rain = _find_rain_from_radar_echo(self.z, self.time)
65
- self.rainfall_rate = _find_rainfall_rate(self.is_rain, data["radar"])
62
+ self.is_rain = self._find_profiles_with_rain()
66
63
  self.is_clutter = _find_clutter(self.v, self.is_rain)
67
64
  self.altitude = data["radar"].altitude
68
65
  self.lv0_files = data["lv0_files"]
69
66
  self.date = data["radar"].get_date()
70
67
 
71
-
72
- def _find_rain_from_radar_echo(
73
- z: np.ndarray,
74
- time: np.ndarray,
75
- time_buffer: int = 5,
76
- ) -> np.ndarray:
77
- """Find profiles affected by rain.
78
-
79
- Rain is present in such profiles where the radar echo in
80
- the third range gate is > 0 dB. To make sure we do not include any
81
- rainy profiles, we also flag a few profiles before and after
82
- detections as raining.
83
-
84
- Args:
85
- z: Radar echo.
86
- time: Time vector.
87
- time_buffer: Time in minutes.
88
-
89
- Returns:
90
- 1D Boolean array denoting profiles with rain.
91
-
92
- """
93
- filled = False
94
- is_rain = ma.array(z[:, 3] > 0, dtype=bool).filled(filled)
95
- is_rain = skimage.morphology.remove_small_objects(
96
- is_rain,
97
- 2,
98
- connectivity=1,
99
- ) # Filter hot pixels
100
- n_profiles = len(time)
101
- n_steps = utils.n_elements(time, time_buffer, "time")
102
- for ind in np.where(is_rain)[0]:
103
- ind1 = max(0, ind - n_steps)
104
- ind2 = min(ind + n_steps, n_profiles)
105
- is_rain[ind1 : ind2 + 1] = True
106
- return is_rain
107
-
108
-
109
- def _find_rainfall_rate(is_rain: np.ndarray, radar) -> np.ndarray:
110
- rainfall_rate = ma.zeros(len(is_rain))
111
- rainfall_rate[is_rain] = ma.masked
112
- if "rainfall_rate" in radar.data:
113
- radar_rainfall_rate = radar.data["rainfall_rate"].data
114
- ind = np.where(~radar_rainfall_rate.mask)
115
- rainfall_rate[ind] = radar_rainfall_rate[ind]
116
- else:
117
- logging.info("No measured rain rate available")
118
- return rainfall_rate
68
+ def _find_profiles_with_rain(self) -> np.ndarray:
69
+ is_rain = self._find_rain_from_radar_echo()
70
+ rain_from_disdrometer = self._find_rain_from_disdrometer()
71
+ ind = ~rain_from_disdrometer.mask
72
+ is_rain[ind] = rain_from_disdrometer[ind]
73
+ return is_rain
74
+
75
+ def _find_rain_from_radar_echo(self) -> np.ndarray:
76
+ gate_number = 3
77
+ threshold = 0
78
+ z = self.z[:, gate_number]
79
+ return np.where((~ma.getmaskarray(z)) & (z > threshold), 1, 0)
80
+
81
+ def _find_rain_from_disdrometer(self) -> ma.MaskedArray:
82
+ threshold_mm_h = 0.25 # Standard threshold for drizzle -> rain
83
+ threshold_particles = 10 # This is arbitrary and should be better tested
84
+ threshold_rate = threshold_mm_h * MM_H_TO_M_S
85
+ try:
86
+ rainfall_rate = self.data["disdrometer"].data["rainfall_rate"].data
87
+ n_particles = self.data["disdrometer"].data["n_particles"].data
88
+ is_rain = ma.array(
89
+ (rainfall_rate > threshold_rate) & (n_particles > threshold_particles),
90
+ dtype=int,
91
+ )
92
+ except (AttributeError, KeyError):
93
+ is_rain = ma.masked_all(self.time.shape, dtype=int)
94
+ return is_rain
119
95
 
120
96
 
121
97
  def _find_clutter(
@@ -0,0 +1,53 @@
1
+ """Mwr module, containing the :class:`Mwr` class."""
2
+ import logging
3
+
4
+ import numpy as np
5
+ from numpy import ma
6
+ from scipy.interpolate import interp1d
7
+
8
+ from cloudnetpy.categorize.lidar import get_gap_ind
9
+ from cloudnetpy.datasource import DataSource
10
+
11
+
12
+ class Disdrometer(DataSource):
13
+ """Disdrometer class, child of DataSource.
14
+
15
+ Args:
16
+ ----
17
+ full_path: Cloudnet Level 1b disdrometer file.
18
+
19
+ """
20
+
21
+ def __init__(self, full_path: str):
22
+ super().__init__(full_path)
23
+ self._init_rainfall_rate()
24
+
25
+ def interpolate_to_grid(self, time_grid: np.ndarray) -> None:
26
+ for key, array in self.data.items():
27
+ self.data[key].data = self._interpolate(array.data, time_grid)
28
+
29
+ def _init_rainfall_rate(self) -> None:
30
+ keys = ("rainfall_rate", "n_particles")
31
+ for key in keys:
32
+ self.append_data(self.dataset.variables[key][:], key)
33
+
34
+ def _interpolate(self, y: ma.MaskedArray, x_new: np.ndarray) -> np.ndarray:
35
+ if ma.getmask(y) is ma.nomask:
36
+ non_masked_indices = np.arange(len(y))
37
+ elif y.mask.all():
38
+ return ma.masked_all(x_new.shape)
39
+ else:
40
+ non_masked_indices = np.where(~y.mask)[0]
41
+ non_masked_values = y[non_masked_indices]
42
+ non_masked_time = self.time[non_masked_indices]
43
+ fun = interp1d(non_masked_time, non_masked_values, fill_value="extrapolate")
44
+ interpolated_array = ma.array(fun(x_new))
45
+ max_time = 1 / 60 # min -> fraction hour
46
+ mask_ind = get_gap_ind(non_masked_time, x_new, max_time)
47
+
48
+ if len(mask_ind) > 0:
49
+ msg = f"Unable to interpolate disdrometer for {len(mask_ind)} time steps"
50
+ logging.warning(msg)
51
+ interpolated_array[mask_ind] = ma.masked
52
+
53
+ return interpolated_array
@@ -147,7 +147,8 @@ def _screen_insects(
147
147
  prob[(above_liquid == 1) & (insect_prob_no_ldr > 0)] = 0
148
148
 
149
149
  def _screen_rainy_profiles() -> None:
150
- prob[obs.is_rain == 1, :] = 0
150
+ rain_smoothed = _smooth_rain(obs.time, obs.is_rain)
151
+ prob[rain_smoothed == 1, :] = 0
151
152
 
152
153
  prob = np.copy(insect_prob)
153
154
  _screen_liquid_layers()
@@ -155,3 +156,15 @@ def _screen_insects(
155
156
  _screen_above_liquid()
156
157
  _screen_rainy_profiles()
157
158
  return prob
159
+
160
+
161
+ def _smooth_rain(time: np.ndarray, is_rain: np.ndarray) -> np.ndarray:
162
+ is_rain_smoothed = np.copy(is_rain)
163
+ time_buffer = 5 # minutes
164
+ n_profiles = len(is_rain)
165
+ n_steps = utils.n_elements(time, time_buffer, "time")
166
+ for rain_idx in np.where(is_rain)[0]:
167
+ idx_start = max(0, rain_idx - n_steps)
168
+ idx_end = min(rain_idx + n_steps, n_profiles)
169
+ is_rain_smoothed[idx_start : idx_end + 1] = True
170
+ return is_rain_smoothed
@@ -44,8 +44,8 @@ class Lidar(DataSource):
44
44
  height_new,
45
45
  )
46
46
  # Mask data points that are too far from the original grid
47
- time_gap_ind = _get_gap_ind(self.time[indices], time_new, max_time)
48
- height_gap_ind = _get_gap_ind(self.height, height_new, max_height)
47
+ time_gap_ind = get_gap_ind(self.time[indices], time_new, max_time)
48
+ height_gap_ind = get_gap_ind(self.height, height_new, max_height)
49
49
  self._mask_profiles(beta_interp, time_gap_ind, "time")
50
50
  self._mask_profiles(beta_interp, height_gap_ind, "height")
51
51
  self.data["beta"].data = beta_interp
@@ -69,7 +69,7 @@ class Lidar(DataSource):
69
69
  self.append_data(3.0, "beta_bias")
70
70
 
71
71
 
72
- def _get_gap_ind(grid: np.ndarray, new_grid: np.ndarray, threshold: float) -> list[int]:
72
+ def get_gap_ind(grid: np.ndarray, new_grid: np.ndarray, threshold: float) -> list[int]:
73
73
  return [
74
74
  ind
75
75
  for ind, value in enumerate(new_grid)
@@ -7,8 +7,7 @@ from numpy import ma
7
7
  from scipy import constants
8
8
 
9
9
  from cloudnetpy import utils
10
- from cloudnetpy.categorize.classify import ClassificationResult
11
- from cloudnetpy.constants import SEC_IN_HOUR
10
+ from cloudnetpy.constants import GHZ_TO_HZ, SEC_IN_HOUR, SPEED_OF_LIGHT
12
11
  from cloudnetpy.datasource import DataSource
13
12
 
14
13
 
@@ -69,8 +68,10 @@ class Radar(DataSource):
69
68
  )
70
69
  case "v_sigma":
71
70
  array.calc_linear_std(self.time, time_new)
72
- case "width" | "rainfall_rate":
71
+ case "width":
73
72
  array.rebin_data(self.time, time_new)
73
+ case "rainfall_rate":
74
+ array.rebin_data(self.time, time_new, mask_zeros=False)
74
75
  case _:
75
76
  continue
76
77
  return bad_time_indices
@@ -214,7 +215,7 @@ class Radar(DataSource):
214
215
  def calc_errors(
215
216
  self,
216
217
  attenuations: dict,
217
- classification: ClassificationResult,
218
+ is_clutter: np.ndarray,
218
219
  ) -> None:
219
220
  """Calculates uncertainties of radar echo.
220
221
 
@@ -223,7 +224,7 @@ class Radar(DataSource):
223
224
 
224
225
  Args:
225
226
  attenuations: 2-D attenuations due to atmospheric gases.
226
- classification: The :class:`ClassificationResult` instance.
227
+ is_clutter: 2-D boolean array denoting pixels contaminated by clutter.
227
228
 
228
229
  References:
229
230
  The method is based on Hogan R. and O'Connor E., 2004,
@@ -235,17 +236,24 @@ class Radar(DataSource):
235
236
  """Returns sensitivity of radar as function of altitude."""
236
237
  mean_gas_atten = ma.mean(attenuations["radar_gas_atten"], axis=0)
237
238
  z_sensitivity = z_power_min + log_range + mean_gas_atten
238
- zc = ma.median(ma.array(z, mask=~classification.is_clutter), axis=0)
239
+ zc = ma.median(ma.array(z, mask=~is_clutter), axis=0)
239
240
  valid_values = np.logical_not(zc.mask)
240
241
  z_sensitivity[valid_values] = zc[valid_values]
241
242
  return z_sensitivity
242
243
 
243
244
  def _calc_error() -> np.ndarray | float:
244
- if "width" not in self.data:
245
- return 0.3
246
- z_precision = 4.343 * (
247
- 1 / np.sqrt(_number_of_independent_pulses())
248
- + utils.db2lin(z_power_min - z_power) / 3
245
+ """Returns error of radar as function of altitude.
246
+
247
+ References:
248
+ Hogan, R. J., 1998: Dual-wavelength radar studies of clouds.
249
+ PhD Thesis, University of Reading, UK.
250
+
251
+ """
252
+ noise_threshold = 3
253
+ n_pulses = _number_of_independent_pulses()
254
+ ln_to_log10 = 10 / np.log(10)
255
+ z_precision = (ln_to_log10 / np.sqrt(n_pulses)) * (
256
+ 1 + (utils.db2lin(z_power_min - z_power) / noise_threshold)
249
257
  )
250
258
  gas_error = attenuations["radar_gas_atten"] * 0.1
251
259
  liq_error = attenuations["liquid_atten_err"].filled(0)
@@ -254,16 +262,22 @@ class Radar(DataSource):
254
262
  return z_error
255
263
 
256
264
  def _number_of_independent_pulses() -> float:
265
+ """Returns number of independent pulses.
266
+
267
+ References:
268
+ Atlas, D., 1964: Advances in radar meteorology.
269
+ Advances in Geophys., 10, 318-478.
270
+
271
+ """
272
+ if "width" not in self.data:
273
+ default_width = 0.3
274
+ width = np.zeros_like(self.data["Z"][:])
275
+ width[~width.mask] = default_width
276
+ else:
277
+ width = self.data["width"][:]
257
278
  dwell_time = utils.mdiff(self.time) * SEC_IN_HOUR
258
- return (
259
- dwell_time
260
- * self.radar_frequency
261
- * 1e9
262
- * 4
263
- * np.sqrt(math.pi)
264
- * self.data["width"][:]
265
- / 3e8
266
- )
279
+ wl = SPEED_OF_LIGHT / (self.radar_frequency * GHZ_TO_HZ)
280
+ return 4 * np.sqrt(math.pi) * dwell_time * width / wl
267
281
 
268
282
  def _calc_z_power_min() -> float:
269
283
  if ma.all(z_power.mask):
@@ -54,7 +54,9 @@ class CloudnetArray:
54
54
  """Masks data from given indices."""
55
55
  self.data[ind] = ma.masked
56
56
 
57
- def rebin_data(self, time: np.ndarray, time_new: np.ndarray) -> list:
57
+ def rebin_data(
58
+ self, time: np.ndarray, time_new: np.ndarray, *, mask_zeros: bool = True
59
+ ) -> list:
58
60
  """Rebins `data` in time.
59
61
 
60
62
  Args:
@@ -66,12 +68,14 @@ class CloudnetArray:
66
68
 
67
69
  """
68
70
  if self.data.ndim == 1:
69
- self.data = utils.rebin_1d(time, self.data, time_new)
71
+ self.data = utils.rebin_1d(time, self.data, time_new, mask_zeros=mask_zeros)
70
72
  bad_indices = list(np.where(self.data == ma.masked)[0])
71
73
  else:
72
74
  if not isinstance(self.data, ma.MaskedArray):
73
75
  self.data = ma.masked_array(self.data)
74
- self.data, bad_indices = utils.rebin_2d(time, self.data, time_new)
76
+ self.data, bad_indices = utils.rebin_2d(
77
+ time, self.data, time_new, mask_zeros=mask_zeros
78
+ )
75
79
  return bad_indices
76
80
 
77
81
  def fetch_attributes(self) -> list:
@@ -20,8 +20,12 @@ RS: Final = 287.058
20
20
  RHO_ICE: Final = 917
21
21
 
22
22
  # other
23
+ SPEED_OF_LIGHT: Final = 3.0e8
23
24
  SEC_IN_MINUTE: Final = 60
24
25
  SEC_IN_HOUR: Final = 3600
25
26
  SEC_IN_DAY: Final = 86400
26
27
  MM_TO_M: Final = 1e-3
27
28
  G_TO_KG: Final = 1e-3
29
+ M_S_TO_MM_H: Final = SEC_IN_HOUR / MM_TO_M
30
+ MM_H_TO_M_S: Final = 1 / M_S_TO_MM_H
31
+ GHZ_TO_HZ: Final = 1e9
@@ -372,7 +372,10 @@ def _parse_spectrum(tokens: Iterator[str]) -> np.ndarray:
372
372
  return np.array(values, dtype="i2").reshape((32, 32))
373
373
 
374
374
 
375
- PARSERS: dict[str, Callable[[Iterator[str]], Any]] = {
375
+ ParserType = Callable[[Iterator[str]], Any]
376
+
377
+
378
+ PARSERS: dict[str, ParserType] = {
376
379
  "I_heating": _parse_float,
377
380
  "T_sensor": _parse_int,
378
381
  "_T_pcb": _parse_int,
@@ -401,6 +404,16 @@ PARSERS: dict[str, Callable[[Iterator[str]], Any]] = {
401
404
  "visibility": _parse_int,
402
405
  }
403
406
 
407
+ EMPTY_VALUES: dict[ParserType, Any] = {
408
+ _parse_int: 0,
409
+ _parse_float: 0.0,
410
+ _parse_date: datetime.date(2000, 1, 1),
411
+ _parse_time: datetime.time(12, 0, 0),
412
+ _parse_datetime: datetime.datetime(2000, 1, 1),
413
+ _parse_vector: np.zeros(32, dtype=float),
414
+ _parse_spectrum: np.zeros((32, 32), dtype="i2"),
415
+ }
416
+
404
417
 
405
418
  def _parse_headers(line: str) -> list[str]:
406
419
  return [CSV_HEADERS[header.strip()] for header in line.split(";")]
@@ -508,7 +521,7 @@ def _read_toa5(filename: str | PathLike) -> dict[str, list]:
508
521
  return data
509
522
 
510
523
 
511
- def _read_typ_op4a(lines: list[str]) -> dict[str, list]:
524
+ def _read_typ_op4a(lines: list[str]) -> dict[str, Any]:
512
525
  """Read output of "CS/PA" command. The output starts with line "TYP OP4A"
513
526
  followed by one line per measured variable in format: <number>:<value>.
514
527
  Output ends with characters: <ETX><CR><LF><NUL>. Lines are separated by
@@ -527,7 +540,7 @@ def _read_typ_op4a(lines: list[str]) -> dict[str, list]:
527
540
  continue
528
541
  parser = PARSERS.get(varname, next)
529
542
  tokens = value.split(";")
530
- data[varname] = [parser(iter(tokens))]
543
+ data[varname] = parser(iter(tokens))
531
544
  return data
532
545
 
533
546
 
@@ -538,15 +551,26 @@ def _read_fmi(content: str):
538
551
  - output of "CS/PA" command without non-printable characters at the end
539
552
  - "]\n"
540
553
  """
541
- output: dict[str, Any] = defaultdict(list)
554
+ output: dict[str, list] = {"_datetime": []}
542
555
  for m in re.finditer(
543
556
  r"\[(?P<year>\d+)-(?P<month>\d+)-(?P<day>\d+) "
544
557
  r"(?P<hour>\d+):(?P<minute>\d+):(?P<second>\d+)"
545
558
  r"(?P<output>[^\]]*)\]",
546
559
  content,
547
560
  ):
548
- for key, value in _read_typ_op4a(m["output"].splitlines()).items():
561
+ try:
562
+ record = _read_typ_op4a(m["output"].splitlines())
563
+ except ValueError:
564
+ continue
565
+
566
+ for key, value in record.items():
567
+ if key not in output:
568
+ output[key] = [None] * len(output["_datetime"])
549
569
  output[key].append(value)
570
+ for key in output:
571
+ if key not in record and key != "_datetime":
572
+ output[key].append(None)
573
+
550
574
  output["_datetime"].append(
551
575
  datetime.datetime(
552
576
  int(m["year"]),
@@ -577,6 +601,7 @@ def _read_parsivel(
577
601
  data = _read_toa5(filename)
578
602
  elif "TYP OP4A" in lines[0]:
579
603
  data = _read_typ_op4a(lines)
604
+ data = {key: [value] for key, value in data.items()}
580
605
  elif "Date" in lines[0]:
581
606
  headers = _parse_headers(lines[0])
582
607
  data = _read_rows(headers, lines[1:])
@@ -597,6 +622,17 @@ def _read_parsivel(
597
622
  combined_data[key].extend(values)
598
623
  if timestamps is not None:
599
624
  combined_data["_datetime"] = list(timestamps)
600
- result = {key: np.array(value) for key, value in combined_data.items()}
625
+ result = {}
626
+ for key, value in combined_data.items():
627
+ array = np.array(
628
+ [
629
+ x
630
+ if x is not None
631
+ else (EMPTY_VALUES[PARSERS[key]] if key in PARSERS else "")
632
+ for x in value
633
+ ]
634
+ )
635
+ mask = [np.full(array.shape[1:], x is None) for x in value]
636
+ result[key] = ma.array(array, mask=mask)
601
637
  result["time"] = result["_datetime"].astype("datetime64[s]")
602
638
  return result
@@ -317,7 +317,7 @@ def add_time_attribute(
317
317
 
318
318
 
319
319
  def add_source_attribute(attributes: dict, data: dict) -> dict:
320
- """Adds source attribute."""
320
+ """Adds source attribute to variables."""
321
321
  variables = {
322
322
  "radar": (
323
323
  "v",
@@ -334,8 +334,11 @@ def add_source_attribute(attributes: dict, data: dict) -> dict:
334
334
  "lidar": ("beta", "lidar_wavelength"),
335
335
  "mwr": ("lwp",),
336
336
  "model": ("uwind", "vwind", "Tw", "q", "pressure", "temperature"),
337
+ "disdrometer": ("rainfall_rate",),
337
338
  }
338
339
  for instrument, keys in variables.items():
340
+ if data[instrument] is None:
341
+ continue
339
342
  source = data[instrument].dataset.source
340
343
  for key in keys:
341
344
  if key in attributes:
@@ -514,6 +514,9 @@ class Plot2D(Plot):
514
514
  if figure_data.height is None:
515
515
  msg = "No height information in the file."
516
516
  raise ValueError(msg)
517
+ if self._data.ndim < 2:
518
+ msg = "Data has to be 2D."
519
+ raise PlottingError(msg)
517
520
  alt = figure_data.height
518
521
  if figure_data.options.max_y is None:
519
522
  return alt