doppy 0.0.1__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.
Potentially problematic release.
This version of doppy might be problematic. Click here for more details.
- doppy/__init__.py +6 -0
- doppy/__main__.py +25 -0
- doppy/bench.py +12 -0
- doppy/data/__init__.py +0 -0
- doppy/data/api.py +44 -0
- doppy/data/cache.py +34 -0
- doppy/data/exceptions.py +6 -0
- doppy/defaults.py +3 -0
- doppy/exceptions.py +10 -0
- doppy/netcdf.py +113 -0
- doppy/options.py +13 -0
- doppy/product/__init__.py +3 -0
- doppy/product/stare.py +579 -0
- doppy/raw/__init__.py +5 -0
- doppy/raw/halo_bg.py +142 -0
- doppy/raw/halo_hpl.py +507 -0
- doppy/raw/halo_sys_params.py +104 -0
- doppy/rs.abi3.so +0 -0
- doppy-0.0.1.dist-info/METADATA +42 -0
- doppy-0.0.1.dist-info/RECORD +24 -0
- doppy-0.0.1.dist-info/WHEEL +4 -0
- doppy-0.0.1.dist-info/entry_points.txt +2 -0
- doppy-0.0.1.dist-info/license_files/LICENSE +21 -0
doppy/product/stare.py
ADDED
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from io import BufferedIOBase
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Sequence, Tuple
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import numpy.typing as npt
|
|
11
|
+
import scipy
|
|
12
|
+
from scipy.ndimage import uniform_filter
|
|
13
|
+
from sklearn.cluster import KMeans
|
|
14
|
+
|
|
15
|
+
import doppy
|
|
16
|
+
from doppy import defaults, options
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class Stare:
|
|
21
|
+
time: npt.NDArray[np.datetime64]
|
|
22
|
+
radial_distance: npt.NDArray[np.float64]
|
|
23
|
+
elevation: npt.NDArray[np.float64]
|
|
24
|
+
beta: npt.NDArray[np.float64]
|
|
25
|
+
radial_velocity: npt.NDArray[np.float64]
|
|
26
|
+
mask: npt.NDArray[np.bool_]
|
|
27
|
+
wavelength: float
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_halo_data(
|
|
31
|
+
cls,
|
|
32
|
+
data: Sequence[str]
|
|
33
|
+
| Sequence[Path]
|
|
34
|
+
| Sequence[bytes]
|
|
35
|
+
| Sequence[BufferedIOBase],
|
|
36
|
+
data_bg: Sequence[str]
|
|
37
|
+
| Sequence[Path]
|
|
38
|
+
| Sequence[tuple[bytes, str]]
|
|
39
|
+
| Sequence[tuple[BufferedIOBase, str]],
|
|
40
|
+
bg_correction_method: options.BgCorrectionMethod,
|
|
41
|
+
) -> Stare:
|
|
42
|
+
raws = doppy.raw.HaloHpl.from_srcs(data)
|
|
43
|
+
|
|
44
|
+
if len(raws) == 0:
|
|
45
|
+
raise doppy.exceptions.NoDataError("HaloHpl data missing")
|
|
46
|
+
|
|
47
|
+
raw = (
|
|
48
|
+
doppy.raw.HaloHpl.merge(_select_raws_for_stare(raws))
|
|
49
|
+
.sorted_by_time()
|
|
50
|
+
.non_strictly_increasing_timesteps_removed()
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
bgs = doppy.raw.HaloBg.from_srcs(data_bg)
|
|
54
|
+
bgs = [bg[:, : raw.header.ngates] for bg in bgs]
|
|
55
|
+
bgs_stare = [bg for bg in bgs if bg.ngates == raw.header.ngates]
|
|
56
|
+
|
|
57
|
+
if len(bgs_stare) == 0:
|
|
58
|
+
raise doppy.exceptions.NoDataError("Background data missing")
|
|
59
|
+
|
|
60
|
+
bg = (
|
|
61
|
+
doppy.raw.HaloBg.merge(bgs_stare)
|
|
62
|
+
.sorted_by_time()
|
|
63
|
+
.non_strictly_increasing_timesteps_removed()
|
|
64
|
+
)
|
|
65
|
+
raw, intensity_bg_corrected = _correct_background(raw, bg, bg_correction_method)
|
|
66
|
+
intensity_noise_bias_corrected = _correct_intensity_noise_bias(
|
|
67
|
+
raw, intensity_bg_corrected
|
|
68
|
+
)
|
|
69
|
+
wavelength = defaults.Halo.wavelength
|
|
70
|
+
beta = _compute_beta(
|
|
71
|
+
intensity_noise_bias_corrected,
|
|
72
|
+
raw.radial_distance,
|
|
73
|
+
raw.header.focus_range,
|
|
74
|
+
wavelength,
|
|
75
|
+
)
|
|
76
|
+
mask = _compute_noise_mask(
|
|
77
|
+
intensity_noise_bias_corrected, raw.radial_velocity, raw.radial_distance
|
|
78
|
+
)
|
|
79
|
+
return Stare(
|
|
80
|
+
time=raw.time,
|
|
81
|
+
radial_distance=raw.radial_distance,
|
|
82
|
+
elevation=raw.elevation,
|
|
83
|
+
beta=beta,
|
|
84
|
+
radial_velocity=raw.radial_velocity,
|
|
85
|
+
mask=mask,
|
|
86
|
+
wavelength=wavelength,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _compute_noise_mask(
|
|
91
|
+
intensity: npt.NDArray[np.float64],
|
|
92
|
+
radial_velocity: npt.NDArray[np.float64],
|
|
93
|
+
radial_distance: npt.NDArray[np.float64],
|
|
94
|
+
) -> npt.NDArray[np.bool_]:
|
|
95
|
+
intensity_mean_mask = uniform_filter(intensity, size=(21, 3)) < 1.0025
|
|
96
|
+
velocity_abs_mean_mask = uniform_filter(np.abs(radial_velocity), size=(21, 3)) > 2
|
|
97
|
+
THREE_PULSES_LENGTH = 90
|
|
98
|
+
near_instrument_noise_mask = np.zeros_like(intensity, dtype=np.bool_)
|
|
99
|
+
near_instrument_noise_mask[:, radial_distance < THREE_PULSES_LENGTH] = True
|
|
100
|
+
low_intensity_mask = intensity < 1
|
|
101
|
+
return np.array(
|
|
102
|
+
(intensity_mean_mask & velocity_abs_mean_mask)
|
|
103
|
+
| near_instrument_noise_mask
|
|
104
|
+
| low_intensity_mask,
|
|
105
|
+
dtype=np.bool_,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _compute_beta(
|
|
110
|
+
intensity: npt.NDArray[np.float64],
|
|
111
|
+
radial_distance: npt.NDArray[np.float64],
|
|
112
|
+
focus: float,
|
|
113
|
+
wavelength: float,
|
|
114
|
+
) -> npt.NDArray[np.float64]:
|
|
115
|
+
"""
|
|
116
|
+
Parameters
|
|
117
|
+
----------
|
|
118
|
+
radial_distance
|
|
119
|
+
distance from the instrument
|
|
120
|
+
focus
|
|
121
|
+
focal length of the telescope for the transmitter and receiver
|
|
122
|
+
wavelength
|
|
123
|
+
laser wavelength
|
|
124
|
+
|
|
125
|
+
Local variables
|
|
126
|
+
---------------
|
|
127
|
+
eta
|
|
128
|
+
detector quantum efficiency
|
|
129
|
+
E
|
|
130
|
+
beam energy
|
|
131
|
+
nu
|
|
132
|
+
optical frequency
|
|
133
|
+
h
|
|
134
|
+
planc's constant
|
|
135
|
+
c
|
|
136
|
+
speed of light
|
|
137
|
+
B
|
|
138
|
+
reveiver bandwidth
|
|
139
|
+
|
|
140
|
+
References
|
|
141
|
+
----------
|
|
142
|
+
Methodology for deriving the telescope focus function and
|
|
143
|
+
its uncertainty for a heterodyne pulsed Doppler lidar
|
|
144
|
+
authors: Pyry Pentikäinen, Ewan James O'Connor,
|
|
145
|
+
Antti Juhani Manninen, and Pablo Ortiz-Amezcua
|
|
146
|
+
doi: https://doi.org/10.5194/amt-13-2849-2020
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
snr = intensity - 1
|
|
150
|
+
h = scipy.constants.Planck
|
|
151
|
+
c = scipy.constants.speed_of_light
|
|
152
|
+
eta = 1
|
|
153
|
+
E = 1e-5
|
|
154
|
+
B = 5e7
|
|
155
|
+
nu = c / wavelength
|
|
156
|
+
A_e = _compute_effective_receiver_energy(radial_distance, focus, wavelength)
|
|
157
|
+
beta = 2 * h * nu * B * radial_distance**2 * snr / (eta * c * E * A_e)
|
|
158
|
+
return np.array(beta, dtype=np.float64)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _compute_effective_receiver_energy(
|
|
162
|
+
radial_distance: npt.NDArray[np.float64],
|
|
163
|
+
focus: float,
|
|
164
|
+
wavelength: float,
|
|
165
|
+
) -> npt.NDArray[np.float64]:
|
|
166
|
+
"""
|
|
167
|
+
NOTE
|
|
168
|
+
----
|
|
169
|
+
Using uncalibrated values from https://doi.org/10.5194/amt-13-2849-2020
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
radial_distance
|
|
175
|
+
distance from the instrument
|
|
176
|
+
focus
|
|
177
|
+
effective focal length of the telescope for the transmitter and receiver
|
|
178
|
+
wavelength
|
|
179
|
+
laser wavelength
|
|
180
|
+
"""
|
|
181
|
+
D = 25e-3 # effective_diameter_of_gaussian_beam
|
|
182
|
+
return np.array(
|
|
183
|
+
np.pi
|
|
184
|
+
* D**2
|
|
185
|
+
/ (
|
|
186
|
+
4
|
|
187
|
+
* (
|
|
188
|
+
1
|
|
189
|
+
+ (np.pi * D**2 / (4 * wavelength * radial_distance)) ** 2
|
|
190
|
+
* (1 - radial_distance / focus) ** 2
|
|
191
|
+
)
|
|
192
|
+
),
|
|
193
|
+
dtype=np.float64,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _correct_intensity_noise_bias(
|
|
198
|
+
raw: doppy.raw.HaloHpl, intensity: npt.NDArray[np.float64]
|
|
199
|
+
) -> npt.NDArray[np.float64]:
|
|
200
|
+
"""
|
|
201
|
+
Parameters
|
|
202
|
+
----------
|
|
203
|
+
intensity:
|
|
204
|
+
intensity after background correction
|
|
205
|
+
"""
|
|
206
|
+
noise_mask = _locate_noise(intensity)
|
|
207
|
+
# Ignore lower gates
|
|
208
|
+
noise_mask[:, raw.radial_distance <= 90] = False
|
|
209
|
+
|
|
210
|
+
A_ = np.concatenate(
|
|
211
|
+
(
|
|
212
|
+
raw.radial_distance[:, np.newaxis],
|
|
213
|
+
np.ones((len(raw.radial_distance), 1)),
|
|
214
|
+
),
|
|
215
|
+
axis=1,
|
|
216
|
+
)[np.newaxis, :, :]
|
|
217
|
+
A = np.tile(
|
|
218
|
+
A_,
|
|
219
|
+
(len(intensity), 1, 1),
|
|
220
|
+
)
|
|
221
|
+
A_noise = np.tile(noise_mask[:, :, np.newaxis], (1, 1, 2))
|
|
222
|
+
A[~A_noise] = 0
|
|
223
|
+
intensity_ = intensity.copy()
|
|
224
|
+
intensity_[~noise_mask] = 0
|
|
225
|
+
|
|
226
|
+
A_pinv = np.linalg.pinv(A)
|
|
227
|
+
x = A_pinv @ intensity_[:, :, np.newaxis]
|
|
228
|
+
noise_fit = (A_ @ x).squeeze(axis=2)
|
|
229
|
+
return np.array(intensity / noise_fit, dtype=np.float64)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _locate_noise(intensity: npt.NDArray[np.float64]) -> npt.NDArray[np.bool_]:
|
|
233
|
+
"""
|
|
234
|
+
Returns
|
|
235
|
+
-------
|
|
236
|
+
boolean array M
|
|
237
|
+
where M[i,j] = True if intensity[i,j] contains only noise
|
|
238
|
+
and False otherwise
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
INTENSITY_THRESHOLD = 1.008
|
|
242
|
+
MEDIAN_KERNEL_THRESHOLD = 1.002
|
|
243
|
+
GAUSSIAN_THRESHOLD = 0.02
|
|
244
|
+
|
|
245
|
+
intensity_normalised = intensity / np.median(intensity, axis=1)[:, np.newaxis]
|
|
246
|
+
intensity_mask = intensity_normalised > INTENSITY_THRESHOLD
|
|
247
|
+
|
|
248
|
+
median_mask = (
|
|
249
|
+
scipy.signal.medfilt2d(intensity_normalised, kernel_size=5)
|
|
250
|
+
> MEDIAN_KERNEL_THRESHOLD
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
gaussian = scipy.ndimage.gaussian_filter(
|
|
254
|
+
(intensity_mask | median_mask).astype(np.float64), sigma=8, radius=16
|
|
255
|
+
)
|
|
256
|
+
gaussian_mask = gaussian > GAUSSIAN_THRESHOLD
|
|
257
|
+
|
|
258
|
+
return np.array(~(intensity_mask | median_mask | gaussian_mask), dtype=np.bool_)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _correct_background(
|
|
262
|
+
raw: doppy.raw.HaloHpl,
|
|
263
|
+
bg: doppy.raw.HaloBg,
|
|
264
|
+
method: options.BgCorrectionMethod,
|
|
265
|
+
) -> Tuple[doppy.raw.HaloHpl, npt.NDArray[np.float64]]:
|
|
266
|
+
"""
|
|
267
|
+
Returns
|
|
268
|
+
-------
|
|
269
|
+
raw_with_bg:
|
|
270
|
+
Same as input raw: HaloHpl, but the profiles that does not corresponding
|
|
271
|
+
background measurement have been removed.
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
intensity_bg_corrected:
|
|
275
|
+
intensity = SNR + 1 = (A_0 * P_0(z)) / (A_bg * P_bg(z)), z = radial_distance
|
|
276
|
+
The measured background signal P_bg contains usually lots of noise that shows as
|
|
277
|
+
vertical stripes in intensity plots. In bg corrected intensity, P_bg is replaced
|
|
278
|
+
with corrected background profile that should represent the noise floor
|
|
279
|
+
more accurately
|
|
280
|
+
"""
|
|
281
|
+
bg_relevant = _select_relevant_background_profiles(bg, raw.time)
|
|
282
|
+
match method:
|
|
283
|
+
case options.BgCorrectionMethod.FIT:
|
|
284
|
+
bg_signal_corrected = _correct_background_by_fitting(
|
|
285
|
+
bg_relevant, raw.radial_distance, fit_method=None
|
|
286
|
+
)
|
|
287
|
+
case options.BgCorrectionMethod.MEAN:
|
|
288
|
+
raise NotImplementedError
|
|
289
|
+
case options.BgCorrectionMethod.PRE_COMPUTED:
|
|
290
|
+
raise NotImplementedError
|
|
291
|
+
|
|
292
|
+
raw2bg = np.searchsorted(bg_relevant.time, raw.time, side="right") - 1
|
|
293
|
+
raw_with_bg = raw[raw2bg >= 0]
|
|
294
|
+
raw2bg = raw2bg[raw2bg >= 0]
|
|
295
|
+
raw_bg_original = bg_relevant.signal[raw2bg]
|
|
296
|
+
raw_bg_corrected = bg_signal_corrected[raw2bg]
|
|
297
|
+
|
|
298
|
+
intensity_bg_corrected = raw_with_bg.intensity * raw_bg_original / raw_bg_corrected
|
|
299
|
+
return raw_with_bg, intensity_bg_corrected
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _correct_background_by_fitting(
|
|
303
|
+
bg: doppy.raw.HaloBg,
|
|
304
|
+
radial_distance: npt.NDArray[np.float64],
|
|
305
|
+
fit_method: options.BgFitMethod | None,
|
|
306
|
+
) -> npt.NDArray[np.float64]:
|
|
307
|
+
clusters = _cluster_background_profiles(bg.signal, radial_distance)
|
|
308
|
+
signal_correcred = np.zeros_like(bg.signal)
|
|
309
|
+
for cluster in set(clusters):
|
|
310
|
+
signal_correcred[clusters == cluster] = _fit_background(
|
|
311
|
+
bg[clusters == cluster], radial_distance, fit_method
|
|
312
|
+
)
|
|
313
|
+
return signal_correcred
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _fit_background(
|
|
317
|
+
bg: doppy.raw.HaloBg,
|
|
318
|
+
radial_distance: npt.NDArray[np.float64],
|
|
319
|
+
fit_method: options.BgFitMethod | None,
|
|
320
|
+
) -> npt.NDArray[np.float64]:
|
|
321
|
+
if fit_method is None:
|
|
322
|
+
fit_method = _infer_fit_type(bg.signal, radial_distance)
|
|
323
|
+
match fit_method:
|
|
324
|
+
case options.BgFitMethod.LIN:
|
|
325
|
+
return _linear_fit(bg.signal, radial_distance)
|
|
326
|
+
case options.BgFitMethod.EXP:
|
|
327
|
+
return _exponential_fit(bg.signal, radial_distance)
|
|
328
|
+
case options.BgFitMethod.EXPLIN:
|
|
329
|
+
return _exponential_linear_fit(bg.signal, radial_distance)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _lin_func(
|
|
333
|
+
x: npt.NDArray[np.float64], radial_distance: npt.NDArray[np.float64]
|
|
334
|
+
) -> npt.NDArray[np.float64]:
|
|
335
|
+
return np.array(x[0] * radial_distance + x[1], dtype=np.float64)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _exp_func(
|
|
339
|
+
x: npt.NDArray[np.float64], radial_distance: npt.NDArray[np.float64]
|
|
340
|
+
) -> npt.NDArray[np.float64]:
|
|
341
|
+
return np.array(x[0] * np.exp(x[1] * radial_distance ** x[2]), dtype=np.float64)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _explin_func(
|
|
345
|
+
x: npt.NDArray[np.float64], radial_distance: npt.NDArray[np.float64]
|
|
346
|
+
) -> npt.NDArray[np.float64]:
|
|
347
|
+
return np.array(
|
|
348
|
+
_exp_func(x[:3], radial_distance) + _lin_func(x[3:], radial_distance),
|
|
349
|
+
dtype=np.float64,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _infer_fit_type(
|
|
354
|
+
bg_signal: npt.NDArray[np.float64], radial_distance: npt.NDArray[np.float64]
|
|
355
|
+
) -> options.BgFitMethod:
|
|
356
|
+
peaks = _detect_peaks(bg_signal, radial_distance)
|
|
357
|
+
dist_mask = (90 < radial_distance) & (radial_distance < 8000)
|
|
358
|
+
mask = dist_mask & ~peaks
|
|
359
|
+
|
|
360
|
+
scale = np.median(bg_signal, axis=1)[:, np.newaxis]
|
|
361
|
+
|
|
362
|
+
rdist = radial_distance[np.newaxis][:, mask]
|
|
363
|
+
|
|
364
|
+
signal = (bg_signal / scale)[:, mask]
|
|
365
|
+
|
|
366
|
+
def lin_func_rss(x: npt.NDArray[np.float64]) -> np.float64:
|
|
367
|
+
return np.float64(((signal - _lin_func(x, rdist)) ** 2).sum())
|
|
368
|
+
|
|
369
|
+
def exp_func_rss(x: npt.NDArray[np.float64]) -> np.float64:
|
|
370
|
+
return np.float64(((signal - _exp_func(x, rdist)) ** 2).sum())
|
|
371
|
+
|
|
372
|
+
def explin_func_rss(x: npt.NDArray[np.float64]) -> np.float64:
|
|
373
|
+
return np.float64(((signal - _explin_func(x, rdist)) ** 2).sum())
|
|
374
|
+
|
|
375
|
+
method = "Nelder-Mead"
|
|
376
|
+
res_lin = scipy.optimize.minimize(
|
|
377
|
+
lin_func_rss, [1e-5, 1], method=method, options={"maxiter": 2 * 600}
|
|
378
|
+
)
|
|
379
|
+
res_exp = scipy.optimize.minimize(
|
|
380
|
+
exp_func_rss, [1, -1, -1], method=method, options={"maxiter": 3 * 600}
|
|
381
|
+
)
|
|
382
|
+
res_explin = scipy.optimize.minimize(
|
|
383
|
+
explin_func_rss, [1, -1, -1, 0, 0], method=method, options={"maxiter": 5 * 600}
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
fit_lin = _lin_func(res_lin.x, rdist)
|
|
387
|
+
fit_exp = _exp_func(res_exp.x, rdist)
|
|
388
|
+
fit_explin = _explin_func(res_explin.x, rdist)
|
|
389
|
+
|
|
390
|
+
lin_rss = ((signal - fit_lin) ** 2).sum()
|
|
391
|
+
exp_rss = ((signal - fit_exp) ** 2).sum()
|
|
392
|
+
explin_rss = ((signal - fit_explin) ** 2).sum()
|
|
393
|
+
|
|
394
|
+
#
|
|
395
|
+
if exp_rss / lin_rss < 0.95 or explin_rss / lin_rss < 0.95:
|
|
396
|
+
if (exp_rss - explin_rss) / lin_rss > 0.05:
|
|
397
|
+
return options.BgFitMethod.EXPLIN
|
|
398
|
+
else:
|
|
399
|
+
return options.BgFitMethod.EXP
|
|
400
|
+
else:
|
|
401
|
+
return options.BgFitMethod.LIN
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _detect_peaks(
|
|
405
|
+
background_signal: npt.NDArray[np.float64], radial_distance: npt.NDArray[np.float64]
|
|
406
|
+
) -> npt.NDArray[np.bool_]:
|
|
407
|
+
"""
|
|
408
|
+
background_signal: dim = (time,range)
|
|
409
|
+
radial_distance: dim = (range,)
|
|
410
|
+
|
|
411
|
+
Returns a boolean mask, dim = (range, ), where True denotes locations of peaks
|
|
412
|
+
that should be ignored in fitting
|
|
413
|
+
"""
|
|
414
|
+
scale = np.median(background_signal, axis=1)[:, np.newaxis]
|
|
415
|
+
bg = background_signal / scale
|
|
416
|
+
return _set_adjacent_true(
|
|
417
|
+
np.concatenate(
|
|
418
|
+
(
|
|
419
|
+
np.array([False]),
|
|
420
|
+
np.diff(np.diff(bg.mean(axis=0))) < -0.01,
|
|
421
|
+
np.array([False]),
|
|
422
|
+
)
|
|
423
|
+
)
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _set_adjacent_true(arr: npt.NDArray[np.bool_]) -> npt.NDArray[np.bool_]:
|
|
428
|
+
temp = np.pad(arr, (1, 1), mode="constant")
|
|
429
|
+
temp[:-2] |= arr
|
|
430
|
+
temp[2:] |= arr
|
|
431
|
+
return temp[1:-1]
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _linear_fit(
|
|
435
|
+
bg_signal: npt.NDArray[np.float64], radial_distance: npt.NDArray[np.float64]
|
|
436
|
+
) -> npt.NDArray[np.float64]:
|
|
437
|
+
dist_mask = 90 < radial_distance
|
|
438
|
+
peaks = _detect_peaks(bg_signal, radial_distance)
|
|
439
|
+
mask = dist_mask & ~peaks
|
|
440
|
+
|
|
441
|
+
scale = np.median(bg_signal, axis=1)[:, np.newaxis]
|
|
442
|
+
rdist_fit = radial_distance[np.newaxis][:, mask]
|
|
443
|
+
signal_fit = (bg_signal / scale)[:, mask]
|
|
444
|
+
|
|
445
|
+
A = np.tile(
|
|
446
|
+
np.concatenate((rdist_fit, np.ones_like(rdist_fit))).T, (signal_fit.shape[0], 1)
|
|
447
|
+
)
|
|
448
|
+
x = np.linalg.pinv(A) @ signal_fit.reshape(-1, 1)
|
|
449
|
+
fit = (
|
|
450
|
+
np.concatenate(
|
|
451
|
+
(radial_distance[:, np.newaxis], np.ones((radial_distance.shape[0], 1))),
|
|
452
|
+
axis=1,
|
|
453
|
+
)
|
|
454
|
+
@ x
|
|
455
|
+
).T
|
|
456
|
+
return np.array(fit * scale, dtype=np.float64)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _exponential_fit(
|
|
460
|
+
bg_signal: npt.NDArray[np.float64], radial_distance: npt.NDArray[np.float64]
|
|
461
|
+
) -> npt.NDArray[np.float64]:
|
|
462
|
+
dist_mask = 90 < radial_distance
|
|
463
|
+
peaks = _detect_peaks(bg_signal, radial_distance)
|
|
464
|
+
mask = dist_mask & ~peaks
|
|
465
|
+
scale = np.median(bg_signal, axis=1)[:, np.newaxis]
|
|
466
|
+
rdist_fit = radial_distance[np.newaxis][:, mask]
|
|
467
|
+
signal_fit = (bg_signal / scale)[:, mask]
|
|
468
|
+
|
|
469
|
+
def exp_func_rss(x: npt.NDArray[np.float64]) -> np.float64:
|
|
470
|
+
return np.float64(((signal_fit - _exp_func(x, rdist_fit)) ** 2).sum())
|
|
471
|
+
|
|
472
|
+
result = scipy.optimize.minimize(
|
|
473
|
+
exp_func_rss, [1, -1, -1], method="Nelder-Mead", options={"maxiter": 3 * 600}
|
|
474
|
+
)
|
|
475
|
+
fit = _exp_func(result.x, radial_distance)[np.newaxis, :]
|
|
476
|
+
return np.array(fit * scale, dtype=np.float64)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _exponential_linear_fit(
|
|
480
|
+
bg_signal: npt.NDArray[np.float64], radial_distance: npt.NDArray[np.float64]
|
|
481
|
+
) -> npt.NDArray[np.float64]:
|
|
482
|
+
dist_mask = 90 < radial_distance
|
|
483
|
+
peaks = _detect_peaks(bg_signal, radial_distance)
|
|
484
|
+
mask = dist_mask & ~peaks
|
|
485
|
+
scale = np.median(bg_signal, axis=1)[:, np.newaxis]
|
|
486
|
+
rdist_fit = radial_distance[np.newaxis][:, mask]
|
|
487
|
+
signal_fit = (bg_signal / scale)[:, mask]
|
|
488
|
+
|
|
489
|
+
def explin_func_rss(x: npt.NDArray[np.float64]) -> np.float64:
|
|
490
|
+
return np.float64(((signal_fit - _explin_func(x, rdist_fit)) ** 2).sum())
|
|
491
|
+
|
|
492
|
+
result = scipy.optimize.minimize(
|
|
493
|
+
explin_func_rss,
|
|
494
|
+
[1, -1, -1, 0, 0],
|
|
495
|
+
method="Nelder-Mead",
|
|
496
|
+
options={"maxiter": 5 * 600},
|
|
497
|
+
)
|
|
498
|
+
fit = _explin_func(result.x, radial_distance)[np.newaxis, :]
|
|
499
|
+
return np.array(fit * scale, dtype=np.float64)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _select_raws_for_stare(
|
|
503
|
+
raws: Sequence[doppy.raw.HaloHpl],
|
|
504
|
+
) -> Sequence[doppy.raw.HaloHpl]:
|
|
505
|
+
groups: dict[tuple[str, int], int] = defaultdict(int)
|
|
506
|
+
|
|
507
|
+
if len(raws) == 0:
|
|
508
|
+
raise doppy.exceptions.NoDataError("No data to select from")
|
|
509
|
+
|
|
510
|
+
# Select files that stare vertically
|
|
511
|
+
raws_stare = [raw for raw in raws if raw.elevation_angles == {90}]
|
|
512
|
+
|
|
513
|
+
if len(raws_stare) == 0:
|
|
514
|
+
raise doppy.exceptions.NoDataError("No data suitable for stare product")
|
|
515
|
+
|
|
516
|
+
# count the number of profiles each (scan_type,ngates) group has
|
|
517
|
+
for raw in raws_stare:
|
|
518
|
+
groups[(raw.header.scan_type, raw.header.ngates)] += raw.time.shape[0]
|
|
519
|
+
|
|
520
|
+
def key_func(key: tuple[str, int]) -> int:
|
|
521
|
+
return groups[key]
|
|
522
|
+
|
|
523
|
+
# (scan_type,ngates) group with the most profiles
|
|
524
|
+
scan_type, ngates = max(groups, key=key_func)
|
|
525
|
+
|
|
526
|
+
return [
|
|
527
|
+
raw
|
|
528
|
+
for raw in raws_stare
|
|
529
|
+
if raw.header.scan_type == scan_type and raw.header.ngates == ngates
|
|
530
|
+
]
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _time2bg_time(
|
|
534
|
+
time: npt.NDArray[np.datetime64], bg_time: npt.NDArray[np.datetime64]
|
|
535
|
+
) -> npt.NDArray[np.int64]:
|
|
536
|
+
return np.searchsorted(bg_time, time, side="right") - 1
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _select_relevant_background_profiles(
|
|
540
|
+
bg: doppy.raw.HaloBg, time: npt.NDArray[np.datetime64]
|
|
541
|
+
) -> doppy.raw.HaloBg:
|
|
542
|
+
"""
|
|
543
|
+
expects bg.time to be sorted
|
|
544
|
+
"""
|
|
545
|
+
time2bg_time = _time2bg_time(time, bg.time)
|
|
546
|
+
|
|
547
|
+
relevant_indices = list(set(time2bg_time[time2bg_time >= 0]))
|
|
548
|
+
bg_ind = np.arange(bg.time.size)
|
|
549
|
+
is_relevant = np.isin(bg_ind, relevant_indices)
|
|
550
|
+
return bg[is_relevant]
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _cluster_background_profiles(
|
|
554
|
+
background_signal: npt.NDArray[np.float64], radial_distance: npt.NDArray[np.float64]
|
|
555
|
+
) -> npt.NDArray[np.int64]:
|
|
556
|
+
default_labels = np.zeros(len(background_signal), dtype=int)
|
|
557
|
+
radial_distance_mask = (90 < radial_distance) & (radial_distance < 1500)
|
|
558
|
+
|
|
559
|
+
normalised_background_signal = background_signal / np.median(
|
|
560
|
+
background_signal, axis=1, keepdims=True
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
profile_median = np.median(
|
|
564
|
+
normalised_background_signal[:, radial_distance_mask], axis=1
|
|
565
|
+
)
|
|
566
|
+
kmeans = KMeans(n_clusters=2, n_init="auto").fit(profile_median[:, np.newaxis])
|
|
567
|
+
cluster_width = np.array([None, None])
|
|
568
|
+
for label in [0, 1]:
|
|
569
|
+
cluster = profile_median[kmeans.labels_ == label]
|
|
570
|
+
cluster_width[label] = np.max(cluster) - np.min(cluster)
|
|
571
|
+
cluster_distance = np.abs(
|
|
572
|
+
kmeans.cluster_centers_[0, 0] - kmeans.cluster_centers_[1, 0]
|
|
573
|
+
)
|
|
574
|
+
max_cluster_width = np.float64(np.max(cluster_width))
|
|
575
|
+
if np.isclose(max_cluster_width, 0):
|
|
576
|
+
return default_labels
|
|
577
|
+
if cluster_distance / max_cluster_width > 3:
|
|
578
|
+
return np.array(kmeans.labels_, dtype=np.int64)
|
|
579
|
+
return default_labels
|