noaa-goes-visualization/tests/test_profile_archive.py

102 lines
4.7 KiB
Python
Raw Permalink Normal View History

2026-10-14 00:27:18 -04:00
"""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