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,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Console entry point (`aetherscan-dashboard`) for manual runs of the live dashboard.
|
|
3
|
+
|
|
4
|
+
main.py auto-launches the dashboard alongside a run (dashboard_launcher.py); this shim covers the
|
|
5
|
+
ad-hoc case — inspecting a saved DB after the fact — replacing the verbose manual incantation from
|
|
6
|
+
dashboard.py's docstring with:
|
|
7
|
+
|
|
8
|
+
aetherscan-dashboard --db-path /path/to/aetherscan.db --tag train_20260101_120000
|
|
9
|
+
|
|
10
|
+
Everything on the command line is forwarded verbatim to dashboard.py's argparse (--db-path, --tag,
|
|
11
|
+
--plots-dir, --refresh). Streamlit must OWN the process (`streamlit run <file>`) for the `st.*`
|
|
12
|
+
calls to render, so this execs `python -m streamlit run` in place — a plain
|
|
13
|
+
`python -m aetherscan.dashboard` would call `st.*` outside a ScriptRunContext and render nothing.
|
|
14
|
+
In a source checkout / the container (no installed entry point), the same shim runs as
|
|
15
|
+
`PYTHONPATH=src python -m aetherscan.dashboard_cli <args>`.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import importlib.util
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
# Same resolution as dashboard_launcher._DASHBOARD_SCRIPT — not imported from there, so this shim
|
|
26
|
+
# stays stdlib-only and the console script works (and fails fast) without the pipeline deps.
|
|
27
|
+
_DASHBOARD_SCRIPT = Path(__file__).resolve().parent / "dashboard.py"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def build_exec_argv(argv: list[str]) -> list[str]:
|
|
31
|
+
"""`python -m streamlit run <packaged dashboard.py> -- <argv>` — build_dashboard_command's
|
|
32
|
+
shape, with the caller's args forwarded verbatim to dashboard.py's argparse. Pure/testable."""
|
|
33
|
+
return [sys.executable, "-m", "streamlit", "run", str(_DASHBOARD_SCRIPT), "--", *argv]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def main(argv: list[str] | None = None) -> None:
|
|
37
|
+
"""Replace this process with the Streamlit server (os.execv never returns on success)."""
|
|
38
|
+
if importlib.util.find_spec("streamlit") is None:
|
|
39
|
+
raise SystemExit(
|
|
40
|
+
"aetherscan-dashboard: streamlit is not installed. "
|
|
41
|
+
"Install the dashboard extra: pip install 'aetherscan[dashboard]'"
|
|
42
|
+
)
|
|
43
|
+
# Parity with dashboard_launcher._DASHBOARD_SCRIPT.is_file(): fail with a clear message
|
|
44
|
+
# instead of a cryptic os.execv OSError / Streamlit error if the packaged script is absent.
|
|
45
|
+
if not _DASHBOARD_SCRIPT.is_file():
|
|
46
|
+
raise SystemExit(f"aetherscan-dashboard: dashboard script not found: {_DASHBOARD_SCRIPT}")
|
|
47
|
+
os.execv(sys.executable, build_exec_argv(sys.argv[1:] if argv is None else argv))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
main()
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auto-launch the live monitoring dashboard (aetherscan/dashboard.py) alongside a train/inference run.
|
|
3
|
+
|
|
4
|
+
main.py calls launch_dashboard() once at startup (after the DB is initialized). It spawns a
|
|
5
|
+
headless Streamlit server as a detached subprocess pointed at this run's DB + tag, logs the
|
|
6
|
+
SSH-forward instructions, and registers atexit + SIGTERM/SIGINT teardown so the server is reaped
|
|
7
|
+
whether the pipeline exits gracefully or is signal-killed. Fully guarded: a missing streamlit, a
|
|
8
|
+
missing dashboard.py, a port already in use, or any spawn failure degrades to a warning — the
|
|
9
|
+
dashboard is optional observability and must never fail the pipeline. Opt out with --no-dashboard
|
|
10
|
+
(config.monitor.dashboard_enabled = False).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import atexit
|
|
16
|
+
import contextlib
|
|
17
|
+
import importlib.util
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
import signal
|
|
21
|
+
import socket
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from aetherscan.config import get_config
|
|
27
|
+
from aetherscan.db import get_db
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
# dashboard.py ships alongside this module inside the package, so it resolves identically for a
|
|
32
|
+
# source checkout, the container, and a pip install (streamlit runs it by file path).
|
|
33
|
+
_DASHBOARD_SCRIPT = Path(__file__).resolve().parent / "dashboard.py"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def build_dashboard_command(
|
|
37
|
+
python_exe: str,
|
|
38
|
+
dashboard_script: str,
|
|
39
|
+
db_path: str,
|
|
40
|
+
tag: str,
|
|
41
|
+
plots_dir: str,
|
|
42
|
+
port: int,
|
|
43
|
+
) -> list[str]:
|
|
44
|
+
"""Construct the `python -m streamlit run ...` argv. Streamlit's own flags precede `--`;
|
|
45
|
+
everything after `--` is forwarded to dashboard.py's argparse. Pure/testable."""
|
|
46
|
+
return [
|
|
47
|
+
python_exe,
|
|
48
|
+
"-m",
|
|
49
|
+
"streamlit",
|
|
50
|
+
"run",
|
|
51
|
+
dashboard_script,
|
|
52
|
+
"--server.port",
|
|
53
|
+
str(port),
|
|
54
|
+
"--server.headless",
|
|
55
|
+
"true",
|
|
56
|
+
"--browser.gatherUsageStats",
|
|
57
|
+
"false",
|
|
58
|
+
"--",
|
|
59
|
+
"--db-path",
|
|
60
|
+
db_path,
|
|
61
|
+
"--tag",
|
|
62
|
+
tag,
|
|
63
|
+
"--plots-dir",
|
|
64
|
+
plots_dir,
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _port_in_use(port: int) -> bool:
|
|
69
|
+
"""True if `port` can't be bound on localhost — i.e. a concurrent run's dashboard already holds
|
|
70
|
+
it. Best-effort probe; either way the caller only ever warns, never fails the pipeline."""
|
|
71
|
+
try:
|
|
72
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
73
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
74
|
+
s.bind(("127.0.0.1", port))
|
|
75
|
+
except OSError:
|
|
76
|
+
return True
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _terminate(proc: subprocess.Popen) -> None:
|
|
81
|
+
"""Best-effort teardown (no logging — may run during interpreter shutdown or in a signal
|
|
82
|
+
handler). The dashboard is spawned with start_new_session=True, so it leads its own process
|
|
83
|
+
group; signal the whole group (killpg) to reap Streamlit AND any grandchildren, not just the
|
|
84
|
+
direct child."""
|
|
85
|
+
if proc.poll() is not None:
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
def _signal_group(sig: int) -> None:
|
|
89
|
+
try:
|
|
90
|
+
os.killpg(os.getpgid(proc.pid), sig)
|
|
91
|
+
except (ProcessLookupError, PermissionError, OSError):
|
|
92
|
+
# group already gone, or getpgid raced the exit — fall back to the direct child
|
|
93
|
+
with contextlib.suppress(Exception):
|
|
94
|
+
proc.send_signal(sig)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
_signal_group(signal.SIGTERM)
|
|
98
|
+
try:
|
|
99
|
+
proc.wait(timeout=5)
|
|
100
|
+
except subprocess.TimeoutExpired:
|
|
101
|
+
_signal_group(signal.SIGKILL)
|
|
102
|
+
except Exception:
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _install_signal_teardown(proc: subprocess.Popen) -> None:
|
|
107
|
+
"""Reap the dashboard on SIGTERM/SIGINT too — atexit hooks do NOT run on a signal-kill, and
|
|
108
|
+
the new-session detachment means a process-group signal to the pipeline never reaches the
|
|
109
|
+
dashboard. Each handler tears down the dashboard, restores the previous disposition, and
|
|
110
|
+
re-raises the signal so the pipeline's own shutdown proceeds exactly as before (no logging in
|
|
111
|
+
the handler — that can deadlock). Signal handlers can only be installed from the main thread."""
|
|
112
|
+
for sig in (signal.SIGTERM, signal.SIGINT):
|
|
113
|
+
try:
|
|
114
|
+
prev = signal.getsignal(sig)
|
|
115
|
+
except (ValueError, OSError):
|
|
116
|
+
continue # not the main thread / unsupported — atexit still covers graceful exits
|
|
117
|
+
|
|
118
|
+
def _handler(signum, frame, _prev=prev):
|
|
119
|
+
_terminate(proc)
|
|
120
|
+
# Restore callables AND SIG_IGN as-is (collapsing SIG_IGN to SIG_DFL would let the
|
|
121
|
+
# re-delivery below kill a process that was ignoring the signal); anything else
|
|
122
|
+
# (SIG_DFL, None from a non-Python handler) falls back to SIG_DFL.
|
|
123
|
+
restore = _prev if callable(_prev) or _prev == signal.SIG_IGN else signal.SIG_DFL
|
|
124
|
+
signal.signal(signum, restore)
|
|
125
|
+
os.kill(os.getpid(), signum) # re-deliver so the pipeline shuts down as it would have
|
|
126
|
+
|
|
127
|
+
with contextlib.suppress(ValueError, OSError):
|
|
128
|
+
signal.signal(sig, _handler)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def launch_dashboard() -> subprocess.Popen | None:
|
|
132
|
+
"""Spawn the headless Streamlit dashboard for this run, or return None (disabled / failed)."""
|
|
133
|
+
config = get_config()
|
|
134
|
+
if config is None or not config.monitor.dashboard_enabled:
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
if not _DASHBOARD_SCRIPT.is_file():
|
|
138
|
+
logger.warning(f"Dashboard not launched: {_DASHBOARD_SCRIPT} not found")
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
if importlib.util.find_spec("streamlit") is None:
|
|
142
|
+
logger.warning(
|
|
143
|
+
"Dashboard skipped: streamlit not installed. Install the extra "
|
|
144
|
+
"(pip install 'aetherscan[dashboard]'), rebuild the NGC container (.sif) / conda env "
|
|
145
|
+
"after the streamlit/plotly deps landed, or run with --no-dashboard."
|
|
146
|
+
)
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
db = get_db()
|
|
150
|
+
db_path = (
|
|
151
|
+
db.db_path if db is not None else os.path.join(config.output_path, "db", "aetherscan.db")
|
|
152
|
+
)
|
|
153
|
+
tag = config.checkpoint.save_tag
|
|
154
|
+
plots_dir = os.path.join(config.output_path, "plots")
|
|
155
|
+
port = config.monitor.dashboard_port
|
|
156
|
+
|
|
157
|
+
if _port_in_use(port):
|
|
158
|
+
# A concurrent run on this node already holds the port. Don't spawn a Streamlit that would
|
|
159
|
+
# exit immediately (stderr is DEVNULL'd) and, worse, don't log an SSH-forward line that
|
|
160
|
+
# would tunnel the user to the OTHER run's dashboard — a wrong-tag/wrong-DB footgun.
|
|
161
|
+
logger.warning(
|
|
162
|
+
f"Dashboard skipped: port {port} is already in use (another run's dashboard?). "
|
|
163
|
+
f"Free the port or pass --dashboard-port to pick another."
|
|
164
|
+
)
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
cmd = build_dashboard_command(
|
|
168
|
+
sys.executable, str(_DASHBOARD_SCRIPT), db_path, tag, plots_dir, port
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
# start_new_session puts the server in its own session/process group so a Ctrl-C sent to
|
|
173
|
+
# the pipeline's whole foreground group doesn't kill it mid-write; teardown is instead
|
|
174
|
+
# driven deterministically by our atexit hook (graceful exit) and SIGTERM/SIGINT handlers
|
|
175
|
+
# (signal-kill), both of which killpg the new group to reap Streamlit + any grandchildren.
|
|
176
|
+
proc = subprocess.Popen(
|
|
177
|
+
cmd,
|
|
178
|
+
stdout=subprocess.DEVNULL,
|
|
179
|
+
stderr=subprocess.DEVNULL,
|
|
180
|
+
stdin=subprocess.DEVNULL,
|
|
181
|
+
start_new_session=True,
|
|
182
|
+
)
|
|
183
|
+
except (OSError, ValueError) as e:
|
|
184
|
+
logger.warning(f"Dashboard failed to launch (is streamlit installed?): {e}")
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
atexit.register(_terminate, proc)
|
|
188
|
+
_install_signal_teardown(proc)
|
|
189
|
+
host = socket.gethostname()
|
|
190
|
+
logger.info(
|
|
191
|
+
f"Live dashboard for '{tag}' on port {port}. Reach it with: "
|
|
192
|
+
f"ssh -L {port}:localhost:{port} {host} then open http://localhost:{port}"
|
|
193
|
+
)
|
|
194
|
+
return proc
|