"""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