102 lines
4.7 KiB
Python
102 lines
4.7 KiB
Python
|
|
"""profile_archive: the availability model is built from the stored judgment.
|
||
|
|
|
||
|
|
The model decides which stretches become training truth and how simulated outages
|
||
|
|
are drawn, so what is pinned here is that it is per band -- a frame judged bad makes
|
||
|
|
its band BAD and an absent frame its band MISSING, leaving the slot's other bands
|
||
|
|
GOOD -- that stubs are not frames, that an unjudged era is refused rather than
|
||
|
|
counted as good, and that only the named run is read.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import json
|
||
|
|
|
||
|
|
import numpy as np
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
import profile_archive
|
||
|
|
from suvi import availability, db, detectors, paths
|
||
|
|
|
||
|
|
T0 = 1704067200 # 2024-01-01
|
||
|
|
|
||
|
|
|
||
|
|
def build(tmp_path, bad=(), missing=(), stub=(), unjudged=()):
|
||
|
|
"""Two slots per satellite, every band; knobs remove or spoil individual frames."""
|
||
|
|
conn = db.connect(str(tmp_path / "index.sqlite"))
|
||
|
|
run = db.create_detector_run(conn, detectors.JUDGMENT_RUN, detectors.judgment_config())
|
||
|
|
other = db.create_detector_run(conn, detectors.JUDGMENT_RUN, {"older": "rules"})
|
||
|
|
rows = []
|
||
|
|
for satellite in (16, 18):
|
||
|
|
for slot in range(2):
|
||
|
|
for wavelength in paths.WAVELENGTHS:
|
||
|
|
key = (satellite, slot, wavelength)
|
||
|
|
if key in missing:
|
||
|
|
continue
|
||
|
|
when = T0 + slot * paths.CADENCE
|
||
|
|
name = paths.FrameName(satellite=satellite, wavelength=wavelength,
|
||
|
|
t_start=when, t_end=when + paths.CADENCE,
|
||
|
|
version="1-0-2")
|
||
|
|
size = 5_000 if key in stub else 1_500_000
|
||
|
|
frame_id = db.upsert_frame(conn, name, name.relpath(), size_bytes=size)
|
||
|
|
if key not in unjudged:
|
||
|
|
rows.append((frame_id, "bad" if key in bad else "good", None, None, 0))
|
||
|
|
db.record_detections(conn, other, [(frame_id, "bad", None, None, 0)])
|
||
|
|
db.record_detections(conn, run, rows)
|
||
|
|
conn.commit()
|
||
|
|
return conn, run
|
||
|
|
|
||
|
|
|
||
|
|
def test_a_bad_band_and_a_missing_band_spoil_only_themselves(tmp_path):
|
||
|
|
conn, run = build(tmp_path, bad={(16, 0, 171)}, missing={(18, 1, 304)})
|
||
|
|
states, rates, unjudged = profile_archive.classify_era(
|
||
|
|
conn, T0, T0 + 2 * paths.CADENCE, (16, 18), run)
|
||
|
|
assert unjudged == 0 and states.shape == (2, 6, 2)
|
||
|
|
band = {w: i for i, w in enumerate(paths.WAVELENGTHS)}
|
||
|
|
expected = np.full((2, 6, 2), availability.GOOD)
|
||
|
|
expected[0, band[171], 0] = availability.BAD
|
||
|
|
expected[1, band[304], 1] = availability.MISSING
|
||
|
|
np.testing.assert_array_equal(states, expected)
|
||
|
|
assert rates[171] == pytest.approx(1 / 4)
|
||
|
|
|
||
|
|
|
||
|
|
def test_stubs_are_not_frames(tmp_path):
|
||
|
|
conn, run = build(tmp_path, stub={(16, 1, 94)}, unjudged={(16, 1, 94)})
|
||
|
|
states, _, unjudged = profile_archive.classify_era(
|
||
|
|
conn, T0, T0 + 2 * paths.CADENCE, (16, 18), run)
|
||
|
|
assert unjudged == 0 and states[0, 0, 1] == availability.MISSING
|
||
|
|
assert (states[0, 1:, 1] == availability.GOOD).all()
|
||
|
|
|
||
|
|
|
||
|
|
def test_only_the_named_run_is_read(tmp_path):
|
||
|
|
"""The other run calls everything bad; none of it may leak in."""
|
||
|
|
conn, run = build(tmp_path)
|
||
|
|
states, _, _ = profile_archive.classify_era(conn, T0, T0 + 2 * paths.CADENCE,
|
||
|
|
(16, 18), run)
|
||
|
|
assert (states == availability.GOOD).all()
|
||
|
|
|
||
|
|
|
||
|
|
def test_an_unjudged_era_is_refused(tmp_path):
|
||
|
|
build(tmp_path, unjudged={(18, 0, 131)})
|
||
|
|
with pytest.raises(SystemExit, match="REFUSING"):
|
||
|
|
profile_archive.main(["--out", str(tmp_path / "out"), "--from", "2024-01-01",
|
||
|
|
"--to", "2024-01-02", "--db", str(tmp_path / "index.sqlite")])
|
||
|
|
|
||
|
|
|
||
|
|
def test_no_judgment_run_under_the_current_rules_is_refused(tmp_path):
|
||
|
|
conn = db.connect(str(tmp_path / "index.sqlite"))
|
||
|
|
db.create_detector_run(conn, detectors.JUDGMENT_RUN, {"older": "rules"})
|
||
|
|
with pytest.raises(SystemExit, match="No judgment run"):
|
||
|
|
profile_archive.main(["--out", str(tmp_path / "out"),
|
||
|
|
"--db", str(tmp_path / "index.sqlite")])
|
||
|
|
|
||
|
|
|
||
|
|
def test_main_writes_the_model_and_report_from_the_current_run(tmp_path):
|
||
|
|
conn, run = build(tmp_path, bad={(16, 0, 171)})
|
||
|
|
out = tmp_path / "out"
|
||
|
|
assert profile_archive.main(["--out", str(out), "--from", "2024-01-01",
|
||
|
|
"--to", "2024-01-02", "--allow-partial",
|
||
|
|
"--db", str(tmp_path / "index.sqlite")]) == 0
|
||
|
|
report = json.loads((out / "availability_report.json").read_text())
|
||
|
|
assert report["judgment_run"] == run
|
||
|
|
model = availability.Availability.load(str(out / "availability.npz"))
|
||
|
|
assert model.states[0, paths.WAVELENGTHS.index(171), 0] == availability.BAD
|
||
|
|
assert "171" in report["damage_patterns"] # the one partial slot
|