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
|
@@ -0,0 +1,308 @@
|
|
|
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
|
+
|
|
11
|
+
import doppy
|
|
12
|
+
from doppy import options
|
|
13
|
+
from doppy.product.stare import PulsesPerRay, RayAccumulationTime, Stare
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class StareDepol:
|
|
18
|
+
"""
|
|
19
|
+
Stare product with depolarisation ratio derived from co-polarised and
|
|
20
|
+
cross-polarised stare data.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
-----------
|
|
24
|
+
time
|
|
25
|
+
An array of datetime64 objects representing the observation times.
|
|
26
|
+
radial_distance
|
|
27
|
+
An array of radial distances from the observation point, in meters.
|
|
28
|
+
elevation
|
|
29
|
+
An array of elevation angles corresponding to the observation points, in
|
|
30
|
+
degrees.
|
|
31
|
+
beta
|
|
32
|
+
An array of backscatter coefficients for the co-polarised signal, in
|
|
33
|
+
sr-1 m-1.
|
|
34
|
+
beta_cross
|
|
35
|
+
An array of backscatter coefficients for the cross-polarised signal, in
|
|
36
|
+
sr-1 m-1.
|
|
37
|
+
radial_velocity
|
|
38
|
+
An array of radial velocities of the co-polarised signal, in m s-1.
|
|
39
|
+
mask_beta
|
|
40
|
+
A boolean array indicating signal (True) or noise (False) data points.
|
|
41
|
+
mask_radial_velocity
|
|
42
|
+
A boolean array indicating signal (True) or noise (False) data points.
|
|
43
|
+
depolarisation
|
|
44
|
+
An array of depolarisation ratios calculated as the ratio of
|
|
45
|
+
co-polarised to cross-polarised backscatter coefficients.
|
|
46
|
+
wavelength
|
|
47
|
+
The wavelength of the lidar, in meters.
|
|
48
|
+
system_id
|
|
49
|
+
A string identifier for the lidar.
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
Raises
|
|
53
|
+
------
|
|
54
|
+
ValueError
|
|
55
|
+
If the input `co` and `cross` products have mismatched wavelengths,
|
|
56
|
+
system IDs, radial distances, or elevation angles, this exception is
|
|
57
|
+
raised.
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
References
|
|
61
|
+
----------
|
|
62
|
+
Aerosol particle depolarization ratio at 1565 nm measured with a Halo Doppler lidar
|
|
63
|
+
authors: Ville Vakkari, Holger Baars, Stephanie Bohlmann, Johannes Bühl,
|
|
64
|
+
Mika Komppula, Rodanthi-Elisavet Mamouri, and Ewan James O'Connor
|
|
65
|
+
doi: https://doi.org/10.5194/acp-21-5807-2021
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
time: npt.NDArray[np.datetime64]
|
|
69
|
+
radial_distance: npt.NDArray[np.float64]
|
|
70
|
+
elevation: npt.NDArray[np.float64]
|
|
71
|
+
beta: npt.NDArray[np.float64]
|
|
72
|
+
beta_cross: npt.NDArray[np.float64]
|
|
73
|
+
radial_velocity: npt.NDArray[np.float64]
|
|
74
|
+
mask_beta: npt.NDArray[np.bool_]
|
|
75
|
+
mask_radial_velocity: npt.NDArray[np.bool_]
|
|
76
|
+
depolarisation: npt.NDArray[np.float64]
|
|
77
|
+
polariser_bleed_through: float
|
|
78
|
+
wavelength: float
|
|
79
|
+
system_id: str
|
|
80
|
+
ray_info: RayAccumulationTime | PulsesPerRay
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
co: Stare,
|
|
85
|
+
cross: Stare,
|
|
86
|
+
polariser_bleed_through: float = 0.0,
|
|
87
|
+
):
|
|
88
|
+
"""
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
co: Stare
|
|
92
|
+
The co-polarised data.
|
|
93
|
+
cross: Stare
|
|
94
|
+
The cross-polarised data. The `cross.time` array is expected to be sorted.
|
|
95
|
+
polariser_bleed_through: float, default=0.0
|
|
96
|
+
The amount of bleed-through from the polariser.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
if co.beta.shape[1] != cross.beta.shape[1]:
|
|
100
|
+
raise doppy.exceptions.ShapeError(
|
|
101
|
+
"Range dimension mismatch in co and cross: "
|
|
102
|
+
f"{co.beta.shape[1]} vs {cross.beta.shape[1]}"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if not np.isclose(co.wavelength, cross.wavelength):
|
|
106
|
+
raise ValueError(
|
|
107
|
+
"Different wavelength in co and cross: "
|
|
108
|
+
f"{co.wavelength} vs {cross.wavelength}"
|
|
109
|
+
)
|
|
110
|
+
if co.system_id != cross.system_id:
|
|
111
|
+
raise ValueError(
|
|
112
|
+
"Different system ID in co and cross: "
|
|
113
|
+
f"{co.system_id} vs {cross.system_id}"
|
|
114
|
+
)
|
|
115
|
+
if not np.allclose(co.radial_distance, cross.radial_distance, atol=1):
|
|
116
|
+
raise ValueError("Different radial distance in co and cross")
|
|
117
|
+
|
|
118
|
+
ind = np.searchsorted(cross.time, co.time, side="left")
|
|
119
|
+
pick_ind = ind < len(cross.time)
|
|
120
|
+
time_diff_threshold = 2 * np.median(np.diff(co.time))
|
|
121
|
+
co_cross_timediff_below_threshold = (
|
|
122
|
+
cross.time[ind[pick_ind]] - co.time[pick_ind] < time_diff_threshold
|
|
123
|
+
)
|
|
124
|
+
pick_ind[pick_ind] &= co_cross_timediff_below_threshold
|
|
125
|
+
|
|
126
|
+
if not np.allclose(
|
|
127
|
+
co.elevation[pick_ind], cross.elevation[ind[pick_ind]], atol=1
|
|
128
|
+
):
|
|
129
|
+
raise ValueError("Different elevation in co and cross")
|
|
130
|
+
|
|
131
|
+
depolarisation = np.full_like(co.beta, np.nan)
|
|
132
|
+
co_beta = co.beta[pick_ind]
|
|
133
|
+
depolarisation[pick_ind] = (
|
|
134
|
+
cross.beta[ind[pick_ind]] - polariser_bleed_through * co_beta
|
|
135
|
+
) / co_beta
|
|
136
|
+
cross_beta = np.full_like(co.beta, np.nan)
|
|
137
|
+
cross_beta[pick_ind] = cross.beta[ind[pick_ind]]
|
|
138
|
+
|
|
139
|
+
self.time = co.time
|
|
140
|
+
self.radial_distance = co.radial_distance
|
|
141
|
+
self.elevation = co.elevation
|
|
142
|
+
self.beta = co.beta
|
|
143
|
+
self.beta_cross = cross_beta
|
|
144
|
+
self.radial_velocity = co.radial_velocity
|
|
145
|
+
self.mask_beta = co.mask_beta
|
|
146
|
+
self.mask_radial_velocity = co.mask_radial_velocity
|
|
147
|
+
self.depolarisation = depolarisation
|
|
148
|
+
self.polariser_bleed_through = polariser_bleed_through
|
|
149
|
+
self.wavelength = co.wavelength
|
|
150
|
+
self.system_id = co.system_id
|
|
151
|
+
self.ray_info = co.ray_info
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def mask_depolarisation(self) -> npt.NDArray[np.bool_]:
|
|
155
|
+
return np.isnan(self.depolarisation)
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def mask_beta_cross(self) -> npt.NDArray[np.bool_]:
|
|
159
|
+
return np.isnan(self.beta_cross)
|
|
160
|
+
|
|
161
|
+
@classmethod
|
|
162
|
+
def from_halo_data(
|
|
163
|
+
cls,
|
|
164
|
+
co_data: Sequence[str]
|
|
165
|
+
| Sequence[Path]
|
|
166
|
+
| Sequence[bytes]
|
|
167
|
+
| Sequence[BufferedIOBase],
|
|
168
|
+
co_data_bg: Sequence[str]
|
|
169
|
+
| Sequence[Path]
|
|
170
|
+
| Sequence[tuple[bytes, str]]
|
|
171
|
+
| Sequence[tuple[BufferedIOBase, str]],
|
|
172
|
+
cross_data: Sequence[str]
|
|
173
|
+
| Sequence[Path]
|
|
174
|
+
| Sequence[bytes]
|
|
175
|
+
| Sequence[BufferedIOBase],
|
|
176
|
+
cross_data_bg: Sequence[str]
|
|
177
|
+
| Sequence[Path]
|
|
178
|
+
| Sequence[tuple[bytes, str]]
|
|
179
|
+
| Sequence[tuple[BufferedIOBase, str]],
|
|
180
|
+
bg_correction_method: options.BgCorrectionMethod,
|
|
181
|
+
polariser_bleed_through: float = 0,
|
|
182
|
+
) -> StareDepol:
|
|
183
|
+
co = Stare.from_halo_data(
|
|
184
|
+
data=co_data, data_bg=co_data_bg, bg_correction_method=bg_correction_method
|
|
185
|
+
)
|
|
186
|
+
cross = Stare.from_halo_data(
|
|
187
|
+
data=cross_data,
|
|
188
|
+
data_bg=cross_data_bg,
|
|
189
|
+
bg_correction_method=bg_correction_method,
|
|
190
|
+
)
|
|
191
|
+
return cls(co, cross, polariser_bleed_through)
|
|
192
|
+
|
|
193
|
+
def write_to_netcdf(self, filename: str | Path) -> None:
|
|
194
|
+
with doppy.netcdf.Dataset(filename) as nc:
|
|
195
|
+
nc.add_dimension("time")
|
|
196
|
+
nc.add_dimension("range")
|
|
197
|
+
nc.add_time(
|
|
198
|
+
name="time",
|
|
199
|
+
dimensions=("time",),
|
|
200
|
+
standard_name="time",
|
|
201
|
+
long_name="Time UTC",
|
|
202
|
+
data=self.time,
|
|
203
|
+
dtype="f8",
|
|
204
|
+
)
|
|
205
|
+
nc.add_variable(
|
|
206
|
+
name="range",
|
|
207
|
+
dimensions=("range",),
|
|
208
|
+
units="m",
|
|
209
|
+
data=self.radial_distance,
|
|
210
|
+
dtype="f4",
|
|
211
|
+
)
|
|
212
|
+
nc.add_variable(
|
|
213
|
+
name="elevation",
|
|
214
|
+
dimensions=("time",),
|
|
215
|
+
units="degrees",
|
|
216
|
+
data=self.elevation,
|
|
217
|
+
dtype="f4",
|
|
218
|
+
long_name="elevation from horizontal",
|
|
219
|
+
)
|
|
220
|
+
nc.add_variable(
|
|
221
|
+
name="beta_raw",
|
|
222
|
+
dimensions=("time", "range"),
|
|
223
|
+
units="sr-1 m-1",
|
|
224
|
+
data=self.beta,
|
|
225
|
+
dtype="f4",
|
|
226
|
+
)
|
|
227
|
+
nc.add_variable(
|
|
228
|
+
name="beta",
|
|
229
|
+
dimensions=("time", "range"),
|
|
230
|
+
units="sr-1 m-1",
|
|
231
|
+
data=self.beta,
|
|
232
|
+
dtype="f4",
|
|
233
|
+
mask=self.mask_beta,
|
|
234
|
+
)
|
|
235
|
+
nc.add_variable(
|
|
236
|
+
name="v",
|
|
237
|
+
dimensions=("time", "range"),
|
|
238
|
+
units="m s-1",
|
|
239
|
+
long_name="Doppler velocity",
|
|
240
|
+
data=self.radial_velocity,
|
|
241
|
+
dtype="f4",
|
|
242
|
+
mask=self.mask_radial_velocity,
|
|
243
|
+
)
|
|
244
|
+
nc.add_scalar_variable(
|
|
245
|
+
name="wavelength",
|
|
246
|
+
units="m",
|
|
247
|
+
standard_name="radiation_wavelength",
|
|
248
|
+
data=self.wavelength,
|
|
249
|
+
dtype="f4",
|
|
250
|
+
)
|
|
251
|
+
nc.add_variable(
|
|
252
|
+
name="depolarisation_raw",
|
|
253
|
+
dimensions=("time", "range"),
|
|
254
|
+
units="1",
|
|
255
|
+
data=self.depolarisation,
|
|
256
|
+
dtype="f4",
|
|
257
|
+
mask=self.mask_depolarisation,
|
|
258
|
+
)
|
|
259
|
+
nc.add_variable(
|
|
260
|
+
name="depolarisation",
|
|
261
|
+
dimensions=("time", "range"),
|
|
262
|
+
units="1",
|
|
263
|
+
data=self.depolarisation,
|
|
264
|
+
dtype="f4",
|
|
265
|
+
mask=self.mask_beta | self.mask_depolarisation,
|
|
266
|
+
)
|
|
267
|
+
nc.add_variable(
|
|
268
|
+
name="beta_cross_raw",
|
|
269
|
+
dimensions=("time", "range"),
|
|
270
|
+
units="sr-1 m-1",
|
|
271
|
+
data=self.beta_cross,
|
|
272
|
+
mask=self.mask_beta_cross,
|
|
273
|
+
dtype="f4",
|
|
274
|
+
)
|
|
275
|
+
nc.add_variable(
|
|
276
|
+
name="beta_cross",
|
|
277
|
+
dimensions=("time", "range"),
|
|
278
|
+
units="sr-1 m-1",
|
|
279
|
+
data=self.beta_cross,
|
|
280
|
+
mask=self.mask_beta | self.mask_beta_cross,
|
|
281
|
+
dtype="f4",
|
|
282
|
+
)
|
|
283
|
+
nc.add_scalar_variable(
|
|
284
|
+
name="polariser_bleed_through",
|
|
285
|
+
units="1",
|
|
286
|
+
long_name="Polariser bleed-through",
|
|
287
|
+
data=self.polariser_bleed_through,
|
|
288
|
+
dtype="f4",
|
|
289
|
+
)
|
|
290
|
+
match self.ray_info:
|
|
291
|
+
case RayAccumulationTime(value):
|
|
292
|
+
nc.add_scalar_variable(
|
|
293
|
+
name="ray_accumulation_time",
|
|
294
|
+
units="s",
|
|
295
|
+
long_name="ray accumulation time",
|
|
296
|
+
data=value,
|
|
297
|
+
dtype="f4",
|
|
298
|
+
)
|
|
299
|
+
case PulsesPerRay(value):
|
|
300
|
+
nc.add_scalar_variable(
|
|
301
|
+
name="pulses_per_ray",
|
|
302
|
+
units="1",
|
|
303
|
+
long_name="pulses per ray",
|
|
304
|
+
data=value,
|
|
305
|
+
dtype="u4",
|
|
306
|
+
)
|
|
307
|
+
nc.add_attribute("serial_number", self.system_id)
|
|
308
|
+
nc.add_attribute("doppy_version", doppy.__version__)
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import numpy.typing as npt
|
|
7
|
+
from scipy.interpolate import RegularGridInterpolator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class HorizontalWind:
|
|
12
|
+
time: npt.NDArray[np.datetime64]
|
|
13
|
+
height: npt.NDArray[np.float64] # Height in meters from reference
|
|
14
|
+
V: npt.NDArray[np.float64] # Horizontal wind speed in m/s
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class VerticalWind:
|
|
19
|
+
time: npt.NDArray[np.datetime64]
|
|
20
|
+
height: npt.NDArray[np.float64] # Height in meters from reference
|
|
21
|
+
w: npt.NDArray[np.float64] # Vertical wind speed in m/s
|
|
22
|
+
mask: npt.NDArray[np.bool_] # mask[t,h] = True iff w[t,h] should be masked
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Options:
|
|
27
|
+
ray_accumulation_time: float # in seconds
|
|
28
|
+
period: float = 600 # period for computing the variance in seconds
|
|
29
|
+
beam_divergence: float = 33e-6 # radians
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class Turbulence:
|
|
34
|
+
time: npt.NDArray[np.datetime64]
|
|
35
|
+
height: npt.NDArray[np.float64]
|
|
36
|
+
turbulent_kinetic_energy_dissipation_rate: npt.NDArray[np.float64]
|
|
37
|
+
mask: npt.NDArray[np.bool_]
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def from_winds(
|
|
41
|
+
cls, vert: VerticalWind, hori: HorizontalWind, options: Options
|
|
42
|
+
) -> Turbulence:
|
|
43
|
+
V = _preprocess_horiontal_wind(vert, hori, options)
|
|
44
|
+
ls_low = _length_scale_low(V, vert.height, options)
|
|
45
|
+
res = _compute_variance(vert, options.period)
|
|
46
|
+
sampling_time = _sampling_time_in_seconds(res)
|
|
47
|
+
ls_up = V * sampling_time
|
|
48
|
+
dissipation_rate = _compute_dissipation_rate(res.variance, ls_low, ls_up)
|
|
49
|
+
mask = np.isnan(dissipation_rate) | vert.mask
|
|
50
|
+
return cls(
|
|
51
|
+
time=vert.time.copy(),
|
|
52
|
+
height=vert.height.copy(),
|
|
53
|
+
turbulent_kinetic_energy_dissipation_rate=dissipation_rate,
|
|
54
|
+
mask=mask,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _sampling_time_in_seconds(r: VarResult) -> npt.NDArray[np.float64]:
|
|
59
|
+
if not all(
|
|
60
|
+
(
|
|
61
|
+
t == np.dtype("datetime64[us]")
|
|
62
|
+
for t in (r.period_start.dtype, r.period_stop.dtype)
|
|
63
|
+
)
|
|
64
|
+
):
|
|
65
|
+
raise ValueError("period times must be on datetime64[us]")
|
|
66
|
+
td = r.period_stop - r.period_start
|
|
67
|
+
td_in_seconds = td / np.timedelta64(1, "s")
|
|
68
|
+
return np.array(td_in_seconds, dtype=np.float64)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class VarResult:
|
|
73
|
+
variance: npt.NDArray[np.float64]
|
|
74
|
+
period_start: npt.NDArray[np.datetime64]
|
|
75
|
+
period_stop: npt.NDArray[np.datetime64]
|
|
76
|
+
nsamples: npt.NDArray[np.int64]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _compute_variance(vert: VerticalWind, period: float) -> VarResult:
|
|
80
|
+
# NOTE: numerically unstable
|
|
81
|
+
|
|
82
|
+
# To compute actual time window
|
|
83
|
+
next_valid = _next_valid_from_mask(vert.mask)
|
|
84
|
+
prev_valid = _prev_valid_from_mask(vert.mask)
|
|
85
|
+
|
|
86
|
+
X = vert.w.copy()
|
|
87
|
+
X[vert.mask] = 0
|
|
88
|
+
X2 = X**2
|
|
89
|
+
X_cumsum = X.cumsum(axis=0)
|
|
90
|
+
X2_cumsum = X2.cumsum(axis=0)
|
|
91
|
+
|
|
92
|
+
N_i = (~vert.mask).astype(int)
|
|
93
|
+
N_cumsum = N_i.cumsum(axis=0)
|
|
94
|
+
|
|
95
|
+
def N_func(i: int, j: int) -> npt.NDArray[np.float64]:
|
|
96
|
+
return np.array(N_cumsum[j] - N_cumsum[i] + N_i[i], dtype=np.float64)
|
|
97
|
+
|
|
98
|
+
def S(i: int, j: int) -> npt.NDArray[np.float64]:
|
|
99
|
+
return np.array(X_cumsum[j] - X_cumsum[i] + X[i], dtype=np.float64)
|
|
100
|
+
|
|
101
|
+
def S2(i: int, j: int) -> npt.NDArray[np.float64]:
|
|
102
|
+
return np.array(X2_cumsum[j] - X2_cumsum[i] + X2[i], dtype=np.float64)
|
|
103
|
+
|
|
104
|
+
def var_ij(i: int, j: int) -> npt.NDArray[np.float64]:
|
|
105
|
+
N = N_func(i, j)
|
|
106
|
+
with np.errstate(invalid="ignore"):
|
|
107
|
+
return np.array((S2(i, j) - S(i, j) ** 2 / N) / N, dtype=np.float64)
|
|
108
|
+
|
|
109
|
+
half_period = np.timedelta64(int(1e6 * period / 2), "us")
|
|
110
|
+
period_start = np.full(vert.w.shape, np.datetime64("NaT", "us"))
|
|
111
|
+
period_stop = np.full(vert.w.shape, np.datetime64("NaT", "us"))
|
|
112
|
+
var = np.full(vert.w.shape, np.nan, dtype=np.float64)
|
|
113
|
+
nsamples = np.zeros_like(vert.w, dtype=np.int64)
|
|
114
|
+
i = 0
|
|
115
|
+
j = 0
|
|
116
|
+
n = len(vert.time)
|
|
117
|
+
for k, t in enumerate(vert.time):
|
|
118
|
+
while i + 1 < n and t - vert.time[i + 1] >= half_period:
|
|
119
|
+
i += 1
|
|
120
|
+
while j + 1 < n and vert.time[j] - t < half_period:
|
|
121
|
+
j += 1
|
|
122
|
+
i_valid = next_valid[i]
|
|
123
|
+
i_inbound = (0 <= i_valid) & (i_valid < n)
|
|
124
|
+
j_valid = prev_valid[j]
|
|
125
|
+
j_inbound = (0 <= j_valid) & (j_valid < n)
|
|
126
|
+
period_start[k][i_inbound] = vert.time[i_valid[i_inbound]]
|
|
127
|
+
period_stop[k][j_inbound] = vert.time[j_valid[j_inbound]]
|
|
128
|
+
var[k] = var_ij(i, j)
|
|
129
|
+
nsamples[k] = N_func(i, j)
|
|
130
|
+
return VarResult(
|
|
131
|
+
variance=var,
|
|
132
|
+
period_start=period_start,
|
|
133
|
+
period_stop=period_stop,
|
|
134
|
+
nsamples=nsamples,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _length_scale_low(
|
|
139
|
+
V: npt.NDArray[np.float64], height: npt.NDArray[np.float64], opts: Options
|
|
140
|
+
) -> npt.NDArray[np.float64]:
|
|
141
|
+
integration_time = opts.ray_accumulation_time
|
|
142
|
+
from_beam = 2 * height * np.sin(opts.beam_divergence / 2)
|
|
143
|
+
from_wind = V * integration_time
|
|
144
|
+
return np.array(from_wind + from_beam[np.newaxis, :], dtype=np.float64)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _preprocess_horiontal_wind(
|
|
148
|
+
vert: VerticalWind, hori: HorizontalWind, options: Options
|
|
149
|
+
) -> npt.NDArray[np.float64]:
|
|
150
|
+
if np.isnan(hori.V).any():
|
|
151
|
+
raise ValueError("horizontal wind speed cannot contains NaNs")
|
|
152
|
+
trg_points = np.meshgrid(vert.time, vert.height, indexing="ij")
|
|
153
|
+
src_points = (hori.time, hori.height)
|
|
154
|
+
src_vals = hori.V
|
|
155
|
+
|
|
156
|
+
interp_nearest = RegularGridInterpolator(
|
|
157
|
+
src_points,
|
|
158
|
+
src_vals,
|
|
159
|
+
method="nearest",
|
|
160
|
+
bounds_error=False,
|
|
161
|
+
fill_value=None,
|
|
162
|
+
)
|
|
163
|
+
interp_linear = RegularGridInterpolator(
|
|
164
|
+
src_points, src_vals, method="linear", bounds_error=False
|
|
165
|
+
)
|
|
166
|
+
V_nearest = interp_nearest(trg_points)
|
|
167
|
+
V_linear = interp_linear(trg_points)
|
|
168
|
+
V = V_linear
|
|
169
|
+
V[np.isnan(V)] = V_nearest[np.isnan(V)]
|
|
170
|
+
if np.isnan(V).any():
|
|
171
|
+
raise ValueError("Unexpected NaNs")
|
|
172
|
+
V_rmean = _rolling_mean_over_time(vert.time, V, options.period)
|
|
173
|
+
return V_rmean
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _rolling_mean_over_time(
|
|
177
|
+
time: npt.NDArray[np.datetime64], arr: npt.NDArray[np.float64], period: float
|
|
178
|
+
) -> npt.NDArray[np.float64]:
|
|
179
|
+
if arr.ndim != 2:
|
|
180
|
+
raise ValueError("number of dims on arr should be 2")
|
|
181
|
+
if time.ndim != 1 or time.shape[0] != arr.shape[0]:
|
|
182
|
+
raise ValueError("time and arr dimensions do not match")
|
|
183
|
+
if time.dtype != np.dtype("datetime64[us]"):
|
|
184
|
+
raise TypeError(f"Invalid time type: {time.dtype}")
|
|
185
|
+
|
|
186
|
+
S = arr.cumsum(axis=0)
|
|
187
|
+
|
|
188
|
+
def rolling_mean(i: int, j: int) -> npt.NDArray[np.float64]:
|
|
189
|
+
return np.array((S[j] - S[i] + arr[i]) / (j - i + 1), dtype=np.float64)
|
|
190
|
+
|
|
191
|
+
half_period = np.timedelta64(int(period * 0.5e6), "us")
|
|
192
|
+
rol_mean = np.full(arr.shape, np.nan, dtype=np.float64)
|
|
193
|
+
|
|
194
|
+
i = 0
|
|
195
|
+
j = 0
|
|
196
|
+
n = len(time)
|
|
197
|
+
for k, t in enumerate(time):
|
|
198
|
+
while i + 1 < n and t - time[i + 1] >= half_period:
|
|
199
|
+
i += 1
|
|
200
|
+
while j + 1 < n and time[j] - t < half_period:
|
|
201
|
+
j += 1
|
|
202
|
+
rol_mean[k] = rolling_mean(i, j)
|
|
203
|
+
return rol_mean
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _compute_dissipation_rate(
|
|
207
|
+
variance: npt.NDArray[np.float64],
|
|
208
|
+
length_scale_lower: npt.NDArray[np.float64],
|
|
209
|
+
length_scale_upper: npt.NDArray[np.float64],
|
|
210
|
+
) -> npt.NDArray[np.float64]:
|
|
211
|
+
"""
|
|
212
|
+
Parameters
|
|
213
|
+
----------
|
|
214
|
+
variance, length_scale_lower, and length_scale_upper
|
|
215
|
+
dimensions: (time,range)
|
|
216
|
+
"""
|
|
217
|
+
kolmogorov_constant = 0.55
|
|
218
|
+
with np.errstate(invalid="ignore"):
|
|
219
|
+
dr = (
|
|
220
|
+
2
|
|
221
|
+
* np.pi
|
|
222
|
+
* (2 / (3 * kolmogorov_constant)) ** (3 / 2)
|
|
223
|
+
* variance ** (3 / 2)
|
|
224
|
+
* (length_scale_upper ** (2 / 3) - length_scale_lower ** (2 / 3))
|
|
225
|
+
** (-3 / 2)
|
|
226
|
+
)
|
|
227
|
+
return np.array(dr, dtype=np.float64)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _next_valid_from_mask(mask: npt.NDArray[np.bool_]) -> npt.NDArray[np.int64]:
|
|
231
|
+
"""
|
|
232
|
+
mask[t,v] (time,value)
|
|
233
|
+
|
|
234
|
+
returns N[t,v] = i where i = min { j | j >= t and mask[j,v] == False}
|
|
235
|
+
if the set is non empty and N[t,v] = len(mask) otherwise
|
|
236
|
+
"""
|
|
237
|
+
n = len(mask)
|
|
238
|
+
N = np.full(mask.shape, n)
|
|
239
|
+
if mask.size == 0:
|
|
240
|
+
return N
|
|
241
|
+
N[-1][~mask[-1]] = n - 1
|
|
242
|
+
|
|
243
|
+
for t in reversed(range(n - 1)):
|
|
244
|
+
N[t][~mask[t]] = t
|
|
245
|
+
N[t][mask[t]] = N[t + 1][mask[t]]
|
|
246
|
+
return np.array(N, dtype=np.int64)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _prev_valid_from_mask(mask: npt.NDArray[np.bool_]) -> npt.NDArray[np.int64]:
|
|
250
|
+
"""
|
|
251
|
+
mask[t,v] (time,value)
|
|
252
|
+
|
|
253
|
+
returns N[t,v] = i where i = max { j | j <= t and mask[j,v] == False}
|
|
254
|
+
if the set is non empty and N[t,v] = -1 otherwise
|
|
255
|
+
"""
|
|
256
|
+
n = len(mask)
|
|
257
|
+
N = np.full(mask.shape, -1)
|
|
258
|
+
if mask.size == 0:
|
|
259
|
+
return N
|
|
260
|
+
N[0][~mask[0]] = 0
|
|
261
|
+
for t in range(1, n):
|
|
262
|
+
N[t][~mask[t]] = t
|
|
263
|
+
N[t][mask[t]] = N[t - 1][mask[t]]
|
|
264
|
+
return np.array(N, dtype=np.int64)
|
doppy/product/utils.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from collections import Counter
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import numpy.typing as npt
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def arr_to_rounded_set(arr: npt.NDArray[np.float64]) -> set[int]:
|
|
8
|
+
return set(int(x) for x in np.round(arr))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def count_rounded(arr: npt.NDArray[np.float64]) -> Counter[int]:
|
|
12
|
+
return Counter(int(x) for x in np.round(arr))
|