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,13 +1,17 @@
1
1
  """Radar module, containing the :class:`Radar` class."""
2
+
2
3
  import logging
3
4
  import math
5
+ from os import PathLike
4
6
 
5
7
  import numpy as np
8
+ import numpy.typing as npt
6
9
  from numpy import ma
7
10
  from scipy import constants
8
11
 
9
12
  from cloudnetpy import utils
10
- from cloudnetpy.categorize.classify import ClassificationResult
13
+ from cloudnetpy.categorize.attenuations import RadarAttenuation
14
+ from cloudnetpy.constants import GHZ_TO_HZ, SEC_IN_HOUR, SPEED_OF_LIGHT
11
15
  from cloudnetpy.datasource import DataSource
12
16
 
13
17
 
@@ -22,9 +26,7 @@ class Radar(DataSource):
22
26
  folding_velocity (float): Radar's folding velocity (m/s).
23
27
  location (str): Location of the radar, copied from the global attribute
24
28
  `location` of the input file.
25
- sequence_indices (list): Indices denoting the different altitude
26
- regimes of the radar.
27
- type (str): Type of the radar, copied from the global attribute
29
+ source_type (str): Type of the radar, copied from the global attribute
28
30
  `source` of the *radar_file*. Can be free form string but must
29
31
  include either 'rpg' or 'mira' denoting one of the two supported
30
32
  radars.
@@ -34,22 +36,21 @@ class Radar(DataSource):
34
36
 
35
37
  """
36
38
 
37
- def __init__(self, full_path: str):
39
+ def __init__(self, full_path: str | PathLike) -> None:
38
40
  super().__init__(full_path, radar=True)
39
41
  self.radar_frequency = float(self.getvar("radar_frequency"))
40
- self.folding_velocity = self._get_folding_velocity()
41
- self.sequence_indices = self._get_sequence_indices()
42
42
  self.location = getattr(self.dataset, "location", "")
43
- self.type = getattr(self.dataset, "source", "")
43
+ self.source_type = getattr(self.dataset, "source", "")
44
+ self.height: npt.NDArray
45
+ self.height_agl: npt.NDArray
46
+ self.altitude: float
44
47
  self._init_data()
45
- self._init_sigma_v()
46
- self._get_folding_velocity_full()
47
48
 
48
- def rebin_to_grid(self, time_new: np.ndarray) -> list:
49
- """Rebins radar data in time using mean.
49
+ def rebin_to_grid(self, time_new: npt.NDArray) -> list:
50
+ """Rebins radar data in time.
50
51
 
51
52
  Args:
52
- time_new: Target time array as fraction hour. Updates *time* attribute.
53
+ time_new: Target time array as fraction hour.
53
54
 
54
55
  """
55
56
  bad_time_indices = []
@@ -60,19 +61,25 @@ class Radar(DataSource):
60
61
  bad_time_indices = array.rebin_data(self.time, time_new)
61
62
  array.lin2db()
62
63
  case "v":
