doppy 0.5.9__cp310-abi3-macosx_10_12_x86_64.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.
doppy/raw/windcube.py ADDED
@@ -0,0 +1,477 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from io import BufferedIOBase
5
+ from pathlib import Path
6
+ from typing import Sequence
7
+
8
+ import numpy as np
9
+ import numpy.typing as npt
10
+ from netCDF4 import Dataset, Variable, num2date
11
+ from numpy import datetime64
12
+
13
+ from doppy.utils import merge_all_equal
14
+
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: (), unit: seconds
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.array(~np.all(np.isnan(self.cnr), axis=1), dtype=np.bool_)]
101
+
102
+
103
+ @dataclass
104
+ class WindCube:
105
+ time: npt.NDArray[datetime64] # dim: (time, )
106
+ radial_distance: npt.NDArray[np.float64] # dim: (time, radial_distance)
107
+ height: npt.NDArray[np.float64] # dim: (time,radial_distance)
108
+ azimuth: npt.NDArray[np.float64] # dim: (time, )
109
+ elevation: npt.NDArray[np.float64] # dim: (time, )
110
+ cnr: npt.NDArray[np.float64] # dim: (time, radial_distance)
111
+ radial_velocity: npt.NDArray[np.float64] # dim: (time, radial_distance)
112
+ radial_velocity_confidence: npt.NDArray[np.float64] # dim: (time, radial_distance)
113
+ scan_index: npt.NDArray[np.int64]
114
+ system_id: str
115
+
116
+ @classmethod
117
+ def from_vad_or_dbs_srcs(
118
+ cls,
119
+ data: Sequence[str]
120
+ | Sequence[Path]
121
+ | Sequence[bytes]
122
+ | Sequence[BufferedIOBase],
123
+ ) -> list[WindCube]:
124
+ return [WindCube.from_vad_or_dbs_src(src) for src in data]
125
+
126
+ @classmethod
127
+ def from_vad_or_dbs_src(cls, data: str | Path | bytes | BufferedIOBase) -> WindCube:
128
+ data_bytes = _src_to_bytes(data)
129
+ nc = Dataset("inmemory.nc", "r", memory=data_bytes)
130
+ return _from_vad_or_dbs_src(nc)
131
+
132
+ @classmethod
133
+ def merge(cls, raws: list[WindCube]) -> WindCube:
134
+ return WindCube(
135
+ scan_index=_merge_scan_index([r.scan_index for r in raws]),
136
+ time=np.concatenate([r.time for r in raws]),
137
+ height=np.concatenate([r.height for r in raws]),
138
+ radial_distance=np.concatenate([r.radial_distance for r in raws]),
139
+ azimuth=np.concatenate([r.azimuth for r in raws]),
140
+ elevation=np.concatenate([r.elevation for r in raws]),
141
+ radial_velocity=np.concatenate([r.radial_velocity for r in raws]),
142
+ radial_velocity_confidence=np.concatenate(
143
+ [r.radial_velocity_confidence for r in raws]
144
+ ),
145
+ cnr=np.concatenate([r.cnr for r in raws]),
146
+ system_id=merge_all_equal("system_id", [r.system_id for r in raws]),
147
+ )
148
+
149
+ def __getitem__(
150
+ self,
151
+ index: int
152
+ | slice
153
+ | list[int]
154
+ | npt.NDArray[np.int64]
155
+ | npt.NDArray[np.bool_]
156
+ | tuple[slice, slice],
157
+ ) -> WindCube:
158
+ if isinstance(index, (int, slice, list, np.ndarray)):
159
+ return WindCube(
160
+ time=self.time[index],
161
+ radial_distance=self.radial_distance[index],
162
+ height=self.height[index],
163
+ azimuth=self.azimuth[index],
164
+ elevation=self.elevation[index],
165
+ radial_velocity=self.radial_velocity[index],
166
+ radial_velocity_confidence=self.radial_velocity_confidence[index],
167
+ cnr=self.cnr[index],
168
+ scan_index=self.scan_index[index],
169
+ system_id=self.system_id,
170
+ )
171
+ raise TypeError
172
+
173
+ def sorted_by_time(self) -> WindCube:
174
+ sort_indices = np.argsort(self.time)
175
+ return self[sort_indices]
176
+
177
+ def non_strictly_increasing_timesteps_removed(self) -> WindCube:
178
+ if len(self.time) == 0:
179
+ return self
180
+ mask = np.ones_like(self.time, dtype=np.bool_)
181
+ latest_time = self.time[0]
182
+ for i, t in enumerate(self.time[1:], start=1):
183
+ if t <= latest_time:
184
+ mask[i] = False
185
+ else:
186
+ latest_time = t
187
+ return self[mask]
188
+
189
+ def reindex_scan_indices(self) -> WindCube:
190
+ new_indices = np.zeros_like(self.scan_index)
191
+ indexed = set()
192
+ j = 0
193
+ for i in self.scan_index:
194
+ if i in indexed:
195
+ continue
196
+ new_indices[i == self.scan_index] = j
197
+ indexed.add(i)
198
+ j += 1
199
+ self.scan_index = new_indices
200
+ return self
201
+
202
+
203
+ def _merge_scan_index(index_list: list[npt.NDArray[np.int64]]) -> npt.NDArray[np.int64]:
204
+ if len(index_list) == 0:
205
+ raise ValueError("cannot merge empty list")
206
+
207
+ new_index_list = []
208
+ current_max = index_list[0].max()
209
+ new_index_list.append(index_list[0])
210
+
211
+ for index_arr in index_list[1:]:
212
+ new_arr = index_arr + current_max + 1
213
+ new_index_list.append(new_arr)
214
+ current_max += index_arr.max() + 1
215
+ return np.concatenate(new_index_list)
216
+
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
+
235
+ def _src_to_bytes(data: str | Path | bytes | BufferedIOBase) -> bytes:
236
+ if isinstance(data, str):
237
+ path = Path(data)
238
+ with path.open("rb") as f:
239
+ return f.read()
240
+ elif isinstance(data, Path):
241
+ with data.open("rb") as f:
242
+ return f.read()
243
+ elif isinstance(data, bytes):
244
+ return data
245
+ elif isinstance(data, BufferedIOBase):
246
+ return data.read()
247
+
248
+
249
+ def _from_fixed_src(nc: Dataset) -> WindCubeFixed:
250
+ time_list = []
251
+ cnr_list = []
252
+ relative_beta_list = []
253
+ radial_wind_speed_list = []
254
+ radial_wind_speed_confidence_list = []
255
+ azimuth_list = []
256
+ elevation_list = []
257
+ range_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
+ * 1e-3 # convert ms to s
304
+ )
305
+
306
+ return WindCubeFixed(
307
+ time=np.concatenate(time_list),
308
+ radial_distance=np.concatenate(range_list),
309
+ azimuth=np.concatenate(azimuth_list),
310
+ elevation=np.concatenate(elevation_list),
311
+ radial_velocity=np.concatenate(radial_wind_speed_list),
312
+ radial_velocity_confidence=np.concatenate(radial_wind_speed_confidence_list),
313
+ cnr=np.concatenate(cnr_list),
314
+ relative_beta=np.concatenate(relative_beta_list),
315
+ doppler_spectrum_width=np.concatenate(doppler_spectrum_width_list),
316
+ ray_accumulation_time=merge_all_equal(
317
+ "ray_accumulation_time",
318
+ list(np.array(ray_accumulation_time_list, dtype=np.float64)),
319
+ ),
320
+ system_id=nc.instrument_name,
321
+ )
322
+
323
+
324
+ def _from_vad_or_dbs_src(nc: Dataset) -> WindCube:
325
+ scan_index_list: list[npt.NDArray[np.int64]] = []
326
+ time_list: list[npt.NDArray[np.datetime64]] = []
327
+ cnr_list: list[npt.NDArray[np.float64]] = []
328
+ radial_wind_speed_list: list[npt.NDArray[np.float64]] = []
329
+ radial_wind_speed_confidence_list: list[npt.NDArray[np.float64]] = []
330
+ azimuth_list: list[npt.NDArray[np.float64]] = []
331
+ elevation_list: list[npt.NDArray[np.float64]] = []
332
+ range_list: list[npt.NDArray[np.float64]] = []
333
+ height_list: list[npt.NDArray[np.float64]] = []
334
+
335
+ time_reference = (
336
+ nc["time_reference"][:] if "time_reference" in nc.variables else None
337
+ )
338
+
339
+ expected_dimensions = ("time", "gate_index")
340
+ for i, group in enumerate(
341
+ nc[group] for group in (nc.variables["sweep_group_name"][:])
342
+ ):
343
+ time_reference_ = time_reference
344
+ if time_reference is None and "time_reference" in group.variables:
345
+ time_reference_ = group["time_reference"][:]
346
+
347
+ time_list.append(_extract_datetime64_or_raise(group["time"], time_reference_))
348
+ radial_wind_speed_list.append(
349
+ _extract_float64_or_raise(group["radial_wind_speed"], expected_dimensions)
350
+ )
351
+ cnr_list.append(_extract_float64_or_raise(group["cnr"], expected_dimensions))
352
+ radial_wind_speed_confidence_list.append(
353
+ _extract_float64_or_raise(
354
+ group["radial_wind_speed_ci"], expected_dimensions
355
+ )
356
+ )
357
+ azimuth_list.append(
358
+ _extract_float64_or_raise(group["azimuth"], expected_dimensions)
359
+ )
360
+ elevation_list.append(
361
+ _extract_float64_or_raise(group["elevation"], expected_dimensions)
362
+ )
363
+ range_list.append(
364
+ _extract_float64_or_raise(group["range"], expected_dimensions)
365
+ )
366
+ height_list.append(
367
+ _extract_float64_or_raise(group["measurement_height"], expected_dimensions)
368
+ )
369
+ scan_index_list.append(np.full(group["time"][:].shape, i, dtype=np.int64))
370
+
371
+ return WindCube(
372
+ scan_index=np.concatenate(scan_index_list),
373
+ time=np.concatenate(time_list),
374
+ radial_distance=np.concatenate(range_list),
375
+ height=np.concatenate(height_list),
376
+ azimuth=np.concatenate(azimuth_list),
377
+ elevation=np.concatenate(elevation_list),
378
+ radial_velocity=np.concatenate(radial_wind_speed_list),
379
+ radial_velocity_confidence=np.concatenate(radial_wind_speed_confidence_list),
380
+ cnr=np.concatenate(cnr_list),
381
+ system_id=nc.instrument_name,
382
+ )
383
+
384
+
385
+ def _extract_datetime64_or_raise(
386
+ nc: Variable[npt.NDArray[np.float64]], time_reference: str | None
387
+ ) -> npt.NDArray[np.datetime64]:
388
+ match nc.name:
389
+ case "time":
390
+ if nc.dimensions != ("time",):
391
+ raise ValueError
392
+
393
+ units = nc.units
394
+ if "time_reference" in nc.units:
395
+ if time_reference is not None:
396
+ units = nc.units.replace("time_reference", time_reference)
397
+ else:
398
+ raise ValueError("Unknown time_reference")
399
+ return np.array(num2date(nc[:], units=units), dtype="datetime64[us]")
400
+ case _:
401
+ raise ValueError(f"Unexpected variable name {nc.name}")
402
+
403
+
404
+ def _dB_to_ratio(decibels: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
405
+ return 10 ** (0.1 * decibels)
406
+
407
+
408
+ def _extract_float64_or_raise(
409
+ nc: Variable[npt.NDArray[np.float64]], expected_dimensions: tuple[str, ...]
410
+ ) -> npt.NDArray[np.float64]:
411
+ match nc.name:
412
+ case "range" | "measurement_height":
413
+ if nc.dimensions != expected_dimensions:
414
+ raise ValueError(f"Unexpected dimensions for {nc.name}")
415
+ if nc.units != "m":
416
+ raise ValueError(f"Unexpected units for {nc.name}")
417
+ if nc[:].mask is not np.bool_(False):
418
+ raise ValueError
419
+ return np.array(nc[:].data, dtype=np.float64)
420
+ case "cnr":
421
+ if nc.dimensions != expected_dimensions:
422
+ raise ValueError(f"Unexpected dimensions for {nc.name}")
423
+ if nc.units != "dB":
424
+ raise ValueError(f"Unexpected units for {nc.name}")
425
+ if nc[:].mask is not np.bool_(False):
426
+ pass # ignore that array contains masked values
427
+ return _dB_to_ratio(np.array(nc[:].data, dtype=np.float64))
428
+ case "relative_beta":
429
+ if nc.dimensions != expected_dimensions:
430
+ raise ValueError(f"Unexpected dimensions for {nc.name}")
431
+ if nc.units != "m-1 sr-1":
432
+ raise ValueError(f"Unexpected units for {nc.name}")
433
+ if nc[:].mask is not np.bool_(False):
434
+ pass # ignore that array contains masked values
435
+ return np.array(nc[:].data, dtype=np.float64)
436
+ case "radial_wind_speed":
437
+ if nc.dimensions != expected_dimensions:
438
+ raise ValueError(f"Unexpected dimensions for {nc.name}")
439
+ if nc.units != "m s-1":
440
+ raise ValueError(f"Unexpected units for {nc.name}")
441
+ if nc[:].mask is not np.bool_(False):
442
+ pass # ignore that array contains masked values
443
+ return np.array(nc[:].data, dtype=np.float64)
444
+ case "radial_wind_speed_ci":
445
+ if nc.dimensions != expected_dimensions:
446
+ raise ValueError(f"Unexpected dimensions for {nc.name}")
447
+ if nc.units != "percent":
448
+ raise ValueError(f"Unexpected units for {nc.name}")
449
+ if nc[:].mask is not np.bool_(False):
450
+ pass # ignore that array contains masked values
451
+ return np.array(nc[:].data, dtype=np.float64)
452
+ case "azimuth" | "elevation":
453
+ if nc.dimensions != (expected_dimensions[0],):
454
+ raise ValueError(f"Unexpected dimensions for {nc.name}")
455
+ if nc.units != "degrees":
456
+ raise ValueError(f"Unexpected units for {nc.name}")
457
+ if nc[:].mask is not np.bool_(False):
458
+ raise ValueError
459
+ return np.array(nc[:].data, dtype=np.float64)
460
+ case "doppler_spectrum_width":
461
+ if nc.dimensions != expected_dimensions:
462
+ raise ValueError(f"Unexpected dimensions for {nc.name}")
463
+ if nc.units != "m s-1":
464
+ raise ValueError(f"Unexpected units for {nc.name}")
465
+ if nc[:].mask is not np.bool_(False):
466
+ pass # ignore that array contains masked values
467
+ return np.array(nc[:].data, dtype=np.float64)
468
+ case "ray_accumulation_time":
469
+ if nc.dimensions != ():
470
+ raise ValueError(f"Unexpected dimensions for {nc.name}")
471
+ if nc.units != "ms":
472
+ raise ValueError(f"Unexpected units for {nc.name}")
473
+ if nc[:].mask is not np.bool_(False):
474
+ raise ValueError(f"Variable {nc.name} contains masked values")
475
+ return np.array(nc[:].data, dtype=np.float64)
476
+ case _:
477
+ raise ValueError(f"Unexpected variable name {nc.name}")
doppy/raw/wls70.py ADDED
@@ -0,0 +1,175 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime, timezone
5
+ from io import BufferedIOBase
6
+ from pathlib import Path
7
+ from typing import Any, Sequence
8
+
9
+ import numpy as np
10
+ import numpy.typing as npt
11
+ from numpy import datetime64
12
+
13
+ import doppy
14
+ from doppy import exceptions
15
+ from doppy.raw.utils import bytes_from_src
16
+ from doppy.utils import merge_all_equal
17
+
18
+
19
+ @dataclass
20
+ class Wls70:
21
+ time: npt.NDArray[datetime64] # dim: (time, )
22
+ altitude: npt.NDArray[np.float64] # dim: (altitude, )
23
+ position: npt.NDArray[np.float64] # dim: (time, )
24
+ temperature: npt.NDArray[np.float64] # dim: (time, )
25
+ wiper: npt.NDArray[np.bool_] # dim: (time, )
26
+ cnr: npt.NDArray[np.float64] # dim: (time, altitude)
27
+ radial_velocity: npt.NDArray[np.float64] # dim: (time, altitude)
28
+ radial_velocity_deviation: npt.NDArray[np.float64] # dim: (time, altitude)
29
+ vh: npt.NDArray[np.float64] # dim: (time, altitude)
30
+ wind_direction: npt.NDArray[np.float64] # dim: (time, altitude)
31
+ zonal_wind: npt.NDArray[np.float64] # u := zonal wind?, dim: (time, altitude)
32
+ meridional_wind: npt.NDArray[
33
+ np.float64
34
+ ] # v := meridional wind?, dim: (time, altitude)
35
+ vertical_wind: npt.NDArray[np.float64] # w := vertical wind?, dim: (time, altitude)
36
+ system_id: str
37
+ cnr_threshold: float
38
+
39
+ @classmethod
40
+ def from_srcs(
41
+ cls, data: Sequence[str | bytes | Path | BufferedIOBase]
42
+ ) -> list[Wls70]:
43
+ data_bytes = [bytes_from_src(src) for src in data]
44
+ raws = doppy.rs.raw.wls70.from_bytes_srcs(data_bytes)
45
+ try:
46
+ return [_raw_rs_to_wls70(r) for r in raws]
47
+ except RuntimeError as err:
48
+ raise exceptions.RawParsingError(err) from err
49
+
50
+ @classmethod
51
+ def from_src(cls, data: str | Path | bytes | BufferedIOBase) -> Wls70:
52
+ data_bytes = bytes_from_src(data)
53
+ try:
54
+ return _raw_rs_to_wls70(doppy.rs.raw.wls70.from_bytes_src(data_bytes))
55
+ except RuntimeError as err:
56
+ raise exceptions.RawParsingError(err) from err
57
+
58
+ def __getitem__(
59
+ self,
60
+ index: int
61
+ | slice
62
+ | list[int]
63
+ | npt.NDArray[np.int64]
64
+ | npt.NDArray[np.bool_]
65
+ | tuple[slice, slice],
66
+ ) -> Wls70:
67
+ if isinstance(index, (int, slice, list, np.ndarray)):
68
+ return Wls70(
69
+ time=self.time[index],
70
+ altitude=self.altitude,
71
+ position=self.position[index],
72
+ temperature=self.temperature[index],
73
+ wiper=self.wiper[index],
74
+ cnr=self.cnr[index],
75
+ radial_velocity=self.radial_velocity[index],
76
+ radial_velocity_deviation=self.radial_velocity_deviation[index],
77
+ vh=self.vh[index],
78
+ wind_direction=self.wind_direction[index],
79
+ zonal_wind=self.zonal_wind[index],
80
+ meridional_wind=self.meridional_wind[index],
81
+ vertical_wind=self.vertical_wind[index],
82
+ system_id=self.system_id,
83
+ cnr_threshold=self.cnr_threshold,
84
+ )
85
+ raise TypeError
86
+
87
+ def sorted_by_time(self) -> Wls70:
88
+ sort_indices = np.argsort(self.time)
89
+ return self[sort_indices]
90
+
91
+ @classmethod
92
+ def merge(cls, raws: Sequence[Wls70]) -> Wls70:
93
+ return cls(
94
+ time=np.concatenate(tuple(r.time for r in raws)),
95
+ altitude=raws[0].altitude,
96
+ position=np.concatenate(tuple(r.position for r in raws)),
97
+ temperature=np.concatenate(tuple(r.temperature for r in raws)),
98
+ wiper=np.concatenate(tuple(r.wiper for r in raws)),
99
+ cnr=np.concatenate(tuple(r.cnr for r in raws)),
100
+ radial_velocity=np.concatenate(tuple(r.radial_velocity for r in raws)),
101
+ radial_velocity_deviation=np.concatenate(
102
+ tuple(r.radial_velocity_deviation for r in raws)
103
+ ),
104
+ vh=np.concatenate(tuple(r.vh for r in raws)),
105
+ wind_direction=np.concatenate(tuple(r.wind_direction for r in raws)),
106
+ zonal_wind=np.concatenate(tuple(r.zonal_wind for r in raws)),
107
+ meridional_wind=np.concatenate(tuple(r.meridional_wind for r in raws)),
108
+ vertical_wind=np.concatenate(tuple(r.vertical_wind for r in raws)),
109
+ system_id=merge_all_equal("system_id", [r.system_id for r in raws]),
110
+ cnr_threshold=merge_all_equal(
111
+ "cnr_threshold", [r.cnr_threshold for r in raws]
112
+ ),
113
+ )
114
+
115
+ def non_strictly_increasing_timesteps_removed(self) -> Wls70:
116
+ if len(self.time) == 0:
117
+ return self
118
+ mask = np.ones_like(self.time, dtype=np.bool_)
119
+ latest_time = self.time[0]
120
+ for i, t in enumerate(self.time[1:], start=1):
121
+ if t <= latest_time:
122
+ mask[i] = False
123
+ else:
124
+ latest_time = t
125
+ return self[mask]
126
+
127
+
128
+ def _raw_rs_to_wls70(
129
+ raw_rs: tuple[dict[str, Any], list[str], npt.NDArray[np.float64]],
130
+ ) -> Wls70:
131
+ info, cols, data = raw_rs
132
+ altitude = info["altitude"]
133
+ system_id = info["system_id"]
134
+ cnr_threshold = float(info["cnr_threshold"])
135
+ data = data.reshape(-1, len(cols))
136
+ time_ts = data[:, 0]
137
+ time = np.array(
138
+ [
139
+ datetime64(datetime.fromtimestamp(ts, timezone.utc).replace(tzinfo=None))
140
+ for ts in time_ts
141
+ ]
142
+ )
143
+
144
+ position = data[:, 1]
145
+ temperature = data[:, 2]
146
+ wiper = np.array(np.isclose(data[:, 3], 1), dtype=np.bool_)
147
+ cnr = data[:, 4::8]
148
+ rws = data[:, 5::8]
149
+ rwsd = data[:, 6::8]
150
+ vh = data[:, 7::8]
151
+ direction = data[:, 8::8]
152
+ u = data[:, 9::8]
153
+ v = data[:, 10::8]
154
+ w = data[:, 11::8]
155
+ mask = (np.abs(u) > 90) | (np.abs(v) > 90) | (np.abs(w) > 90)
156
+ u[mask] = np.nan
157
+ v[mask] = np.nan
158
+ w[mask] = np.nan
159
+ return Wls70(
160
+ time=time,
161
+ altitude=altitude,
162
+ position=position,
163
+ temperature=temperature,
164
+ wiper=wiper,
165
+ cnr=cnr,
166
+ radial_velocity=rws,
167
+ radial_velocity_deviation=rwsd,
168
+ vh=vh,
169
+ wind_direction=direction,
170
+ zonal_wind=u,
171
+ meridional_wind=v,
172
+ vertical_wind=w,
173
+ system_id=system_id,
174
+ cnr_threshold=cnr_threshold,
175
+ )