aetherscan 1.0.0__py3-none-any.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.
- aetherscan/__init__.py +45 -0
- aetherscan/benchmark.py +179 -0
- aetherscan/candidate_figures.py +328 -0
- aetherscan/cli.py +3032 -0
- aetherscan/config.py +1002 -0
- aetherscan/dashboard.py +709 -0
- aetherscan/dashboard_cli.py +51 -0
- aetherscan/dashboard_launcher.py +194 -0
- aetherscan/data_generation.py +1448 -0
- aetherscan/db/__init__.py +21 -0
- aetherscan/db/db.py +2789 -0
- aetherscan/hf_hub.py +547 -0
- aetherscan/inference.py +912 -0
- aetherscan/inference_viz.py +1494 -0
- aetherscan/latent_gif.py +512 -0
- aetherscan/latent_variants.py +352 -0
- aetherscan/logger/__init__.py +21 -0
- aetherscan/logger/logger.py +467 -0
- aetherscan/logger/slack_handler.py +760 -0
- aetherscan/main.py +1269 -0
- aetherscan/manager/__init__.py +21 -0
- aetherscan/manager/manager.py +781 -0
- aetherscan/models/__init__.py +21 -0
- aetherscan/models/random_forest.py +171 -0
- aetherscan/models/vae.py +849 -0
- aetherscan/monitor/__init__.py +19 -0
- aetherscan/monitor/monitor.py +935 -0
- aetherscan/pfb.py +161 -0
- aetherscan/preprocessing.py +2718 -0
- aetherscan/rf_metrics.py +100 -0
- aetherscan/round_data.py +932 -0
- aetherscan/run_state.py +277 -0
- aetherscan/seeding.py +160 -0
- aetherscan/shap_parallel.py +238 -0
- aetherscan/tag_guards.py +206 -0
- aetherscan/train.py +7681 -0
- aetherscan-1.0.0.dist-info/METADATA +1187 -0
- aetherscan-1.0.0.dist-info/RECORD +41 -0
- aetherscan-1.0.0.dist-info/WHEEL +4 -0
- aetherscan-1.0.0.dist-info/entry_points.txt +2 -0
- aetherscan-1.0.0.dist-info/licenses/LICENSE +13 -0
aetherscan/pfb.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Polyphase filterbank (PFB) static passband equalization for Aetherscan's energy detection.
|
|
3
|
+
|
|
4
|
+
Radio-telescope backends (e.g. the GBT/Breakthrough Listen digital backend) channelize the
|
|
5
|
+
band with a polyphase filterbank whose prototype filter imprints the same scalloped passband
|
|
6
|
+
shape onto every coarse channel. Because that shape is a static property of the instrument's
|
|
7
|
+
filter design — not of the data — it can be computed once from the filter parameters and
|
|
8
|
+
divided out, instead of fitting a spline to every coarse channel of every file (the historical
|
|
9
|
+
default, and a real per-channel cost inside energy detection).
|
|
10
|
+
|
|
11
|
+
This is a native NumPy port of the response-generation logic in bliss
|
|
12
|
+
(github.com/n-west/bliss, bliss/preprocess/passband_static_equalize.cpp); it introduces no
|
|
13
|
+
bliss (or TensorFlow) dependency. One intentional deviation: the C++ approximates sinc near
|
|
14
|
+
zero with a truncated cosine product (cos(x/2)*cos(x/4)*cos(x/8) for |x| < 0.01) purely as a
|
|
15
|
+
numerical shortcut, while np.sinc evaluates the same normalized sinc exactly — the unit tests
|
|
16
|
+
pin the two filter designs against each other.
|
|
17
|
+
|
|
18
|
+
A separate, unrelated quirk lives in the instrument itself: the CASPER GBT512 configuration
|
|
19
|
+
(PFBPassband.jl) carries bug=true — the hardware's coefficient generation omits the
|
|
20
|
+
half-sample offset from the sinc argument (sinc evaluated at (n - N/2) / nchan instead of the
|
|
21
|
+
bug-free (n + 0.5 - N/2) / nchan, N = taps * nchan), mis-centering the sinc by half a sample
|
|
22
|
+
relative to the Hamming window. firdes below is exactly the bug-free design; the two folded
|
|
23
|
+
power responses agree to ~1e-5 (relative) at a typical GBT geometry (64 coarse channels,
|
|
24
|
+
12 taps — bounded < 1e-4 by the unit tests, shrinking as N grows), orders of magnitude below
|
|
25
|
+
the 5% mismatch-warning tolerance, so the exact bug-free form is kept (issue #180).
|
|
26
|
+
|
|
27
|
+
The response depends on (fine_per_coarse, num_coarse_channels, taps_per_channel) only, so
|
|
28
|
+
gen_coarse_channel_response is lru_cached: each process pays the one-time FFT (seconds at
|
|
29
|
+
full GBT resolution), after which equalizing a channel is a single vectorized divide.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import functools
|
|
35
|
+
import logging
|
|
36
|
+
|
|
37
|
+
import numpy as np
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
# Fraction of a coarse channel treated as the "edge" band (each side) and the "mid" band by
|
|
42
|
+
# edge_mid_power_ratio — the validation statistic used to detect a static-response mismatch.
|
|
43
|
+
_RATIO_BAND_FRACTION = 16
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def firdes(num_taps: int, fc: float) -> np.ndarray:
|
|
47
|
+
"""
|
|
48
|
+
Design a windowed-sinc (Hamming) lowpass FIR prototype filter.
|
|
49
|
+
|
|
50
|
+
h[n] = sinc(fc * (n - (num_taps - 1) / 2)) * w[n], where w is the Hamming window
|
|
51
|
+
w[n] = 0.54 - 0.46 * cos(2*pi*n / (num_taps - 1)) (== np.hamming) and np.sinc is the
|
|
52
|
+
normalized sin(pi*t)/(pi*t). fc is the cutoff as a fraction of the sample rate
|
|
53
|
+
(1/num_coarse_channels for a critically-sampled PFB). Returns float64 of shape (num_taps,).
|
|
54
|
+
"""
|
|
55
|
+
n = np.arange(num_taps, dtype=np.float64)
|
|
56
|
+
return np.sinc(fc * (n - (num_taps - 1) / 2.0)) * np.hamming(num_taps)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# maxsize=4 bounds memory if a long-lived process sees several parameter combinations
|
|
60
|
+
# (e.g. a test session); a production run only ever uses one.
|
|
61
|
+
@functools.lru_cache(maxsize=4)
|
|
62
|
+
def gen_coarse_channel_response(
|
|
63
|
+
fine_per_coarse: int, num_coarse_channels: int, taps_per_channel: int
|
|
64
|
+
) -> np.ndarray:
|
|
65
|
+
"""
|
|
66
|
+
Compute the static per-coarse-channel passband response H of a critically-sampled PFB.
|
|
67
|
+
|
|
68
|
+
Steps (mirroring the bliss reference): design the prototype lowpass
|
|
69
|
+
(taps_per_channel * num_coarse_channels taps, cutoff 1/num_coarse_channels); zero-pad to
|
|
70
|
+
num_coarse_channels * fine_per_coarse points; take |fftshift(fft(.))|^2; drop half a
|
|
71
|
+
coarse channel from each end so the remaining band is an integer number of coarse-channel
|
|
72
|
+
spans centered on channel boundaries; fold those (num_coarse_channels - 1) spans on top of
|
|
73
|
+
each other (summing in adjacent-channel leakage); normalize to a peak of 1.0.
|
|
74
|
+
|
|
75
|
+
Returns H of shape (fine_per_coarse,), float64, read-only (the array is cached and shared
|
|
76
|
+
across callers — copy before mutating). Cached per argument tuple, so each process pays
|
|
77
|
+
the FFT cost once per distinct (file resolution, coarse count, taps) combination.
|
|
78
|
+
"""
|
|
79
|
+
if fine_per_coarse < 2:
|
|
80
|
+
raise ValueError(f"fine_per_coarse must be >= 2, got {fine_per_coarse}")
|
|
81
|
+
if fine_per_coarse % 2 != 0:
|
|
82
|
+
# Half a coarse channel (fine_per_coarse // 2) is sliced off each end below, so the
|
|
83
|
+
# remaining band is an exact multiple of fine_per_coarse only when it is even.
|
|
84
|
+
raise ValueError(f"fine_per_coarse must be even, got {fine_per_coarse}")
|
|
85
|
+
if num_coarse_channels < 2:
|
|
86
|
+
# The fold needs at least one full coarse-channel span after edge-slicing.
|
|
87
|
+
raise ValueError(f"num_coarse_channels must be >= 2, got {num_coarse_channels}")
|
|
88
|
+
if taps_per_channel < 1:
|
|
89
|
+
raise ValueError(f"taps_per_channel must be >= 1, got {taps_per_channel}")
|
|
90
|
+
|
|
91
|
+
h = firdes(taps_per_channel * num_coarse_channels, 1.0 / num_coarse_channels)
|
|
92
|
+
full_len = num_coarse_channels * fine_per_coarse
|
|
93
|
+
padded = np.zeros(full_len, dtype=np.float64)
|
|
94
|
+
padded[: h.size] = h
|
|
95
|
+
spectrum = np.abs(np.fft.fftshift(np.fft.fft(padded))) ** 2
|
|
96
|
+
|
|
97
|
+
half_fine = fine_per_coarse // 2
|
|
98
|
+
sliced = spectrum[half_fine : full_len - half_fine]
|
|
99
|
+
response = sliced.reshape(num_coarse_channels - 1, fine_per_coarse).sum(axis=0)
|
|
100
|
+
response /= response.max()
|
|
101
|
+
response.setflags(write=False)
|
|
102
|
+
|
|
103
|
+
logger.debug(
|
|
104
|
+
f"Generated PFB coarse-channel response: fine_per_coarse={fine_per_coarse}, "
|
|
105
|
+
f"num_coarse_channels={num_coarse_channels}, taps_per_channel={taps_per_channel}"
|
|
106
|
+
)
|
|
107
|
+
return response
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def equalize_passband(channel: np.ndarray, response: np.ndarray) -> np.ndarray:
|
|
111
|
+
"""
|
|
112
|
+
Divide one coarse channel by the static passband response, broadcasting over time.
|
|
113
|
+
|
|
114
|
+
channel has shape (time_bins, fine_per_coarse) and response (fine_per_coarse,) — e.g. the
|
|
115
|
+
output of gen_coarse_channel_response. Returns the equalized channel (float64 when
|
|
116
|
+
response is float64); the inputs are not modified.
|
|
117
|
+
"""
|
|
118
|
+
if channel.shape[-1] != response.shape[0]:
|
|
119
|
+
raise ValueError(
|
|
120
|
+
f"channel width {channel.shape[-1]} does not match response length {response.shape[0]}"
|
|
121
|
+
)
|
|
122
|
+
# Defensive floor: the 12-tap GBT design bottoms out around 0.25-0.5 at channel edges,
|
|
123
|
+
# but a very high taps_per_channel (sharper rolloff) could push edge bins toward zero
|
|
124
|
+
# and turn the divide into inf. The floor sits far below any physical response value,
|
|
125
|
+
# so realistic responses pass through untouched.
|
|
126
|
+
return channel / np.maximum(response, 1e-10)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def edge_mid_band_slices(n: int) -> tuple[slice, slice, slice]:
|
|
130
|
+
"""
|
|
131
|
+
Return the (left edge, mid, right edge) bin slices edge_mid_power_ratio evaluates over a
|
|
132
|
+
coarse channel of n bins: the outermost n // 16 bins on each side and a central band of
|
|
133
|
+
the same width (_RATIO_BAND_FRACTION). Exposed so callers that only need the ratio can
|
|
134
|
+
read just these bands from disk (see preprocessing._flattened_edge_mid_ratio) while
|
|
135
|
+
staying bin-exact with edge_mid_power_ratio.
|
|
136
|
+
"""
|
|
137
|
+
band = max(1, n // _RATIO_BAND_FRACTION)
|
|
138
|
+
left = slice(0, band)
|
|
139
|
+
mid = slice(n // 2 - band // 2, n // 2 + band // 2 + 1)
|
|
140
|
+
right = slice(n - band, n)
|
|
141
|
+
return left, mid, right
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def edge_mid_power_ratio(spectrum: np.ndarray) -> float:
|
|
145
|
+
"""
|
|
146
|
+
Mean power in the coarse-channel edge bands relative to the mid band.
|
|
147
|
+
|
|
148
|
+
spectrum is a 1-D per-bin power array over one coarse channel (either the theoretical
|
|
149
|
+
response H or a time-integrated data channel). Both the edge band (outermost n // 16 bins
|
|
150
|
+
on each side) and the mid band (a central band of the same n // 16 width) use
|
|
151
|
+
_RATIO_BAND_FRACTION (see edge_mid_band_slices). Applied to a channel flattened by the
|
|
152
|
+
static response (data / H), a ratio near 1.0 is the cheap residual-flatness sanity check
|
|
153
|
+
(after the bliss `validate` flag) for whether the configured response actually matches
|
|
154
|
+
the recording.
|
|
155
|
+
"""
|
|
156
|
+
left, mid, right = edge_mid_band_slices(spectrum.shape[0])
|
|
157
|
+
edge = 0.5 * (float(spectrum[left].mean()) + float(spectrum[right].mean()))
|
|
158
|
+
mid_mean = float(spectrum[mid].mean())
|
|
159
|
+
# Defensive floor (mirroring equalize_passband's): an all-zero — e.g. fully masked —
|
|
160
|
+
# channel would otherwise raise ZeroDivisionError on the Python-float divide.
|
|
161
|
+
return edge / max(mid_mean, 1e-30)
|