doppy 0.3.6__cp310-abi3-macosx_11_0_arm64.whl → 0.4.0__cp310-abi3-macosx_11_0_arm64.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.

Potentially problematic release.


This version of doppy might be problematic. Click here for more details.

doppy/defaults.py CHANGED
@@ -1,2 +1,18 @@
1
+ DEFAULT_BEAM_ENERGY = 1e-5
2
+ DEFAULT_EFFECTIVE_DIAMETER = 25e-3
3
+
4
+
1
5
  class Halo:
2
- wavelength = 1.565e-6
6
+ wavelength = 1.565e-6 # [m]
7
+ receiver_bandwidth = 50e6 # [Hz]
8
+ beam_energy = DEFAULT_BEAM_ENERGY
9
+ effective_diameter = DEFAULT_EFFECTIVE_DIAMETER
10
+
11
+
12
+ class WindCube:
13
+ # https://doi.org/10.5194/essd-13-3539-2021
14
+ wavelength = 1.54e-6 # [m]
15
+ receiver_bandwidth = 55e6 # [Hz]
16
+ beam_energy = DEFAULT_BEAM_ENERGY
17
+ effective_diameter = 50e-3 # [m]
18
+ focus = 1e3 # [m]
doppy/product/stare.py CHANGED
@@ -9,7 +9,7 @@ from typing import Sequence, Tuple, TypeAlias
9
9
  import numpy as np
10
10
  import numpy.typing as npt
11
11
  import scipy
12
- from scipy.ndimage import uniform_filter
12
+ from scipy.ndimage import median_filter, uniform_filter
13
13
  from sklearn.cluster import KMeans
14
14
 
15
15
  import doppy
@@ -51,6 +51,46 @@ class Stare:
51
51
  )
52
52
  raise TypeError
53
53
 
54
+ @classmethod
55
+ def mask_nan(cls, x: npt.NDArray[np.float64]) -> npt.NDArray[np.bool_]:
56
+ return np.isnan(x)
57
+
58
+ @classmethod
59
+ def from_windcube_data(
60
+ cls,
61
+ data: Sequence[str]
62
+ | Sequence[Path]
63
+ | Sequence[bytes]
64
+ | Sequence[BufferedIOBase],
65
+ ) -> Stare:
66
+ raws = doppy.raw.WindCubeFixed.from_srcs(data)
67
+ raw = (
68
+ doppy.raw.WindCubeFixed.merge(raws).sorted_by_time().nan_profiles_removed()
69
+ )
70
+
71
+ wavelength = defaults.WindCube.wavelength
72
+ beta = _compute_beta(
73
+ snr=raw.cnr,
74
+ radial_distance=raw.radial_distance,
75
+ wavelength=wavelength,
76
+ beam_energy=defaults.WindCube.beam_energy,
77
+ receiver_bandwidth=defaults.WindCube.receiver_bandwidth,
78
+ focus=defaults.WindCube.focus,
79
+ effective_diameter=defaults.WindCube.effective_diameter,
80
+ )
81
+
82
+ mask = _compute_noise_mask_for_windcube(raw)
83
+ return cls(
84
+ time=raw.time,
85
+ radial_distance=raw.radial_distance,
86
+ elevation=raw.elevation,
87
+ beta=beta,
88
+ radial_velocity=raw.radial_velocity,
89
+ mask=mask,
90
+ wavelength=wavelength,
91
+ system_id=raw.system_id,
92
+ )
93
+
54
94
  @classmethod
55
95
  def from_halo_data(
56
96
  cls,
@@ -95,16 +135,21 @@ class Stare:
95
135
  raw, intensity_bg_corrected
96
136
  )
97
137
  wavelength = defaults.Halo.wavelength
138
+
98
139
  beta = _compute_beta(
99
- intensity_noise_bias_corrected,
100
- raw.radial_distance,
101
- raw.header.focus_range,
102
- wavelength,
140
+ snr=intensity_noise_bias_corrected - 1,
141
+ radial_distance=raw.radial_distance,
142
+ wavelength=wavelength,
143
+ beam_energy=defaults.Halo.beam_energy,
144
+ receiver_bandwidth=defaults.Halo.receiver_bandwidth,
145
+ focus=raw.header.focus_range,
146
+ effective_diameter=defaults.Halo.effective_diameter,
103
147
  )
148
+
104
149
  mask = _compute_noise_mask(
105
150
  intensity_noise_bias_corrected, raw.radial_velocity, raw.radial_distance
106
151
  )
