doppy 0.0.1__cp310-abi3-manylinux_2_17_x86_64.manylinux2014_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/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
doppy/raw/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .halo_bg import HaloBg
2
+ from .halo_hpl import HaloHpl
3
+ from .halo_sys_params import HaloSysParams
4
+
5
+ __all__ = ["HaloHpl", "HaloBg", "HaloSysParams"]