63
- array.rebin_velocity(
64
- self.time,
64
+ array.data = self._rebin_velocity(
65
+ array.data,
65
66
  time_new,
66
- self.folding_velocity,
67
- self.sequence_indices,
68
67
  )
69
68
  case "v_sigma":
70
- array.calc_linear_std(self.time, time_new)
71
- case "width" | "rainfall_rate":
69
+ array.data, _ = utils.rebin_2d(
70
+ self.time,
71
+ array.data,
72
+ time_new,
73
+ "std",
74
+ mask_zeros=True,
75
+ )
76
+ case "width":
77
+ array.rebin_data(self.time, time_new)
78
+ case "rainfall_rate":
72
79
  array.rebin_data(self.time, time_new)
73
80
  case _:
74
81
  continue
75
- return bad_time_indices
82
+ return list(bad_time_indices)
76
83
 
77
84
  def remove_incomplete_pixels(self) -> None:
78
85
  """Mask radar pixels where one or more required quantities are missing.
@@ -83,7 +90,7 @@ class Radar(DataSource):
83
90
 
84
91
  """
85
92
  good_ind = ~ma.getmaskarray(self.data["Z"][:]) & ~ma.getmaskarray(
86
- self.data["v"][:]
93
+ self.data["v"][:],
87
94
  )
88
95
 
89
96
  if "width" in self.data:
@@ -121,20 +128,33 @@ class Radar(DataSource):
121
128
  if n_profiles_with_data < 300:
122
129
  return
123
130
  n_vertical = self._filter(
124
- data, 1, min_coverage=0.5, z_limit=10, distance=4, n_blocks=100
131
+ data,
132
+ axis=1,
133
+ min_coverage=0.5,
134
+ z_limit=10,
135
+ distance=4,
136
+ n_blocks=100,
125
137
  )
126
138
  n_horizontal = self._filter(
127
- data, 0, min_coverage=0.3, z_limit=-30, distance=3, n_blocks=20
139
+ data,
140
+ axis=0,
141
+ min_coverage=0.3,
142
+ z_limit=-30,
143
+ distance=3,
144
+ n_blocks=20,
128
145
  )
129
146
  if n_vertical > 0 or n_horizontal > 0:
130
147
  logging.debug(
131
- f"Filtered {n_vertical} vertical and {n_horizontal} horizontal stripes "
132
- f"from radar data using {variable}"
148
+ "Filtered %s vertical and %s horizontal stripes "
149
+ "from radar data using %s",
150
+ n_vertical,
151
+ n_horizontal,
152
+ variable,
133
153
  )
134
154
 
135
155
  def _filter(
136
156
  self,
137
- data: np.ndarray,
157
+ data: npt.NDArray,
138
158
  axis: int,
139
159
  min_coverage: float,
140
160
  z_limit: float,
@@ -159,10 +179,14 @@ class Radar(DataSource):
159
179
  q3 = np.quantile(n_values, 0.75)
160
180
  except IndexError:
161
181
  continue
182
+
183
+ if q1 == q3:
184
+ continue
185
+
162
186
  threshold = distance * (q3 - q1) + q3
163
187
 
164
188
  indices = np.where(
165
- (n_values > threshold) & (n_values > (min_coverage * data.shape[1]))
189
+ (n_values > threshold) & (n_values > (min_coverage * data.shape[1])),
166
190
  )[0]
167
191
  true_ind = [int(x) for x in (block_number * len_block + indices)]
168
192
  n_removed = len(indices)
@@ -180,25 +204,30 @@ class Radar(DataSource):
180
204
 
181
205
  return n_removed_total
182
206
 
183
- def correct_atten(self, attenuations: dict) -> None:
207
+ def correct_atten(self, attenuations: RadarAttenuation) -> None:
184
208
  """Corrects radar echo for liquid and gas attenuation.
185
209
 
186
210
  Args:
187
- attenuations: 2-D attenuations due to atmospheric gases and liquid:
188
- `radar_gas_atten`, `radar_liquid_atten`.
211
+ attenuations: Radar attenuation object.
189
212
 
190
213
  References:
191
214
  The method is based on Hogan R. and O'Connor E., 2004,
192
215
  https://bit.ly/2Yjz9DZ and the original Cloudnet Matlab implementation.
193
216
 
194
217
  """
195
- z_corrected = self.data["Z"][:] + attenuations["radar_gas_atten"]
196
- ind = ma.where(attenuations["radar_liquid_atten"])
197
- z_corrected[ind] += attenuations["radar_liquid_atten"][ind]
218
+ z_corrected = self.data["Z"][:] + attenuations.gas.amount
219
+ ind = ma.where(attenuations.liquid.amount)
220
+ z_corrected[ind] += attenuations.liquid.amount[ind]
221
+ ind = ma.where(attenuations.rain.amount)
222
+ z_corrected[ind] += attenuations.rain.amount[ind]
223
+ ind = ma.where(attenuations.melting.amount)
224
+ z_corrected[ind] += attenuations.melting.amount[ind]
198
225
  self.append_data(z_corrected, "Z")
199
226
 
200
227
  def calc_errors(
201
- self, attenuations: dict, classification: ClassificationResult
228
+ self,
229
+ attenuations: RadarAttenuation,
230
+ is_clutter: npt.NDArray,
202
231
  ) -> None:
203
232
  """Calculates uncertainties of radar echo.
204
233
 
@@ -207,7 +236,7 @@ class Radar(DataSource):
207
236
 
208
237
  Args:
209
238
  attenuations: 2-D attenuations due to atmospheric gases.
210
- classification: The :class:`ClassificationResult` instance.
239
+ is_clutter: 2-D boolean array denoting pixels contaminated by clutter.
211
240
 
212
241
  References:
213
242
  The method is based on Hogan R. and O'Connor E., 2004,
@@ -215,40 +244,62 @@ class Radar(DataSource):
215
244
 
216
245
  """
217
246
 
218
- def _calc_sensitivity() -> np.ndarray:
247
+ def _calc_sensitivity() -> npt.NDArray:
219
248
  """Returns sensitivity of radar as function of altitude."""
220
- mean_gas_atten = ma.mean(attenuations["radar_gas_atten"], axis=0)
249
+ mean_gas_atten = ma.mean(attenuations.gas.amount, axis=0)
221
250
  z_sensitivity = z_power_min + log_range + mean_gas_atten
222
- zc = ma.median(ma.array(z, mask=~classification.is_clutter), axis=0)
251
+ zc = ma.median(ma.array(z, mask=~is_clutter), axis=0)
223
252
  valid_values = np.logical_not(zc.mask)
224
253
  z_sensitivity[valid_values] = zc[valid_values]
225
254
  return z_sensitivity
226
255
 
227
- def _calc_error() -> np.ndarray | float:
228
- if "width" not in self.data:
229
- return 0.3
230
- z_precision = 4.343 * (
231
- 1 / np.sqrt(_number_of_independent_pulses())
232
- + utils.db2lin(z_power_min - z_power) / 3
256
+ def _calc_error() -> npt.NDArray | float:
257
+ """Returns error of radar as function of altitude.
258
+
259
+ References:
260
+ Hogan, R. J., 1998: Dual-wavelength radar studies of clouds.
261
+ PhD Thesis, University of Reading, UK.
262
+
263
+ """
264
+ noise_threshold = 3
265
+ n_pulses = _number_of_independent_pulses()
266
+ ln_to_log10 = 10 / np.log(10)
267
+ z_precision = ma.divide(ln_to_log10, np.sqrt(n_pulses)) * (
268
+ 1 + (utils.db2lin(z_power_min - z_power) / noise_threshold)
269
+ )
270
+
271
+ z_error = utils.l2norm(
272
+ z_precision,
273
+ attenuations.liquid.error.filled(0),
274
+ attenuations.rain.error.filled(0),
275
+ attenuations.melting.error.filled(0),
233
276
  )
234
- gas_error = attenuations["radar_gas_atten"] * 0.1
235
- liq_error = attenuations["liquid_atten_err"].filled(0)
236
- z_error = utils.l2norm(gas_error, liq_error, z_precision)
237
- z_error[attenuations["liquid_uncorrected"]] = ma.masked
277
+
278
+ z_error[
279
+ attenuations.liquid.uncorrected
280
+ | attenuations.rain.uncorrected
281
+ | attenuations.melting.uncorrected
282
+ ] = ma.masked
283
+
238
284
  return z_error
239
285
 
240
286
  def _number_of_independent_pulses() -> float:
241
- seconds_in_hour = 3600
242
- dwell_time = utils.mdiff(self.time) * seconds_in_hour
243
- return (
244
- dwell_time
245
- * self.radar_frequency
246
- * 1e9
247
- * 4
248
- * np.sqrt(math.pi)
249
- * self.data["width"][:]
250
- / 3e8
251
- )
287
+ """Returns number of independent pulses.
288
+
289
+ References:
290
+ Atlas, D., 1964: Advances in radar meteorology.
291
+ Advances in Geophys., 10, 318-478.
292
+
293
+ """
294
+ if "width" not in self.data:
295
+ default_width = 0.3
296
+ width = np.zeros_like(self.data["Z"][:])
297
+ width[~width.mask] = default_width
298
+ else:
299
+ width = self.data["width"][:]
300
+ dwell_time = utils.mdiff(self.time) * SEC_IN_HOUR
301
+ wl = SPEED_OF_LIGHT / (self.radar_frequency * GHZ_TO_HZ)
302
+ return 4 * np.sqrt(math.pi) * dwell_time * width / wl
252
303
 
253
304
  def _calc_z_power_min() -> float:
254
305
  if ma.all(z_power.mask):
@@ -256,7 +307,7 @@ class Radar(DataSource):
256
307
  return np.percentile(z_power.compressed(), 0.1)
257
308
 
258
309
  z = self.data["Z"][:]
259
- radar_range = self.km2m(self.dataset.variables["range"])
310
+ radar_range = self.to_m(self.dataset.variables["range"])
260
311
  log_range = utils.lin2db(radar_range, scale=20)
261
312
  z_power = z - log_range
262
313
  z_power_min = _calc_z_power_min()
@@ -266,57 +317,119 @@ class Radar(DataSource):
266
317
 
267
318
  def add_meta(self) -> None:
268
319
  """Copies misc. metadata from the input file."""
269
- for key in ("latitude", "longitude", "altitude"):
270
- self.append_data(np.array(self.getvar(key)), key)
271
320
  for key in ("time", "height", "radar_frequency"):
272
321
  self.append_data(np.array(getattr(self, key)), key)
273
322
 
274
- def _init_data(self):
323
+ def add_location(self, time_new: npt.NDArray) -> None:
324
+ """Add latitude, longitude and altitude from nearest timestamp."""
325
+ idx = np.searchsorted(self.time, time_new)
326
+ idx_left = np.clip(idx - 1, 0, len(self.time) - 1)
327
+ idx_right = np.clip(idx, 0, len(self.time) - 1)
328
+ diff_left = np.abs(time_new - self.time[idx_left])
329
+ diff_right = np.abs(time_new - self.time[idx_right])
330
+ idx_closest = np.where(diff_left < diff_right, idx_left, idx_right)
331
+ for key in ("latitude", "longitude", "altitude"):
332
+ data = self.getvar(key)
333
+ if not utils.isscalar(data):
334
+ data = data[idx_closest]
335
+ if not np.any(ma.getmaskarray(data)):
336
+ data = np.array(data)
337
+ self.append_data(data, key)
338
+
339
+ def _init_data(self) -> None:
275
340
  self.append_data(self.getvar("Zh"), "Z", units="dBZ")
276
- for key in ("v", "ldr", "width", "sldr", "rainfall_rate"):
277
- try:
278
- self._variables_to_cloudnet_arrays((key,))
279
- except KeyError:
280
- continue
281
-
282
- def _init_sigma_v(self) -> None:
283
- """Initializes std of the velocity field. The std will be calculated
284
- later when re-binning the data."""
285
341
  self.append_data(self.getvar("v"), "v_sigma")
342
+ for key in ("v", "ldr", "width", "sldr", "rainfall_rate", "nyquist_velocity"):
343
+ if key in self.dataset.variables:
344
+ data = self.dataset.variables[key]
345
+ self.append_data(data, key)
286
346
 
287
- def _get_sequence_indices(self) -> list:
288
- """Mira has only one sequence and one folding velocity. RPG has
289
- several sequences with different folding velocities."""
290
- assert self.height is not None
291
- all_indices = np.arange(len(self.height))
292
- if not utils.isscalar(self.folding_velocity):
293
- starting_indices = self.getvar("chirp_start_indices")
294
- return np.split(all_indices, starting_indices[1:])
295
- return [all_indices]
296
-
297
- def _get_folding_velocity(self) -> np.ndarray | float:
347
+ def _rebin_velocity(
348
+ self,
349
+ data: npt.NDArray,
350
+ time_new: npt.NDArray,
351
+ ) -> npt.NDArray:
352
+ """Rebins Doppler velocity in polar coordinates."""
353
+ folding_velocity = self._get_expanded_folding_velocity()
354
+ # with the new shape (maximum value in every bin)
355
+ max_folding_binned, _ = utils.rebin_2d(
356
+ self.time,
357
+ folding_velocity,
358
+ time_new,
359
+ "max",
360
+ )
361
+ # store this in the file
362
+ self.append_data(max_folding_binned, "nyquist_velocity")
363
+
364
+ if "correction_bits" in self.dataset.variables:
365
+ bits = self.dataset.variables["correction_bits"][:]
366
+ dealiasing_bit = utils.isbit(bits, 0)
367
+ if ma.all(dealiasing_bit):
368
+ return utils.rebin_2d(self.time, data, time_new, "mean")[0]
369
+ if ma.any(dealiasing_bit):
370
+ msg = "Data are only party dealiased. Deal with this later."
371
+ raise NotImplementedError(msg)
372
+
373
+ # with original shape (repeat maximum value for each point in every bin)
374
+ max_folding_full, _ = utils.rebin_2d(
375
+ self.time,
376
+ folding_velocity,
377
+ time_new,
378
+ "max",
379
+ keepdim=True,
380
+ )
381
+
382
+ data_scaled = data * (np.pi / max_folding_full)
383
+ vel_x = ma.cos(data_scaled)
384
+ vel_y = ma.sin(data_scaled)
385
+ vel_x_mean, _ = utils.rebin_2d(self.time, vel_x, time_new)
386
+ vel_y_mean, _ = utils.rebin_2d(self.time, vel_y, time_new)
387
+ vel_scaled = ma.arctan2(vel_y_mean, vel_x_mean)
388
+ return vel_scaled / (np.pi / max_folding_binned)
389
+
390
+ def _get_expanded_folding_velocity(self) -> npt.NDArray:
298
391
  if "nyquist_velocity" in self.dataset.variables:
299
- return self.getvar("nyquist_velocity")
300
- if "prf" in self.dataset.variables:
392
+ fvel = self.getvar("nyquist_velocity")
393
+ elif "prf" in self.dataset.variables:
301
394
  prf = self.getvar("prf")
302
- return _prf_to_folding_velocity(prf, self.radar_frequency)
303
- raise RuntimeError("Unable to determine folding velocity")
304
-
305
- def _get_folding_velocity_full(self):
306
- folding_velocity: list | np.ndarray = []
307
- if utils.isscalar(self.folding_velocity):
308
- folding_velocity = np.repeat(
309
- self.folding_velocity, len(self.sequence_indices[0])
310
- )
395
+ fvel = _prf_to_folding_velocity(prf, self.radar_frequency)
311
396
  else:
312
- assert isinstance(folding_velocity, list)
313
- assert isinstance(self.folding_velocity, np.ndarray)
314
- for indices, velocity in zip(self.sequence_indices, self.folding_velocity):
315
- folding_velocity.append(np.repeat(velocity, len(indices)))
316
- folding_velocity = np.hstack(folding_velocity)
317
- self.append_data(folding_velocity, "nyquist_velocity")
397
+ msg = "Unable to determine folding velocity"
398
+ raise RuntimeError(msg)
399
+
400
+ n_time = self.getvar("time").size
401
+ n_height = self.height.size
402
+
403
+ if fvel.shape == (n_time, n_height):
404
+ # Folding velocity is already expanded in radar file
405
+ return fvel
406
+ if utils.isscalar(fvel):
407
+ # e.g. MIRA
408
+ return np.broadcast_to(fvel, (n_time, n_height))
409
+
410
+ # RPG radars have chirp segments
411
+ starts = self.getvar("chirp_start_indices")
412
+ n_seg = starts.size if starts.ndim == 1 else starts.shape[1]
413
+
414
+ starts = np.broadcast_to(starts, (n_time, n_seg))
415
+ fvel = np.broadcast_to(fvel, (n_time, n_seg))
416
+
417
+ # Indices should start from zero (first range gate)
418
+ # In pre-processed RV Meteor files the first index is 1, so normalize:
419
+ # Normalize starts so indices begin from zero
420
+ first_values = starts[:, [0]]
421
+ if not np.all(np.isin(first_values, [0, 1])):
422
+ msg = "First value of chirp_start_indices must be 0 or 1"
423
+ raise ValueError(msg)
424
+ starts = starts - first_values
425
+
426
+ chirp_size = np.diff(starts, append=n_height)
427
+ return np.repeat(fvel.ravel(), chirp_size.ravel()).reshape((n_time, n_height))
318
428
 
319
429
 
320
- def _prf_to_folding_velocity(prf: np.ndarray, radar_frequency: float) -> float:
430
+ def _prf_to_folding_velocity(prf: npt.NDArray, radar_frequency: float) -> npt.NDArray:
321
431
  ghz_to_hz = 1e9
322
- return float(prf * constants.c / (4 * radar_frequency * ghz_to_hz))
432
+ if len(prf) != 1:
433
+ msg = "Unable to determine folding velocity"
434
+ raise RuntimeError(msg)
435
+ return prf[0] * constants.c / (4 * radar_frequency * ghz_to_hz)