107
- return Stare(
152
+ return cls(
108
153
  time=raw.time,
109
154
  radial_distance=raw.radial_distance,
110
155
  elevation=raw.elevation,
@@ -177,6 +222,36 @@ class Stare:
177
222
  nc.add_attribute("doppy_version", doppy.__version__)
178
223
 
179
224
 
225
+ def _compute_noise_mask_for_windcube(
226
+ raw: doppy.raw.WindCubeFixed,
227
+ ) -> npt.NDArray[np.bool_]:
228
+ if np.any(np.isnan(raw.cnr)) or np.any(np.isnan(raw.radial_velocity)):
229
+ raise ValueError("Unexpected nans in crn or radial_velocity")
230
+
231
+ mask = _mask_with_cnr_norm_dist(raw.cnr) | (np.abs(raw.radial_velocity) > 30)
232
+
233
+ cnr = raw.cnr.copy()
234
+ cnr[mask] = np.finfo(float).eps
235
+ cnr_filt = median_filter(cnr, size=(3, 3))
236
+ rel_diff = np.abs(cnr - cnr_filt) / np.abs(cnr)
237
+ diff_mask = rel_diff > 0.25
238
+
239
+ mask = mask | diff_mask
240
+
241
+ return np.array(mask, dtype=np.bool_)
242
+
243
+
244
+ def _mask_with_cnr_norm_dist(cnr: npt.NDArray[np.float64]) -> npt.NDArray[np.bool_]:
245
+ th_trunc = -5.5
246
+ std_factor = 2
247
+ log_cnr = np.log(cnr)
248
+ log_cnr_trunc = log_cnr[log_cnr < th_trunc]
249
+ th_trunc_fit = np.percentile(log_cnr_trunc, 90)
250
+ log_cnr_for_fit = log_cnr_trunc[log_cnr_trunc < th_trunc_fit]
251
+ mean, std = scipy.stats.norm.fit(log_cnr_for_fit)
252
+ return np.array(np.log(cnr) < (mean + std_factor * std), dtype=np.bool_)
253
+
254
+
180
255
  def _compute_noise_mask(
181
256
  intensity: npt.NDArray[np.float64],
182
257
  radial_velocity: npt.NDArray[np.float64],
@@ -197,14 +272,19 @@ def _compute_noise_mask(
197
272
 
198
273
 
199
274
  def _compute_beta(
200
- intensity: npt.NDArray[np.float64],
275
+ snr: npt.NDArray[np.float64],
201
276
  radial_distance: npt.NDArray[np.float64],
202
- focus: float,
203
277
  wavelength: float,
278
+ beam_energy: float,
279
+ receiver_bandwidth: float,
280
+ focus: float,
281
+ effective_diameter: float,
204
282
  ) -> npt.NDArray[np.float64]:
205
283
  """
206
284
  Parameters
207
285
  ----------
286
+ snr
287
+ for halo: intensity - 1
208
288
  radial_distance
209
289
  distance from the instrument
210
290
  focus
@@ -236,22 +316,24 @@ def _compute_beta(
236
316
  doi: https://doi.org/10.5194/amt-13-2849-2020
237
317
  """
238
318
 
239
- snr = intensity - 1
240
319
  h = scipy.constants.Planck
241
320
  c = scipy.constants.speed_of_light
242
321
  eta = 1
243
- E = 1e-5
244
- B = 5e7
322
+ E = beam_energy
323
+ B = receiver_bandwidth
245
324
  nu = c / wavelength
246
- A_e = _compute_effective_receiver_energy(radial_distance, focus, wavelength)
325
+ A_e = _compute_effective_receiver_energy(
326
+ radial_distance, wavelength, focus, effective_diameter
327
+ )
247
328
  beta = 2 * h * nu * B * radial_distance**2 * snr / (eta * c * E * A_e)
248
329
  return np.array(beta, dtype=np.float64)
249
330
 
250
331
 
251
332
  def _compute_effective_receiver_energy(
252
333
  radial_distance: npt.NDArray[np.float64],
253
- focus: float,
254
334
  wavelength: float,
335
+ focus: float,
336
+ effective_diameter: float,
255
337
  ) -> npt.NDArray[np.float64]:
256
338
  """
257
339
  NOTE
@@ -268,7 +350,7 @@ def _compute_effective_receiver_energy(
268
350
  wavelength
269
351
  laser wavelength
270
352
  """
271
- D = 25e-3 # effective_diameter_of_gaussian_beam
353
+ D = effective_diameter
272
354
  return np.array(
273
355
  np.pi
274
356
  * D**2
doppy/raw/__init__.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from .halo_bg import HaloBg
2
2
  from .halo_hpl import HaloHpl
3
3
  from .halo_sys_params import HaloSysParams
4
- from .windcube import WindCube
4
+ from .windcube import WindCube, WindCubeFixed
5
5
  from .wls70 import Wls70
6
6
 
7
- __all__ = ["HaloHpl", "HaloBg", "HaloSysParams", "WindCube", "Wls70"]
7
+ __all__ = ["HaloHpl", "HaloBg", "HaloSysParams", "WindCube", "WindCubeFixed", "Wls70"]
doppy/raw/halo_hpl.py CHANGED
@@ -4,7 +4,7 @@ import functools
4
4
  import io
5
5
  import re
6
6
  from dataclasses import dataclass
7
- from datetime import datetime, timedelta
7
+ from datetime import datetime, timedelta, timezone
8
8
  from io import BufferedIOBase
9
9
  from os.path import commonprefix
10
10
  from pathlib import Path
@@ -309,7 +309,11 @@ def _raw_tuple2halo_hpl(
309
309
  resolution=float(header_dict["resolution"]),
310
310
  scan_type=str(header_dict["scan_type"]),
311
311
  focus_range=int(header_dict["focus_range"]),
312
- start_time=datetime64(datetime.utcfromtimestamp(header_dict["start_time"])),
312
+ start_time=datetime64(
313
+ datetime.fromtimestamp(header_dict["start_time"], timezone.utc).replace(
314
+ tzinfo=None
315
+ )
316
+ ),
313
317
  system_id=str(header_dict["system_id"]),
314
318
  instrument_spectral_width=float(header_dict["instrument_spectral_width"])
315
319
  if header_dict["instrument_spectral_width"] is not None
doppy/raw/windcube.py CHANGED
@@ -7,17 +7,104 @@ from typing import Sequence
7
7
 
8
8
  import numpy as np
9
9
  import numpy.typing as npt
10
- from netCDF4 import Dataset, num2date
10
+ from netCDF4 import Dataset, Variable, num2date
11
11
  from numpy import datetime64
12
12
 
13
13
  from doppy.utils import merge_all_equal
14
14
 
15
15
 
16
+ @dataclass
17
+ class WindCubeFixed:
18
+ time: npt.NDArray[datetime64] # dim: (time, )
19
+ radial_distance: npt.NDArray[np.float64] # dim: (radial_distance,)
20
+ azimuth: npt.NDArray[np.float64] # dim: (time, )
21
+ elevation: npt.NDArray[np.float64] # dim: (time, )
22
+ cnr: npt.NDArray[np.float64] # dim: (time, radial_distance)
23
+ relative_beta: npt.NDArray[np.float64] # dim: (time, radial_distance)
24
+ radial_velocity: npt.NDArray[np.float64] # dim: (time, radial_distance)
25
+ doppler_spectrum_width: npt.NDArray[np.float64] # dim: (time, radial_distance)
26
+ radial_velocity_confidence: npt.NDArray[np.float64] # dim: (time, radial_distance)
27
+ ray_accumulation_time: np.float64 # dim: ()
28
+ system_id: str
29
+
30
+ @classmethod
31
+ def from_srcs(
32
+ cls,
33
+ data: Sequence[str]
34
+ | Sequence[Path]
35
+ | Sequence[bytes]
36
+ | Sequence[BufferedIOBase],
37
+ ) -> list[WindCubeFixed]:
38
+ return [WindCubeFixed.from_fixed_src(src) for src in data]
39
+
40
+ @classmethod
41
+ def from_fixed_src(cls, data: str | Path | bytes | BufferedIOBase) -> WindCubeFixed:
42
+ data_bytes = _src_to_bytes(data)
43
+ nc = Dataset("inmemory.nc", "r", memory=data_bytes)
44
+ return _from_fixed_src(nc)
45
+
46
+ @classmethod
47
+ def merge(cls, raws: list[WindCubeFixed]) -> WindCubeFixed:
48
+ return WindCubeFixed(
49
+ time=np.concatenate([r.time for r in raws]),
50
+ radial_distance=_merge_radial_distance_for_fixed(
51
+ [r.radial_distance for r in raws]
52
+ ),
53
+ azimuth=np.concatenate([r.azimuth for r in raws]),
54
+ elevation=np.concatenate([r.elevation for r in raws]),
55
+ radial_velocity=np.concatenate([r.radial_velocity for r in raws]),
56
+ radial_velocity_confidence=np.concatenate(
57
+ [r.radial_velocity_confidence for r in raws]
58
+ ),
59
+ cnr=np.concatenate([r.cnr for r in raws]),
60
+ relative_beta=np.concatenate([r.relative_beta for r in raws]),
61
+ doppler_spectrum_width=np.concatenate(
62
+ [r.doppler_spectrum_width for r in raws]
63
+ ),
64
+ ray_accumulation_time=merge_all_equal(
65
+ "ray_accumulation_time", [r.ray_accumulation_time for r in raws]
66
+ ),
67
+ system_id=merge_all_equal("system_id", [r.system_id for r in raws]),
68
+ )
69
+
70
+ def __getitem__(
71
+ self,
72
+ index: int
73
+ | slice
74
+ | list[int]
75
+ | npt.NDArray[np.int64]
76
+ | npt.NDArray[np.bool_]
77
+ | tuple[slice, slice],
78
+ ) -> WindCubeFixed:
79
+ if isinstance(index, (int, slice, list, np.ndarray)):
80
+ return WindCubeFixed(
81
+ time=self.time[index],
82
+ radial_distance=self.radial_distance,
83
+ azimuth=self.azimuth[index],
84
+ elevation=self.elevation[index],
85
+ radial_velocity=self.radial_velocity[index],
86
+ radial_velocity_confidence=self.radial_velocity_confidence[index],
87
+ cnr=self.cnr[index],
88
+ relative_beta=self.relative_beta[index],
89
+ doppler_spectrum_width=self.doppler_spectrum_width[index],
90
+ ray_accumulation_time=self.ray_accumulation_time,
91
+ system_id=self.system_id,
92
+ )
93
+ raise TypeError
94
+
95
+ def sorted_by_time(self) -> WindCubeFixed:
96
+ sort_indices = np.argsort(self.time)
97
+ return self[sort_indices]
98
+
99
+ def nan_profiles_removed(self) -> WindCubeFixed:
100
+ return self[~np.all(np.isnan(self.cnr), axis=1)]
101
+
102
+
16
103
  @dataclass
17
104
  class WindCube:
18
105
  time: npt.NDArray[datetime64] # dim: (time, )
19
- radial_distance: npt.NDArray[np.int64] # dim: (time, radial_distance)
20
- height: npt.NDArray[np.int64] # dim: (time,radial_distance)
106
+ radial_distance: npt.NDArray[np.float64] # dim: (time, radial_distance)
107
+ height: npt.NDArray[np.float64] # dim: (time,radial_distance)
21
108
  azimuth: npt.NDArray[np.float64] # dim: (time, )
22
109
  elevation: npt.NDArray[np.float64] # dim: (time, )
23
110
  cnr: npt.NDArray[np.float64] # dim: (time, radial_distance)
@@ -128,6 +215,23 @@ def _merge_scan_index(index_list: list[npt.NDArray[np.int64]]) -> npt.NDArray[np
128
215
  return np.concatenate(new_index_list)
129
216
 
130
217
 
218
+ def _merge_radial_distance_for_fixed(
219
+ radial_distance_list: list[npt.NDArray[np.float64]],
220
+ ) -> npt.NDArray[np.float64]:
221
+ if len(radial_distance_list) == 0:
222
+ raise ValueError("cannot merge empty list")
223
+ if not all(
224
+ np.allclose(arr.shape, radial_distance_list[0].shape)
225
+ for arr in radial_distance_list
226
+ ):
227
+ raise ValueError("Cannot merge radial distances with different shapes")
228
+ if not all(
229
+ np.allclose(arr, radial_distance_list[0]) for arr in radial_distance_list
230
+ ):
231
+ raise ValueError("Cannot merge radial distances")
232
+ return radial_distance_list[0]
233
+
234
+
131
235
  def _src_to_bytes(data: str | Path | bytes | BufferedIOBase) -> bytes:
132
236
  if isinstance(data, str):
133
237
  path = Path(data)
@@ -140,38 +244,127 @@ def _src_to_bytes(data: str | Path | bytes | BufferedIOBase) -> bytes:
140
244
  return data
141
245
  elif isinstance(data, BufferedIOBase):
142
246
  return data.read()
143
- raise TypeError("Unsupported data type")
144
247
 
145
248
 
146
- def _from_vad_or_dbs_src(nc: Dataset) -> WindCube:
147
- scan_index_list = []
249
+ def _from_fixed_src(nc: Dataset) -> WindCubeFixed:
148
250
  time_list = []
149
251
  cnr_list = []
252
+ relative_beta_list = []
150
253
  radial_wind_speed_list = []
151
254
  radial_wind_speed_confidence_list = []
152
255
  azimuth_list = []
153
256
  elevation_list = []
154
257
  range_list = []
155
- height_list = []
258
+ doppler_spectrum_width_list = []
259
+ ray_accumulation_time_list = []
260
+ time_reference = (
261
+ nc["time_reference"][:] if "time_reference" in nc.variables else None
262
+ )
263
+
264
+ expected_dimensions = ("time", "range")
265
+ for _, group in enumerate(
266
+ nc[group] for group in (nc.variables["sweep_group_name"][:])
267
+ ):
268
+ time_reference_ = time_reference
269
+ if time_reference is None and "time_reference" in group.variables:
270
+ time_reference_ = group["time_reference"][:]
271
+
272
+ time_list.append(_extract_datetime64_or_raise(group["time"], time_reference_))
273
+ radial_wind_speed_list.append(
274
+ _extract_float64_or_raise(group["radial_wind_speed"], expected_dimensions)
275
+ )
276
+ cnr_list.append(_extract_float64_or_raise(group["cnr"], expected_dimensions))
277
+ relative_beta_list.append(
278
+ _extract_float64_or_raise(group["relative_beta"], expected_dimensions)
279
+ )
280
+ radial_wind_speed_confidence_list.append(
281
+ _extract_float64_or_raise(
282
+ group["radial_wind_speed_ci"], expected_dimensions
283
+ )
284
+ )
285
+ azimuth_list.append(
286
+ _extract_float64_or_raise(group["azimuth"], expected_dimensions)
287
+ )
288
+ elevation_list.append(
289
+ _extract_float64_or_raise(group["elevation"], expected_dimensions)
290
+ )
291
+ range_list.append(
292
+ _extract_float64_or_raise(group["range"], (expected_dimensions[1],))
293
+ )
294
+ doppler_spectrum_width_list.append(
295
+ _extract_float64_or_raise(
296
+ group["doppler_spectrum_width"], expected_dimensions
297
+ )
298
+ )
299
+ ray_accumulation_time_list.append(
300
+ _extract_float64_or_raise(
301
+ group["ray_accumulation_time"], expected_dimensions
302
+ )
303
+ )
304
+
305
+ return WindCubeFixed(
306
+ time=np.concatenate(time_list),
307
+ radial_distance=np.concatenate(range_list),
308
+ azimuth=np.concatenate(azimuth_list),
309
+ elevation=np.concatenate(elevation_list),
310
+ radial_velocity=np.concatenate(radial_wind_speed_list),
311
+ radial_velocity_confidence=np.concatenate(radial_wind_speed_confidence_list),
312
+ cnr=np.concatenate(cnr_list),
313
+ relative_beta=np.concatenate(relative_beta_list),
314
+ doppler_spectrum_width=np.concatenate(doppler_spectrum_width_list),
315
+ ray_accumulation_time=merge_all_equal(
316
+ "ray_accumulation_time",
317
+ np.array(ray_accumulation_time_list, dtype=np.float64).tolist(),
318
+ ),
319
+ system_id=nc.instrument_name,
320
+ )
321
+
322
+
323
+ def _from_vad_or_dbs_src(nc: Dataset) -> WindCube:
324
+ scan_index_list: list[npt.NDArray[np.int64]] = []
325
+ time_list: list[npt.NDArray[np.datetime64]] = []
326
+ cnr_list: list[npt.NDArray[np.float64]] = []
327
+ radial_wind_speed_list: list[npt.NDArray[np.float64]] = []
328
+ radial_wind_speed_confidence_list: list[npt.NDArray[np.float64]] = []
329
+ azimuth_list: list[npt.NDArray[np.float64]] = []
330
+ elevation_list: list[npt.NDArray[np.float64]] = []
331
+ range_list: list[npt.NDArray[np.float64]] = []
332
+ height_list: list[npt.NDArray[np.float64]] = []
333
+
156
334
  time_reference = (
157
335
  nc["time_reference"][:] if "time_reference" in nc.variables else None
158
336
  )
159
337
 
338
+ expected_dimensions = ("time", "gate_index")
160
339
  for i, group in enumerate(
161
340
  nc[group] for group in (nc.variables["sweep_group_name"][:])
162
341
  ):
163
- time_list.append(_extract_datetime64_or_raise(group["time"], time_reference))
342
+ time_reference_ = time_reference
343
+ if time_reference is None and "time_reference" in group.variables:
344
+ time_reference_ = group["time_reference"][:]
345
+
346
+ time_list.append(_extract_datetime64_or_raise(group["time"], time_reference_))
164
347
  radial_wind_speed_list.append(
165
- _extract_float64_or_raise(group["radial_wind_speed"])
348
+ _extract_float64_or_raise(group["radial_wind_speed"], expected_dimensions)
166
349
  )
167
- cnr_list.append(_extract_float64_or_raise(group["cnr"]))
350
+ cnr_list.append(_extract_float64_or_raise(group["cnr"], expected_dimensions))
168
351
  radial_wind_speed_confidence_list.append(
169
- _extract_float64_or_raise(group["radial_wind_speed_ci"])
352
+ _extract_float64_or_raise(
353
+ group["radial_wind_speed_ci"], expected_dimensions
354
+ )
355
+ )
356
+ azimuth_list.append(
357
+ _extract_float64_or_raise(group["azimuth"], expected_dimensions)
358
+ )
359
+ elevation_list.append(
360
+ _extract_float64_or_raise(group["elevation"], expected_dimensions)
361
+ )
362
+ range_list.append(
363
+ _extract_float64_or_raise(group["range"], expected_dimensions)
364
+ )
365
+ height_list.append(
366
+ _extract_float64_or_raise(group["measurement_height"], expected_dimensions)
170
367
  )
171
- azimuth_list.append(_extract_float64_or_raise(group["azimuth"]))
172
- elevation_list.append(_extract_float64_or_raise(group["elevation"]))
173
- range_list.append(_extract_int64_or_raise(group["range"]))
174
- height_list.append(_extract_int64_or_raise(group["measurement_height"]))
175
368
  scan_index_list.append(np.full(group["time"][:].shape, i, dtype=np.int64))
176
369
 
177
370
  return WindCube(
@@ -189,7 +382,7 @@ def _from_vad_or_dbs_src(nc: Dataset) -> WindCube:
189
382
 
190
383
 
191
384
  def _extract_datetime64_or_raise(
192
- nc: Dataset, time_reference: str | None
385
+ nc: Variable[npt.NDArray[np.float64]], time_reference: str | None
193
386
  ) -> npt.NDArray[np.datetime64]:
194
387
  match nc.name:
195
388
  case "time":
@@ -207,18 +400,40 @@ def _extract_datetime64_or_raise(
207
400
  raise ValueError(f"Unexpected variable name {nc.name}")
208
401
 
209
402
 
210
- def _extract_float64_or_raise(nc: Dataset) -> npt.NDArray[np.float64]:
403
+ def _dB_to_ratio(decibels: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
404
+ return 10 ** (0.1 * decibels)
405
+
406
+
407
+ def _extract_float64_or_raise(
408
+ nc: Variable[npt.NDArray[np.float64]], expected_dimensions: tuple[str, ...]
409
+ ) -> npt.NDArray[np.float64]:
211
410
  match nc.name:
411
+ case "range" | "measurement_height":
412
+ if nc.dimensions != expected_dimensions:
413
+ raise ValueError(f"Unexpected dimensions for {nc.name}")
414
+ if nc.units != "m":
415
+ raise ValueError(f"Unexpected units for {nc.name}")
416
+ if nc[:].mask is not np.bool_(False):
417
+ raise ValueError
418
+ return np.array(nc[:].data, dtype=np.float64)
212
419
  case "cnr":
213
- if nc.dimensions != ("time", "gate_index"):
420
+ if nc.dimensions != expected_dimensions:
214
421
  raise ValueError(f"Unexpected dimensions for {nc.name}")
215
422
  if nc.units != "dB":
216
423
  raise ValueError(f"Unexpected units for {nc.name}")
217
424
  if nc[:].mask is not np.bool_(False):
218
425
  pass # ignore that array contains masked values
426
+ return _dB_to_ratio(np.array(nc[:].data, dtype=np.float64))
427
+ case "relative_beta":
428
+ if nc.dimensions != expected_dimensions:
429
+ raise ValueError(f"Unexpected dimensions for {nc.name}")
430
+ if nc.units != "m-1 sr-1":
431
+ raise ValueError(f"Unexpected units for {nc.name}")
432
+ if nc[:].mask is not np.bool_(False):
433
+ pass # ignore that array contains masked values
219
434
  return np.array(nc[:].data, dtype=np.float64)
220
435
  case "radial_wind_speed":
221
- if nc.dimensions != ("time", "gate_index"):
436
+ if nc.dimensions != expected_dimensions:
222
437
  raise ValueError(f"Unexpected dimensions for {nc.name}")
223
438
  if nc.units != "m s-1":
224
439
  raise ValueError(f"Unexpected units for {nc.name}")
@@ -226,7 +441,7 @@ def _extract_float64_or_raise(nc: Dataset) -> npt.NDArray[np.float64]:
226
441
  pass # ignore that array contains masked values
227
442
  return np.array(nc[:].data, dtype=np.float64)
228
443
  case "radial_wind_speed_ci":
229
- if nc.dimensions != ("time", "gate_index"):
444
+ if nc.dimensions != expected_dimensions:
230
445
  raise ValueError(f"Unexpected dimensions for {nc.name}")
231
446
  if nc.units != "percent":
232
447
  raise ValueError(f"Unexpected units for {nc.name}")
@@ -234,26 +449,28 @@ def _extract_float64_or_raise(nc: Dataset) -> npt.NDArray[np.float64]:
234
449
  pass # ignore that array contains masked values
235
450
  return np.array(nc[:].data, dtype=np.float64)
236
451
  case "azimuth" | "elevation":
237
- if nc.dimensions != ("time",):
452
+ if nc.dimensions != (expected_dimensions[0],):
238
453
  raise ValueError(f"Unexpected dimensions for {nc.name}")
239
454
  if nc.units != "degrees":
240
455
  raise ValueError(f"Unexpected units for {nc.name}")
241
456
  if nc[:].mask is not np.bool_(False):
242
457
  raise ValueError
243
458
  return np.array(nc[:].data, dtype=np.float64)
244
- case _:
245
- raise ValueError(f"Unexpected variable name {nc.name}")
246
-
247
-
248
- def _extract_int64_or_raise(nc: Dataset) -> npt.NDArray[np.int64]:
249
- match nc.name:
250
- case "range" | "measurement_height":
251
- if nc.dimensions != ("time", "gate_index"):
459
+ case "doppler_spectrum_width":
460
+ if nc.dimensions != expected_dimensions:
252
461
  raise ValueError(f"Unexpected dimensions for {nc.name}")
253
- if nc.units != "m":
462
+ if nc.units != "m s-1":
254
463
  raise ValueError(f"Unexpected units for {nc.name}")
255
464
  if nc[:].mask is not np.bool_(False):
256
- raise ValueError
257
- return np.array(nc[:].data, dtype=np.int64)
465
+ pass # ignore that array contains masked values
466
+ return np.array(nc[:].data, dtype=np.float64)
467
+ case "ray_accumulation_time":
468
+ if nc.dimensions != ():
469
+ raise ValueError(f"Unexpected dimensions for {nc.name}")
470
+ if nc.units != "ms":
471
+ raise ValueError(f"Unexpected units for {nc.name}")
472
+ if nc[:].mask is not np.bool_(False):
473
+ raise ValueError(f"Variable {nc.name} contains masked values")
474
+ return np.array(nc[:].data, dtype=np.float64)
258
475
  case _:
259
476
  raise ValueError(f"Unexpected variable name {nc.name}")
doppy/raw/wls70.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
- from datetime import datetime
4
+ from datetime import datetime, timezone
5
5
  from io import BufferedIOBase
6
6
  from pathlib import Path
7
7
  from typing import Any, Sequence
@@ -166,7 +166,12 @@ def _raw_rs_to_wls70(
166
166
  cnr_threshold = float(info["cnr_threshold"])
167
167
  data = data.reshape(-1, len(cols))
168
168
  time_ts = data[:, 0]
169
- time = np.array([datetime64(datetime.utcfromtimestamp(ts)) for ts in time_ts])
169
+ time = np.array(
170
+ [
171
+ datetime64(datetime.fromtimestamp(ts, timezone.utc).replace(tzinfo=None))
172
+ for ts in time_ts
173
+ ]
174
+ )
170
175
 
171
176
  position = data[:, 1]
172
177
  temperature = data[:, 2]
doppy/rs.abi3.so CHANGED
Binary file
doppy/utils.py CHANGED
@@ -1,9 +1,24 @@
1
- from typing import TypeVar
1
+ from typing import TypeVar, cast
2
+
3
+ import numpy as np
4
+ from numpy.typing import NDArray
2
5
 
3
6
  T = TypeVar("T")
7
+ NT = TypeVar("NT", bound=np.generic)
4
8
 
5
9
 
6
10
  def merge_all_equal(key: str, lst: list[T]) -> T:
7
11
  if len(set(lst)) != 1:
8
12
  raise ValueError(f"Cannot merge header key {key} values {lst}")
9
13
  return lst[0]
14
+
15
+
16
+ def merge_all_close(key: str, lst: list[NDArray[NT]]) -> NT:
17
+ if len(lst) == 0:
18
+ raise ValueError(f"Cannot merge empty list for key {key}")
19
+ if any(arr.size == 0 for arr in lst):
20
+ raise ValueError(f"Cannot merge key {key}, one or more arrays are empty.")
21
+ arr = np.concatenate([arr.flatten() for arr in lst])
22
+ if not np.allclose(arr, arr[0]):
23
+ raise ValueError(f"Cannot merge key {key}, values are not close enough")
24
+ return cast(NT, arr[0])
@@ -1,11 +1,12 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: doppy
3
- Version: 0.3.6
3
+ Version: 0.4.0
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3.10
7
7
  Classifier: Programming Language :: Python :: 3.11
8
8
  Classifier: Programming Language :: Python :: 3.12
9
+ Classifier: Programming Language :: Python :: 3.13
9
10
  Classifier: License :: OSI Approved :: MIT License
10
11
  Classifier: Intended Audience :: Science/Research
11
12
  Classifier: Operating System :: OS Independent
@@ -25,6 +26,7 @@ Requires-Dist: py-spy ; extra == 'dev'
25
26
  Requires-Dist: maturin ==1.4 ; extra == 'dev'
26
27
  Requires-Dist: release-version ; extra == 'dev'
27
28
  Requires-Dist: pre-commit ; extra == 'dev'
29
+ Requires-Dist: xarray[io] ; extra == 'dev'
28
30
  Provides-Extra: dev
29
31
  License-File: LICENSE
30
32
  License-File: LICENSE
@@ -1,29 +1,29 @@
1
- doppy-0.3.6.dist-info/METADATA,sha256=rIdxynFJ0EY_sbL0ATtBX4OTvQfS6zcsPeqTI6FG8eU,4182
2
- doppy-0.3.6.dist-info/WHEEL,sha256=i-5e1nGSEdQbR_kAzcSB5cV7omUpJT_GdwQP7UpE6S0,103
3
- doppy-0.3.6.dist-info/entry_points.txt,sha256=9b_Ca7vJoh6AwL3W8qAPh_UmJ_1Pa6hi-TDfCTDjvSk,43
4
- doppy-0.3.6.dist-info/licenses/LICENSE,sha256=V-0iroMNMI8ctnLgUau1kdFvwhkYhr9vi-5kWKxw2wc,1089
1
+ doppy-0.4.0.dist-info/METADATA,sha256=bnTR2j3JN6ZfhwrYNbhIj0i6HdM-zpergm-CCBQa_5I,4276
2
+ doppy-0.4.0.dist-info/WHEEL,sha256=9pH0c92OgjhIvXK7g4XTh8KK6q5vYgxz1YCJsDe0sC0,103
3
+ doppy-0.4.0.dist-info/entry_points.txt,sha256=9b_Ca7vJoh6AwL3W8qAPh_UmJ_1Pa6hi-TDfCTDjvSk,43
4
+ doppy-0.4.0.dist-info/licenses/LICENSE,sha256=V-0iroMNMI8ctnLgUau1kdFvwhkYhr9vi-5kWKxw2wc,1089
5
5
  doppy/options.py,sha256=73BDODO4OYHn2qOshhwz6u6G3J1kNd3uj6P0a3V4HBE,205
6
6
  doppy/__init__.py,sha256=Z9aEUlbPRWRUAoB8_-djkgrJuS4-6pjem4-mVSB6Z9I,191
7
7
  doppy/product/wind.py,sha256=hkSzAyi4tfPoGwUx8iE3l65oCDyzl_tRgtF2loxaEpY,16557
8
8
  doppy/product/__init__.py,sha256=C6s9cX20m9UwRsKo1lZH6TnYFfM5KmsX5MPUyShbgl4,235
9
9
  doppy/product/stare_depol.py,sha256=thrWzCpvdH3AiA6gmR37vrH_pDACNY2QTtqPteJ2s8Y,9653
10
- doppy/product/stare.py,sha256=OD9Ff0IUVvRVR-KRIfOiWIQq3sC5KKhYtlmnif81HcU,22625
10
+ doppy/product/stare.py,sha256=TE6BrrXFuwv9-zLTYCCn3HNpNgjwdldbyU6qk1__FzA,25343
11
11
  doppy/bench.py,sha256=iVNYveMVGGRES2oe3Orsn31jQFCKTXOmxRFuFiJ8_OA,248
12
12
  doppy/netcdf.py,sha256=34TzB9UyypTwDpT2MfrRD14g2zGP3X-NuEU15mQGaUI,3925
13
- doppy/utils.py,sha256=qPtIYBJPaKKTmRWwJI93TFUuhJg7CAoecpyHCm5ZyxI,214
13
+ doppy/utils.py,sha256=cY-tWrArrROX7lg_zJ2M4QJ6GUnU_BjhkUYL5aG9kbA,790
14
14
  doppy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
15
  doppy/exceptions.py,sha256=OzdLXmKs3qZrvzwaI0pxjzpM2w9J5Of7dCo_Ekygecc,183
16
- doppy/defaults.py,sha256=-il4DWU1HPttlGFoVQTPC7R4pDZERoPvt3kw6xwMKrw,38
16
+ doppy/defaults.py,sha256=-jR_xAVLf_soZNDu3uJ6mhOZa9L1tfKtt0k0tI6Rd9s,472
17
17
  doppy/data/cache.py,sha256=cxCZ7HyEQt2EKAGEiMqx3wJ2-5Y3hEAEPQ_XB4gKlkk,1160
18
18
  doppy/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  doppy/data/api.py,sha256=QaVKj304OPcu8OF5xgtduQzDis8Srn-I6UgR9qb2u9E,1863
20
20
  doppy/data/exceptions.py,sha256=6CS6OHIWq8CqlxiceEvC1j0EfWUYoIfM7dW88apQVn4,89
21
21
  doppy/raw/halo_sys_params.py,sha256=DeOIIRtVdO7HnNY3FaI4u035ScI65jfDgUrEccLepEo,3937
22
- doppy/raw/__init__.py,sha256=AMHyONuH0aUJUQz20EhlANaq9UjWJtSZf7kWUVx3ZjA,228
23
- doppy/raw/wls70.py,sha256=PMyr_JYTiKuYv1JpWHgVX6OA5zU83DOnr8vBCxFyFAw,7420
22
+ doppy/raw/__init__.py,sha256=4pj4xSK9rA0XEfM79NsRBRYId7GaJRDCyPABxbEm63g,260
23
+ doppy/raw/wls70.py,sha256=TePgyhmlWaUQCw1vkWh7uD7KT5EFMaDhNeVYoF2rspc,7510
24
24
  doppy/raw/halo_bg.py,sha256=8t9j-SUF1yJht3vrT6KAYJQyxcg3W-0zr8h0jAEhWes,5815
25
- doppy/raw/halo_hpl.py,sha256=YFTepAWXvIfAVGgLZCadsc1d-VaOh7j2RB3cpb6Sv6k,18486
26
- doppy/raw/windcube.py,sha256=LO5TXxWGdVxYD-kGwjaIebwEnEegB0AyxMXynkdrJqE,9952
25
+ doppy/raw/halo_hpl.py,sha256=yzVPQEATnDVf1K3N-mGLXR4_RuT-E94OVXathT2bHes,18580
26
+ doppy/raw/windcube.py,sha256=vgkoXVgcbI2Or4BsFCeq2STdjUP49h3mBQLoDXdBQHM,19074
27
27
  doppy/__main__.py,sha256=zrKQJVj0k0ypBQCGK65Czt9G9FZ_qx3ussw6Q9VJ14g,346
28
- doppy/rs.abi3.so,sha256=oKXPJzvWd4h79MPSQ-kNf6g3YUehmB2-PoTGW3jD_cY,2569712
29
- doppy-0.3.6.dist-info/RECORD,,
28
+ doppy/rs.abi3.so,sha256=o1UmwiBlSeRyDR2KeIm83bBpGJyTk9DdywuVjJUnTos,2583328
29
+ doppy-0.4.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: maturin (1.7.4)
2
+ Generator: maturin (1.7.7)
3
3
  Root-Is-Purelib: false
4
4
  Tag: cp310-abi3-macosx_11_0_arm64