import json import os import numpy as np import pytest from conftest import solar_disc from suvi import corpus, dataset, paths, segments as segments_lib CAD = paths.CADENCE BASE = 1715385600 # 2024-05-11 00:00 UTC def make_segments(lengths, gap=400): """Contiguous runs of the given slot lengths, spaced apart.""" out, t = [], BASE for length in lengths: out.append((t, t + length * CAD)) t += (length + gap) * CAD return out def test_slices_are_capped_so_no_segment_dominates(): segments = make_segments([300, 2000, 350]) chosen, used = corpus.choose_slices(segments, cap=512, budget_bytes=1e12, satellites=(16, 18), seed=0, val_count=0, test_count=0) lengths = sorted((b - a) // CAD for a, b in chosen["train"]) assert lengths == [300, 350, 512] # the 2000-slot run was truncated assert used == pytest.approx(sum(lengths) * corpus.SLOT_BYTES * 2) def test_slices_stay_inside_their_segment(): segments = make_segments([2000]) for seed in range(8): chosen, _ = corpus.choose_slices(segments, cap=512, budget_bytes=1e12, satellites=(16,), seed=seed, val_count=0, test_count=0) (start, end), = chosen["train"] assert BASE <= start and end <= BASE + 2000 * CAD assert (end - start) // CAD == 512 def test_budget_is_respected_and_prefers_more_segments(): segments = make_segments([400] * 20) budget = 10 * 400 * corpus.SLOT_BYTES * 2 # room for exactly ten chosen, used = corpus.choose_slices(segments, cap=512, budget_bytes=budget, satellites=(16, 18), seed=1, val_count=0, test_count=0) assert len(chosen["train"]) == 10 assert used <= budget def test_evaluation_sets_are_admitted_first_and_are_diverse(): """val/test must not be squeezed out by a full disk, and must span many segments -- three long stretches would make validation measure three days.""" segments = make_segments([400] * 30) budget = 12 * 400 * corpus.SLOT_BYTES * 2 chosen, _ = corpus.choose_slices(segments, cap=512, budget_bytes=budget, satellites=(16, 18), seed=2, val_count=5, test_count=5) assert len(chosen["val"]) == 5 and len(chosen["test"]) == 5 assert len(chosen["train"]) == 2 everything = chosen["train"] + chosen["val"] + chosen["test"] assert len({s for s, _ in everything}) == len(everything) # no reuse def test_evaluation_segments_are_stable_across_budgets(): """Repacking at a different budget or cap must not silently change what the model is being measured on.""" segments = make_segments([400] * 30) big, _ = corpus.choose_slices(segments, cap=512, budget_bytes=1e12, satellites=(16,), seed=3, val_count=4, test_count=4) small, _ = corpus.choose_slices(segments, cap=512, budget_bytes=10 * 400 * corpus.SLOT_BYTES, satellites=(16,), seed=3, val_count=4, test_count=4) assert big["val"] == small["val"] and big["test"] == small["test"] def test_wanted_slots_groups_every_chosen_slice_by_day_and_satellite(): """The slices say which Sun to ship; the builder needs it keyed the way shards are, one ascending list per (day, satellite).""" chosen = {"train": [(BASE, BASE + 5 * CAD)], "val": [(BASE + 100 * CAD, BASE + 102 * CAD)], # same day: 360/day "test": []} grouped = corpus.wanted_slots(chosen, (16, 18)) assert set(grouped) == {("2024-05-11", 16), ("2024-05-11", 18)} for times in grouped.values(): assert times == sorted(times) assert len(times) == 7 # 5 train + 2 val on one day assert all((t - BASE) % CAD == 0 for t in times) def test_wanted_slots_splits_a_slice_that_crosses_midnight(): """Shards are day-keyed, so a slice spanning a day boundary must land in two.""" start = BASE + 358 * CAD # 23:52 UTC grouped = corpus.wanted_slots({"train": [(start, start + 4 * CAD)]}, (16,)) assert sorted(grouped) == [("2024-05-11", 16), ("2024-05-12", 16)] assert len(grouped[("2024-05-11", 16)]) == 2 assert len(grouped[("2024-05-12", 16)]) == 2 def test_no_day_hosts_two_splits(): """Shards are day-keyed, so a shared day would place val frames inside the training sampler's context reach -- mild leakage, easy to prevent here.""" # Adjacent segments separated by only a few slots share calendar days. segments = [(BASE + i * 150 * CAD, BASE + (i * 150 + 130) * CAD) for i in range(24)] chosen, _ = corpus.choose_slices(segments, cap=512, budget_bytes=1e12, satellites=(16, 18), seed=4, val_count=6, test_count=6) owner = {} for role, slices in chosen.items(): for pair in slices: for day in segments_lib.days_touched([pair]): assert owner.setdefault(day, role) == role, f"{day} shared" assert chosen["val"] and chosen["test"] and chosen["train"] def test_default_cap_is_half_a_solar_rotation(): """The scale at which a contiguous segment stops being one view of one hemisphere. Capping below this discards new Sun, not redundancy.""" slots = segments_lib.decorrelation_slots() days = slots * CAD / 86400 assert 12.5 < days < 14.0, f"{days:.1f} d is not half a synodic rotation" # Nothing in the present corpus reaches it, so nothing gets truncated. segments = make_segments([2570]) chosen, _ = corpus.choose_slices(segments, cap=slots, budget_bytes=1e12, satellites=(16, 18), seed=0, val_count=0, test_count=0) (a, b), = chosen["train"] assert (b - a) // CAD == 2570