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
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Models package for Aetherscan pipeline
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from .random_forest import (
|
|
8
|
+
RandomForestModel,
|
|
9
|
+
prepare_latent_features,
|
|
10
|
+
)
|
|
11
|
+
from .vae import (
|
|
12
|
+
Sampling,
|
|
13
|
+
create_beta_vae_model,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"RandomForestModel",
|
|
18
|
+
"Sampling",
|
|
19
|
+
"create_beta_vae_model",
|
|
20
|
+
"prepare_latent_features",
|
|
21
|
+
]
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# TODO: refactor to expose public APIs for creating & destroying RandomForestModel instances
|
|
2
|
+
"""
|
|
3
|
+
Random Forest classifier implementation for Aetherscan Pipeline
|
|
4
|
+
Receives concatenated latents grouped by their original 6-observation cadence pattern
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
import joblib
|
|
12
|
+
import numpy as np
|
|
13
|
+
import numpy.typing as npt
|
|
14
|
+
from sklearn.ensemble import RandomForestClassifier
|
|
15
|
+
from sklearn.utils import shuffle
|
|
16
|
+
|
|
17
|
+
from aetherscan.config import get_config
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def prepare_latent_features(
|
|
23
|
+
latent_vectors: np.ndarray,
|
|
24
|
+
num_observations: int = 6,
|
|
25
|
+
dtype: npt.DTypeLike = np.float64,
|
|
26
|
+
) -> np.ndarray:
|
|
27
|
+
"""
|
|
28
|
+
Reshape per-observation latent vectors of shape (num_cadences * num_observations, latent_dim)
|
|
29
|
+
into per-cadence features of shape (num_cadences, num_observations * latent_dim), so each
|
|
30
|
+
cadence's 6 latents are concatenated into a single feature row for the RF.
|
|
31
|
+
|
|
32
|
+
Caller must keep row i..i+num_observations-1 grouped as cadence i. Raises ValueError if the
|
|
33
|
+
row count isn't divisible by num_observations. `dtype` defaults to float64 (see the note at
|
|
34
|
+
the return) — training callers MUST keep it; inference passes float32.
|
|
35
|
+
"""
|
|
36
|
+
# Expected shape: (num_cadences * num_observations, latent_dim)
|
|
37
|
+
num_latents = latent_vectors.shape[0]
|
|
38
|
+
|
|
39
|
+
if num_latents % num_observations != 0:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
f"Received {num_latents} latent vectors. Not divisible by num_observations ({num_observations})"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
num_cadences = num_latents // num_observations
|
|
45
|
+
latent_dim = latent_vectors.shape[1]
|
|
46
|
+
|
|
47
|
+
# Target shape: (num_cadences, num_observations * latent_dim)
|
|
48
|
+
# Where each element in the latent vector is treated as a feature by the Random Forest
|
|
49
|
+
# We flatten the observations so all 6 latents in a cadence are grouped together.
|
|
50
|
+
# A row-major reshape IS that flatten (row i = rows i*num_obs..(i+1)*num_obs-1
|
|
51
|
+
# raveled) — the per-row Python loop this replaces cost ~0.1-0.5 s per RFI-dense
|
|
52
|
+
# cadence at inference (#301). `dtype` defaults to float64 and MUST stay float64 for
|
|
53
|
+
# the TRAINING callers: the Active-Units gate computes .var() on this output, whose
|
|
54
|
+
# float32 accumulation differs in exactly the low bits that decide a hovering-at-
|
|
55
|
+
# threshold dim (#288's margin lesson). Inference passes dtype=float32 — it never runs
|
|
56
|
+
# that .var() gate (active_dims is loaded from the saved config, not recomputed), and
|
|
57
|
+
# every inference consumer (build_variant_features, sample_z_flat, predict_proba) casts
|
|
58
|
+
# to float32 anyway, so float32 here is byte-identical downstream (float32->float64->
|
|
59
|
+
# float32 round-trips exactly) while skipping two full-matrix float64 widenings per
|
|
60
|
+
# cadence on the GPU-idle RF stage.
|
|
61
|
+
return np.asarray(latent_vectors, dtype=dtype).reshape(
|
|
62
|
+
num_cadences, num_observations * latent_dim
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class RandomForestModel:
|
|
67
|
+
"""Random Forest classifier for SETI signal detection"""
|
|
68
|
+
|
|
69
|
+
def __init__(self):
|
|
70
|
+
self.config = get_config()
|
|
71
|
+
if self.config is None:
|
|
72
|
+
raise ValueError("get_config() returned None")
|
|
73
|
+
|
|
74
|
+
self.model = RandomForestClassifier(
|
|
75
|
+
n_estimators=self.config.rf.n_estimators,
|
|
76
|
+
bootstrap=self.config.rf.bootstrap,
|
|
77
|
+
max_features=self.config.rf.max_features,
|
|
78
|
+
n_jobs=self.config.rf.n_jobs,
|
|
79
|
+
# Derived from the pipeline root seed unless --rf-seed overrides (#279)
|
|
80
|
+
random_state=self.config.resolved_rf_seed(),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
self.is_trained = False
|
|
84
|
+
|
|
85
|
+
def train(self, latent_vectors: np.ndarray, binary_labels: np.ndarray):
|
|
86
|
+
"""
|
|
87
|
+
Fit the Random Forest on `latent_vectors` of shape
|
|
88
|
+
(n_cadences * num_observations, latent_dim) and `binary_labels` of shape (n_cadences,)
|
|
89
|
+
(0 = false, 1 = true signal). latent_vectors must be grouped so row
|
|
90
|
+
i..i+num_observations-1 corresponds to cadence i — this is what
|
|
91
|
+
prepare_latent_features expects.
|
|
92
|
+
"""
|
|
93
|
+
# Prepare features
|
|
94
|
+
features = prepare_latent_features(latent_vectors, self.config.data.num_observations)
|
|
95
|
+
|
|
96
|
+
# Sanity check: make sure length of feature & label arrays are aligned
|
|
97
|
+
if features.shape[0] != binary_labels.shape[0]:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"Feature/label count mismatch: {features.shape[0]} vs {binary_labels.shape[0]}"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Shuffle data
|
|
103
|
+
features, binary_labels = shuffle(
|
|
104
|
+
features, binary_labels, random_state=self.config.resolved_rf_seed()
|
|
105
|
+
)
|
|
106
|
+
logger.info(f"Prepared {features.shape[0]} training samples")
|
|
107
|
+
|
|
108
|
+
# Start training
|
|
109
|
+
logger.info("Training Random Forest classifier...")
|
|
110
|
+
self.model.fit(features, binary_labels)
|
|
111
|
+
self.is_trained = True
|
|
112
|
+
|
|
113
|
+
def predict_proba(self, latent_vectors: np.ndarray) -> np.ndarray:
|
|
114
|
+
"""
|
|
115
|
+
Predict per-class probabilities for each cadence; returns shape (n_cadences, 2) with
|
|
116
|
+
columns [P(class=0), P(class=1)]. latent_vectors follows the same grouped layout as
|
|
117
|
+
train().
|
|
118
|
+
"""
|
|
119
|
+
if not self.is_trained:
|
|
120
|
+
logger.warning("Making predictions with untrained model")
|
|
121
|
+
|
|
122
|
+
features = prepare_latent_features(latent_vectors, self.config.data.num_observations)
|
|
123
|
+
return self.model.predict_proba(features)
|
|
124
|
+
|
|
125
|
+
def predict(self, latent_vectors: np.ndarray, threshold: float = 0.5) -> np.ndarray:
|
|
126
|
+
"""
|
|
127
|
+
Binary class predictions: 1 where P(class=1) > threshold, else 0. Returns shape
|
|
128
|
+
(n_cadences,) int array.
|
|
129
|
+
"""
|
|
130
|
+
probas = self.predict_proba(latent_vectors)
|
|
131
|
+
return (probas[:, 1] > threshold).astype(int)
|
|
132
|
+
|
|
133
|
+
def predict_verbose(
|
|
134
|
+
self, latent_vectors: np.ndarray, threshold: float = 0.5
|
|
135
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
136
|
+
"""
|
|
137
|
+
Like predict(), but also returns per-cadence confidence (the probability of the predicted
|
|
138
|
+
class — so high for confident negatives too, not just confident positives). Returns
|
|
139
|
+
(predictions, confidences), each of shape (n_cadences,).
|
|
140
|
+
"""
|
|
141
|
+
probas = self.predict_proba(latent_vectors)
|
|
142
|
+
predictions = (probas[:, 1] > threshold).astype(int)
|
|
143
|
+
|
|
144
|
+
# Confidence score = the probability of the predicted class
|
|
145
|
+
confidences = np.where(predictions, probas[:, 1], probas[:, 0])
|
|
146
|
+
|
|
147
|
+
return predictions, confidences
|
|
148
|
+
|
|
149
|
+
def save(self, filepath: str):
|
|
150
|
+
"""Save RF model weights"""
|
|
151
|
+
if not self.is_trained:
|
|
152
|
+
logger.warning("Saving untrained model")
|
|
153
|
+
|
|
154
|
+
joblib.dump(self.model, filepath)
|
|
155
|
+
logger.info(f"Saved Random Forest model to {filepath}")
|
|
156
|
+
|
|
157
|
+
def load(self, filepath: str):
|
|
158
|
+
"""Load RF model weights"""
|
|
159
|
+
if self.is_trained:
|
|
160
|
+
logger.warning("Overriding trained model")
|
|
161
|
+
|
|
162
|
+
self.model = joblib.load(filepath)
|
|
163
|
+
# Predict-time parallelism must come from the RUNTIME config, not the training
|
|
164
|
+
# host's pickled value (#301): joblib.load replaces the estimator wholesale, so
|
|
165
|
+
# the constructor's rf.n_jobs was silently dead on every loaded model. n_jobs is
|
|
166
|
+
# a predict-time execution knob on a fitted forest — reassigning it cannot touch
|
|
167
|
+
# the trees. Default (-1) matches the deployed artifacts, so behavior only
|
|
168
|
+
# changes when an operator asks for it.
|
|
169
|
+
self.model.n_jobs = self.config.rf.n_jobs
|
|
170
|
+
self.is_trained = True
|
|
171
|
+
logger.info(f"Loaded Random Forest model from {filepath}")
|