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/__init__.py +6 -0
- doppy/bench.py +13 -0
- doppy/data/__init__.py +0 -0
- doppy/data/api.py +58 -0
- doppy/data/cache.py +43 -0
- doppy/data/exceptions.py +6 -0
- doppy/defaults.py +18 -0
- doppy/exceptions.py +14 -0
- doppy/netcdf.py +134 -0
- doppy/options.py +13 -0
- doppy/product/__init__.py +6 -0
- doppy/product/noise_utils.py +106 -0
- doppy/product/stare.py +807 -0
- doppy/product/stare_depol.py +308 -0
- doppy/product/turbulence.py +264 -0
- doppy/product/utils.py +12 -0
- doppy/product/wind.py +460 -0
- doppy/py.typed +0 -0
- doppy/raw/__init__.py +16 -0
- doppy/raw/halo_bg.py +173 -0
- doppy/raw/halo_hpl.py +480 -0
- doppy/raw/halo_sys_params.py +135 -0
- doppy/raw/utils.py +14 -0
- doppy/raw/windcube.py +477 -0
- doppy/raw/wls70.py +175 -0
- doppy/raw/wls77.py +163 -0
- doppy/rs.abi3.so +0 -0
- doppy/utils.py +24 -0
- doppy-0.5.9.dist-info/METADATA +144 -0
- doppy-0.5.9.dist-info/RECORD +33 -0
- doppy-0.5.9.dist-info/WHEEL +4 -0
- doppy-0.5.9.dist-info/entry_points.txt +2 -0
- doppy-0.5.9.dist-info/licenses/LICENSE +21 -0
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
|
+
)
|