Files
GotthardGandClaude Fable 5.1 82f0d9099b
CI / ledger-and-tools (push) Successful in 19s
gate: bootstrap by mount, light states and excluded frames of the gold set
Frames of one mount are the same loop seen again, so the paired bootstrap now draws
mounts (data/gold/groups.json, built by tools/gold_groups.py from the mount ids the
studio writes in the gold manifest). The comparison also reports the unseen core without
the saturated frames, and frames the studio dropped from the gold set are pruned.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 15:46:31 +02:00

152 lines
8.3 KiB
Python

"""tools/train_run.py gate: recall-aware mask score and the pass rule."""
import json
from tools import train_run
def _report(f1, iou, tp, fn, classes=None):
return {"overall": {"f1": f1, "mean_mask_iou": iou, "tp": tp, "fn": fn, "fp": 0, "precision": 1.0, "recall": tp / (tp + fn)},
"per_class": classes or {}}
def test_mask_score_weights_by_recall():
assert abs(train_run.mask_score({"mean_mask_iou": 0.9, "tp": 60, "fn": 40}) - 0.54) < 1e-9
assert train_run.mask_score({"mean_mask_iou": 0.9, "tp": 0, "fn": 0}) == 0.0
def test_gate_prefers_finding_more_with_slightly_looser_masks(tmp_path):
base = tmp_path / "base.json"
base.write_text(json.dumps(_report(0.69, 0.903, 60, 40, {"loop_all": {"f1": 0.71}, "pin": {"f1": 0.84}})))
new = _report(0.85, 0.883, 85, 15, {"loop_all": {"f1": 0.90}, "pin": {"f1": 0.92}})
g = train_run.gate(new, base)
assert g["pass"] is True and g["mask_iou_delta"] > 0 and g["matched_only_iou_delta"] < 0
def test_gate_refuses_a_guarded_class_regression(tmp_path):
base = tmp_path / "base.json"
base.write_text(json.dumps(_report(0.70, 0.90, 60, 40, {"loop_all": {"f1": 0.80}, "pin": {"f1": 0.84}})))
new = _report(0.80, 0.90, 80, 20, {"loop_all": {"f1": 0.75}, "pin": {"f1": 0.90}})
assert train_run.gate(new, base)["pass"] is False
def test_no_baseline_means_no_gate():
assert train_run.gate(_report(0.8, 0.9, 1, 0), None) is None
def _frames(per_frame_tp, fp=0, objects=2, iou=0.9):
"""One class, ``objects`` gold objects per frame, ``tp`` of them found."""
return {f"f{i}": {0: (float(tp), float(fp), float(objects - tp), iou * tp)} for i, tp in enumerate(per_frame_tp)}
def test_paired_bootstrap_separates_a_real_gain_from_a_tie():
from tools import gate_stats
base = _frames([1] * 60)
better = _frames([2] * 40 + [1] * 20) # finds more on two thirds of the frames
same = _frames([1] * 60)
r = gate_stats.paired_bootstrap(better, base, draws=400)
assert r["frames"] == 60 and r["f1"]["win"] > 0.99 and r["f1"]["lo"] > 0 and r["mask_score"]["win"] > 0.99
t = gate_stats.paired_bootstrap(same, base, draws=400)
assert t["f1"]["delta"] == 0 and t["f1"]["win"] == 0.0 # never strictly better
def test_paired_bootstrap_pairs_only_on_common_frames():
from tools import gate_stats
cand = _frames([2] * 10); base = _frames([1] * 6)
assert gate_stats.paired_bootstrap(cand, base, draws=50)["frames"] == 6
assert gate_stats.paired_bootstrap(cand, {}, draws=50) == {"frames": 0, "draws": 0}
def test_gate_with_bootstrap_needs_a_clear_win(tmp_path):
base = tmp_path / "base.json"
base.write_text(json.dumps(_report(0.80, 0.90, 80, 20, {"loop_all": {"f1": 0.90}, "pin": {"f1": 0.90}})))
new = _report(0.806, 0.90, 81, 19, {"loop_all": {"f1": 0.90}, "pin": {"f1": 0.90}})
noisy = {"frames": 150, "draws": 2000, "f1": {"delta": 0.006, "lo": -0.02, "hi": 0.03, "win": 0.66},
"mask_score": {"delta": 0.004, "lo": -0.02, "hi": 0.03, "win": 0.60}}
g = train_run.gate(new, base, noisy)
assert g["pass_point_rule"] is True and g["pass"] is False and g["rule"] == "paired bootstrap"
clear = {"frames": 150, "draws": 2000, "f1": {"delta": 0.05, "lo": 0.02, "hi": 0.08, "win": 1.0},
"mask_score": {"delta": 0.04, "lo": 0.01, "hi": 0.07, "win": 0.99}}
assert train_run.gate(new, base, clear)["pass"] is True
lopsided = {"frames": 150, "draws": 2000, "f1": {"delta": 0.03, "lo": 0.0, "hi": 0.06, "win": 0.97},
"mask_score": {"delta": -0.04, "lo": -0.07, "hi": -0.01, "win": 0.01}}
assert train_run.gate(new, base, lopsided)["pass"] is False # wins F1 but clearly loses the masks
def test_gate_without_predictions_keeps_the_point_rule(tmp_path):
base = tmp_path / "base.json"
base.write_text(json.dumps(_report(0.69, 0.903, 60, 40)))
g = train_run.gate(_report(0.85, 0.883, 85, 15), base)
assert g["rule"] == "point" and g["pass"] is True and g["pass_point_rule"] is True
def test_same_environment_trusts_legacy_reports_only_on_the_spark():
here = {"host": "gpu102", "ultralytics": "8.4.37", "torch": "2.5.1"}
assert train_run.same_environment({"ultralytics": "8.4.37", "torch": "2.5.1", "host": "mx-spark-01"}, here) is True
assert train_run.same_environment({"ultralytics": "8.4.160", "torch": "2.5.1"}, here) is False
assert train_run.same_environment(None, here) is False # legacy report, read on the cluster
assert train_run.same_environment(None, {"host": "mx-spark-01", "ultralytics": "x", "torch": "y"}) is True
def test_baseline_from_another_environment_is_measured_again_or_refused(tmp_path, monkeypatch):
reg = tmp_path / "models" / "registry" / "v-old"; reg.mkdir(parents=True)
stored = reg / "gold_report.json"
stored.write_text(json.dumps(dict(_report(0.84, 0.865, 80, 20), environment={"ultralytics": "8.4.160", "torch": "2.14.0"})))
monkeypatch.setattr(train_run, "ROOT", tmp_path)
monkeypatch.setattr(train_run, "environment", lambda: {"host": "gpu102", "ultralytics": "8.4.37", "torch": "2.5.1"})
# no weights here: no baseline to gate against, and a reason
path, note = train_run.baseline_here(stored, ["loop_all"], 0.25)
assert path is None and "weights are not here" in note
# an lfs pointer is not weights either
(reg / "weights.pt").write_text("version https://git-lfs.github.com/spec/v1")
assert train_run.baseline_here(stored, ["loop_all"], 0.25)[0] is None
# real weights: measured again here, the local report is the baseline
(reg / "weights.pt").write_bytes(b"0" * 1_100_000)
monkeypatch.setattr(train_run, "measure", lambda w, work, names, conf=0.25: _report(0.86, 0.886, 85, 15))
path, note = train_run.baseline_here(stored, ["loop_all"], 0.25)
assert path.name == "gold_report.local.json" and json.loads(path.read_text())["overall"]["f1"] == 0.86
assert "measured again" in note
# same environment: the stored report is used as it is
monkeypatch.setattr(train_run, "environment", lambda: {"host": "x", "ultralytics": "8.4.160", "torch": "2.14.0"})
assert train_run.baseline_here(stored, ["loop_all"], 0.25) == (stored, None)
def test_bootstrap_by_mount_widens_the_interval_of_repeated_frames():
"""Ten frames of one mount all won by the candidate: frame-wise draws call it a sure win,
mount-wise draws see one lucky mount among eleven."""
from tools import gate_stats
base = _frames([1] * 20)
cand = _frames([2] * 10 + [1] * 10)
groups = {f"f{i}": "mount-a" for i in range(10)}
by_frame = gate_stats.paired_bootstrap(cand, base, draws=800)
by_mount = gate_stats.paired_bootstrap(cand, base, draws=800, groups=groups)
assert by_frame["mounts"] == 20 and by_mount["mounts"] == 11
assert by_frame["f1"]["delta"] == by_mount["f1"]["delta"] # the point estimate does not move
assert by_frame["f1"]["win"] > 0.99 and by_mount["f1"]["win"] < 0.75
assert by_mount["f1"]["lo"] <= 0 < by_frame["f1"]["lo"]
def test_gold_groups_lists_shared_mounts_and_dropped_frames(tmp_path):
from tools import gate_stats, gold_groups
gold = tmp_path / "data" / "gold"
(gold / "labels").mkdir(parents=True); (gold / "images").mkdir()
manifest = {"images": {"tpi_1": {"tpid": 1, "mount": "PXII:s6416"}, "tpi_2": {"tpid": 2, "mount": "PXII:s6416"},
"tpi_3": {"tpid": 3, "mount": "PXIII:s7"}, "tpi_4": {"tpid": 4}},
"dropped": {"tpi_9": "same image as tpi_1"}}
(gold / "gold_manifest.json").write_text(json.dumps(manifest))
for stem in ("tpi_1", "tpi_9"):
(gold / "labels" / f"{stem}.txt").write_text("")
assert gold_groups.prune(gold) == 1 and not (gold / "labels" / "tpi_9.txt").exists()
g = gold_groups.build(gold, with_light=False)
assert g["mount"] == {"tpi_1": "PXII:s6416", "tpi_2": "PXII:s6416"} # single-frame mounts are not listed
assert g["excluded"] == {"tpi_9": "same image as tpi_1"}
(gold / "groups.json").write_text(json.dumps(g))
assert gate_stats.load_groups(gold)["mount"]["tpi_2"] == "PXII:s6416"
assert gate_stats.load_groups(tmp_path)["mount"] == {} # no file: every frame its own mount
def test_light_state_thresholds():
from tools import gold_groups
assert gold_groups.light_state(0.6, 200) == "saturated"
assert gold_groups.light_state(0.0, 12) == "dark"
assert gold_groups.light_state(0.1, 120) == "ok"