"""Aggregate complete matched point-KD triplets without selecting best epochs.""" from __future__ import annotations import csv import hashlib import itertools import json from pathlib import Path import numpy as np ROOT = Path(r"E:\Gaze_estimation") RUN_ROOT = ROOT / "artifacts" / "kd-teacher-trap-diagnostic" / "matched-runs-v1" OUT = ROOT / "artifacts" / "kd-teacher-trap-diagnostic" ARMS = ("control", "point_kd", "quality_gated_point_kd", "shuffled_teacher_kd") def sha256(path: Path): value = hashlib.sha256() with path.open("rb") as stream: for block in iter(lambda: stream.read(1024 * 1024), b""): value.update(block) return value.hexdigest().upper() def read_run(path: Path): summary_path, config_path, epochs_path = path / "summary.json", path / "config.json", path / "epochs.jsonl" summary = json.loads(summary_path.read_text(encoding="utf-8")) config = json.loads(config_path.read_text(encoding="utf-8")) if summary["config_sha256"] != sha256(config_path) or summary["epoch_log_sha256"] != sha256(epochs_path): raise RuntimeError(f"run hashes changed: {path}") epochs = [json.loads(line) for line in epochs_path.read_text(encoding="utf-8").splitlines()] if epochs[-1] != summary["primary_result"]: raise RuntimeError(f"primary result is not final epoch: {path}") return summary, config, epochs def sign_flip(values, seed=20260809): values = np.asarray(values, dtype=float) observed = abs(values.mean()) if len(values) <= 20: outcomes = [abs((values * np.asarray(signs)).mean()) for signs in itertools.product((-1, 1), repeat=len(values))] return float(np.mean(np.asarray(outcomes) >= observed)), "exact" rng = np.random.default_rng(seed) outcomes = np.abs((rng.choice((-1, 1), size=(200000, len(values))) * values).mean(axis=1)) return float((np.sum(outcomes >= observed) + 1) / (len(outcomes) + 1)), "monte_carlo_200000" def contrast(rows, arm, field=None, interpretation="negative favors arm; positive favors control"): field = field or f"{arm}_minus_control_deg" differences = np.asarray([row[field] for row in rows]) by_participant = {} for row in rows: by_participant.setdefault(row["held_out"], []).append(row[field]) participant_values = np.asarray([np.mean(value) for value in by_participant.values()]) rng = np.random.default_rng(20260809) keys = list(by_participant) boot = [] for _ in range(20000): selected = rng.choice(keys, size=len(keys), replace=True) boot.append(np.mean([np.mean(by_participant[key]) for key in selected])) p_value, method = sign_flip(participant_values) return { "arm": arm, "fold_seed_pairs": len(rows), "participants": len(keys), "mean_paired_difference_deg": float(differences.mean()), "median_paired_difference_deg": float(np.median(differences)), "participant_cluster_bootstrap_95ci_deg": [float(x) for x in np.quantile(boot, (0.025, 0.975))], "participant_sign_flip_p_two_sided": p_value, "participant_sign_flip_method": method, "interpretation": interpretation, } def main(): grouped = {} for summary_path in RUN_ROOT.glob("p??/seed-*/?*/summary.json"): run_dir = summary_path.parent summary, config, epochs = read_run(run_dir) key = (summary["held_out"], int(summary["seed"])) grouped.setdefault(key, {})[summary["arm"]] = (summary, config, epochs) complete = {key: value for key, value in grouped.items() if set(value) == set(ARMS)} if not complete: raise RuntimeError("no complete control/point-KD/gated-point-KD triplets") rows = [] for (held_out, seed), runs in sorted(complete.items()): configs = [runs[arm][1] for arm in ARMS] for field in ("initial_state_sha256", "sampler_orders_sha256", "shuffled_teacher_permutation_sha256", "effective_lambda_kd", "gate_tau_good_deg", "gate_tau_bad_deg", "epochs", "lr", "batch_size"): if len({json.dumps(config[field], sort_keys=True) for config in configs}) != 1: raise RuntimeError(f"unmatched {field} for {held_out}/seed-{seed}") control = runs["control"][0]["primary_result"]["student_3d_error_mean_deg"] point = runs["point_kd"][0]["primary_result"]["student_3d_error_mean_deg"] gated = runs["quality_gated_point_kd"][0]["primary_result"]["student_3d_error_mean_deg"] shuffled = runs["shuffled_teacher_kd"][0]["primary_result"]["student_3d_error_mean_deg"] rows.append({ "held_out": held_out, "seed": seed, "control_final_3d_error_deg": control, "point_kd_final_3d_error_deg": point, "quality_gated_point_kd_final_3d_error_deg": gated, "shuffled_teacher_kd_final_3d_error_deg": shuffled, "point_kd_minus_control_deg": point - control, "quality_gated_point_kd_minus_control_deg": gated - control, "shuffled_teacher_kd_minus_control_deg": shuffled - control, "point_kd_minus_shuffled_teacher_kd_deg": point - shuffled, "heldout_teacher_3d_error_deg": runs["control"][0]["primary_result"]["teacher_3d_error_mean_deg"], "initial_state_sha256": configs[0]["initial_state_sha256"], "sampler_orders_sha256": configs[0]["sampler_orders_sha256"], "effective_lambda_kd": configs[0]["effective_lambda_kd"], }) csv_path = OUT / "matched_pointkd_primary_results.csv" with csv_path.open("w", encoding="utf-8", newline="") as stream: writer = csv.DictWriter(stream, fieldnames=rows[0].keys()); writer.writeheader(); writer.writerows(rows) participants = sorted({row["held_out"] for row in rows}) report = { "schema": "matched-pointkd-analysis-v1", "primary_epoch_policy": "fixed final epoch; no held-out early stopping", "participants": participants, "seeds": sorted({row["seed"] for row in rows}), "complete_triplets": len(rows), "study_status": "CONFIRMATORY_COMPLETE" if len(participants) == 15 and min(sum(row["held_out"] == p for row in rows) for p in participants) >= 3 else "PREFLIGHT_ONLY", "contrasts": [ contrast(rows, "point_kd"), contrast(rows, "quality_gated_point_kd"), contrast(rows, "shuffled_teacher_kd"), contrast( rows, "point_kd_vs_shuffled_teacher_kd", field="point_kd_minus_shuffled_teacher_kd_deg", interpretation="negative favors real teacher correspondence; positive favors shuffled teacher", ), ], "primary_results_csv": str(csv_path.resolve()), "primary_results_csv_sha256": sha256(csv_path), } output = OUT / "matched_pointkd_analysis.json" output.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") print(json.dumps(report, indent=2)) if __name__ == "__main__": main()