Skip to content

Commit e82e87c

Browse files
committed
trace ace: add runtime promotion validation harness
1 parent 690fe54 commit e82e87c

1 file changed

Lines changed: 116 additions & 0 deletions

File tree

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#!/usr/bin/env python3
2+
"""Rule/runtime validation helpers for a frozen Trace the Ace submission.
3+
4+
This script is intentionally model-agnostic. It validates the generated
5+
submission contract, compares frozen research/runtime predictions, and checks
6+
sample-independence fixtures produced by the runtime candidate.
7+
"""
8+
from __future__ import annotations
9+
10+
import argparse
11+
import json
12+
from pathlib import Path
13+
import numpy as np
14+
import pandas as pd
15+
16+
17+
def read_headers(path: Path) -> list[str]:
18+
return list(pd.read_csv(path, nrows=0).columns)
19+
20+
21+
def validate_output(fmt_path: Path, pred_path: Path) -> dict:
22+
print("submission_format columns:", read_headers(fmt_path))
23+
print("submission columns:", read_headers(pred_path))
24+
fmt = pd.read_csv(fmt_path)
25+
pred = pd.read_csv(pred_path)
26+
required = ["response_id", "probability"]
27+
if list(pred.columns) != required:
28+
raise SystemExit(f"FAIL columns: expected {required}, got {list(pred.columns)}")
29+
if len(pred) != len(fmt):
30+
raise SystemExit(f"FAIL row count: {len(pred)} != {len(fmt)}")
31+
if pred.response_id.duplicated().any():
32+
raise SystemExit("FAIL duplicate response_id")
33+
if pred.response_id.astype(str).tolist() != fmt.response_id.astype(str).tolist():
34+
raise SystemExit("FAIL response IDs/order differ from submission_format")
35+
p = pred.probability.to_numpy(float)
36+
if not np.isfinite(p).all():
37+
raise SystemExit("FAIL non-finite probability")
38+
if ((p < 0) | (p > 1)).any():
39+
raise SystemExit("FAIL probability outside [0,1]")
40+
result = {
41+
"rows": int(len(p)),
42+
"min_probability": float(p.min()),
43+
"max_probability": float(p.max()),
44+
"lt_0p01": int((p < .01).sum()),
45+
"gt_0p99": int((p > .99).sum()),
46+
"quantiles": {str(q): float(np.quantile(p, q)) for q in [0,.001,.01,.05,.5,.95,.99,.999,1]},
47+
}
48+
print(json.dumps(result, indent=2))
49+
return result
50+
51+
52+
def compare_predictions(a_path: Path, b_path: Path, tol: float) -> dict:
53+
print("reference columns:", read_headers(a_path))
54+
print("runtime columns:", read_headers(b_path))
55+
a = pd.read_csv(a_path)
56+
b = pd.read_csv(b_path)
57+
if "response_id" not in a or "probability" not in a or "response_id" not in b or "probability" not in b:
58+
raise SystemExit("FAIL comparison inputs need response_id,probability")
59+
m = a[["response_id","probability"]].merge(
60+
b[["response_id","probability"]], on="response_id", suffixes=("_a","_b"), validate="one_to_one"
61+
)
62+
if len(m) != len(a) or len(m) != len(b):
63+
raise SystemExit("FAIL comparison response ID sets differ")
64+
d = np.abs(m.probability_a.to_numpy(float) - m.probability_b.to_numpy(float))
65+
result = {"rows": int(len(m)), "max_abs_difference": float(d.max(initial=0)), "tolerance": tol}
66+
print(json.dumps(result, indent=2))
67+
if result["max_abs_difference"] > tol:
68+
raise SystemExit("FAIL prediction parity")
69+
return result
70+
71+
72+
def independence(paths: list[Path], response_id: str, tol: float) -> dict:
73+
vals = []
74+
for path in paths:
75+
print(f"{path.name} columns:", read_headers(path))
76+
df = pd.read_csv(path)
77+
hit = df.loc[df.response_id.astype(str) == str(response_id), "probability"]
78+
if len(hit) != 1:
79+
raise SystemExit(f"FAIL {path}: expected one row for {response_id}, got {len(hit)}")
80+
vals.append(float(hit.iloc[0]))
81+
spread = max(vals) - min(vals)
82+
result = {"response_id": str(response_id), "probabilities": vals, "spread": spread, "tolerance": tol}
83+
print(json.dumps(result, indent=2))
84+
if spread > tol:
85+
raise SystemExit("FAIL sample independence")
86+
return result
87+
88+
89+
def self_test() -> None:
90+
import tempfile
91+
with tempfile.TemporaryDirectory() as td:
92+
root = Path(td)
93+
fmt = pd.DataFrame({"response_id":["a","b"], "probability":[0.5,0.5]})
94+
p = pd.DataFrame({"response_id":["a","b"], "probability":[0.2,0.8]})
95+
fmt.to_csv(root/"fmt.csv", index=False); p.to_csv(root/"p.csv", index=False); p.to_csv(root/"q.csv", index=False)
96+
validate_output(root/"fmt.csv", root/"p.csv")
97+
compare_predictions(root/"p.csv", root/"q.csv", 1e-8)
98+
independence([root/"p.csv", root/"q.csv"], "a", 1e-8)
99+
print("SELF TEST PASS")
100+
101+
102+
def main() -> None:
103+
ap = argparse.ArgumentParser()
104+
sub = ap.add_subparsers(dest="cmd", required=True)
105+
sub.add_parser("self-test")
106+
p = sub.add_parser("output"); p.add_argument("--format", required=True); p.add_argument("--predictions", required=True)
107+
p = sub.add_parser("parity"); p.add_argument("--reference", required=True); p.add_argument("--runtime", required=True); p.add_argument("--tol", type=float, default=1e-8)
108+
p = sub.add_parser("independence"); p.add_argument("--response-id", required=True); p.add_argument("--predictions", nargs="+", required=True); p.add_argument("--tol", type=float, default=1e-8)
109+
args = ap.parse_args()
110+
if args.cmd == "self-test": self_test()
111+
elif args.cmd == "output": validate_output(Path(args.format), Path(args.predictions))
112+
elif args.cmd == "parity": compare_predictions(Path(args.reference), Path(args.runtime), args.tol)
113+
else: independence([Path(x) for x in args.predictions], args.response_id, args.tol)
114+
115+
if __name__ == "__main__":
116+
main()

0 commit comments

Comments
 (0)