ControlAI-Agent / scripts /validate_sft.py
atakan
feat: Initial open-source release of ControlAI
9936912
Raw
History Blame Contribute Delete
11.9 kB
"""Validate ControlAI SFT drafts and detect benchmark-family leakage."""
from __future__ import annotations
import argparse
import hashlib
import json
import re
import sys
from collections import Counter
from pathlib import Path
from typing import Any
import numpy as np
EXPECTED_ROLES = ["system", "user", "assistant"]
def normalized_hash(text: str) -> str:
normalized = re.sub(r"\s+", " ", text.casefold()).strip()
return hashlib.sha256(normalized.encode("utf-8")).hexdigest()
def load_jsonl(path: Path) -> tuple[list[dict[str, Any]], list[str]]:
records: list[dict[str, Any]] = []
errors: list[str] = []
if not path.exists():
return records, [f"missing file: {path}"]
with path.open(encoding="utf-8") as handle:
for line_number, line in enumerate(handle, start=1):
location = f"{path}:{line_number}"
if not line.strip():
errors.append(f"{location}: blank lines are not allowed")
continue
try:
record = json.loads(line)
except json.JSONDecodeError as exc:
errors.append(f"{location}: invalid JSON ({exc.msg})")
continue
if not isinstance(record, dict):
errors.append(f"{location}: record must be a JSON object")
continue
record["_location"] = location
records.append(record)
return records, errors
def benchmark_families(path: Path) -> tuple[set[str], list[str]]:
records, errors = load_jsonl(path)
families = {
record.get("family")
for record in records
if isinstance(record.get("family"), str)
}
return families, errors
def validate_messages(record: dict[str, Any]) -> list[str]:
location = record["_location"]
messages = record.get("messages")
if not isinstance(messages, list):
return [f"{location}: messages must be a list"]
roles = [message.get("role") for message in messages if isinstance(message, dict)]
errors: list[str] = []
if roles != EXPECTED_ROLES or len(messages) != len(EXPECTED_ROLES):
errors.append(
f"{location}: expected roles {EXPECTED_ROLES}, received {roles}"
)
for index, message in enumerate(messages):
if not isinstance(message, dict):
errors.append(f"{location}: messages[{index}] must be an object")
continue
content = message.get("content")
if not isinstance(content, str) or not content.strip():
errors.append(f"{location}: messages[{index}].content must be non-empty")
return errors
def validate_metadata(
record: dict[str, Any], held_out_families: set[str]
) -> list[str]:
location = record["_location"]
metadata = record.get("metadata")
if not isinstance(metadata, dict):
return [f"{location}: metadata must be an object"]
errors: list[str] = []
for field in ("id", "domain", "family", "source_type", "status"):
if not isinstance(metadata.get(field), str) or not metadata[field].strip():
errors.append(f"{location}: metadata.{field} must be non-empty text")
family = metadata.get("family")
if family in held_out_families:
errors.append(
f"{location}: benchmark-family leakage detected for {family!r}"
)
if metadata.get("status") not in {"draft", "approved", "rejected"}:
errors.append(f"{location}: unsupported metadata.status")
return errors
def validate_stability_record(record: dict[str, Any]) -> list[str]:
"""Independently verify a real 2x2 Hurwitz decision using trace and determinant."""
metadata = record["metadata"]
if metadata.get("family") != "continuous_lti_eigenvalue_stability":
return []
location = record["_location"]
ground_truth = metadata.get("ground_truth")
if not isinstance(ground_truth, dict):
return [f"{location}: stability record is missing ground_truth"]
try:
matrix = np.asarray(ground_truth["A"], dtype=float)
claimed = ground_truth["asymptotically_stable"]
except (KeyError, TypeError, ValueError) as exc:
return [f"{location}: invalid stability ground_truth ({exc})"]
if matrix.shape != (2, 2):
return [f"{location}: expected a 2x2 A matrix, received {matrix.shape}"]
if not isinstance(claimed, bool):
return [f"{location}: asymptotically_stable must be boolean"]
trace = float(np.trace(matrix))
determinant = float(np.linalg.det(matrix))
independently_stable = trace < 0.0 and determinant > 0.0
if claimed != independently_stable:
return [
f"{location}: claimed stability={claimed}, but the independent 2x2 "
f"Hurwitz test gives {independently_stable} "
f"(trace={trace:.6g}, determinant={determinant:.6g})"
]
return []
def complex_values(pairs: list[list[float]]) -> np.ndarray:
return np.asarray([complex(real, imag) for real, imag in pairs])
def same_roots(left: np.ndarray, right: np.ndarray) -> bool:
"""Compare small root sets without depending on eigensolver ordering."""
left = np.asarray(left, dtype=complex)
right = np.asarray(right, dtype=complex)
if left.shape != right.shape:
return False
left = left[np.lexsort((np.imag(left), np.real(left)))]
right = right[np.lexsort((np.imag(right), np.real(right)))]
return bool(np.allclose(left, right, rtol=1e-7, atol=1e-8))
def validate_numeric_ground_truth(record: dict[str, Any]) -> list[str]:
"""Recompute every numeric v0 ground truth independently of answer text."""
location = record["_location"]
metadata = record.get("metadata")
if not isinstance(metadata, dict):
return []
gt = metadata.get("ground_truth")
if not isinstance(gt, dict):
return []
kind = gt.get("kind")
try:
if kind == "second_order":
a1, a0 = float(gt["a1"]), float(gt["a0"])
wn = np.sqrt(a0)
zeta = a1 / (2 * wn)
poles = np.roots([1.0, a1, a0])
valid = (
np.isclose(wn, gt["omega_n"])
and np.isclose(zeta, gt["zeta"])
and same_roots(poles, complex_values(gt["poles"]))
)
elif kind == "observability":
A = np.asarray(gt["A"], dtype=float)
C = np.asarray(gt["C"], dtype=float)
O = np.vstack([C, C @ A])
rank = int(np.linalg.matrix_rank(O))
valid = (
np.allclose(O, gt["O"])
and rank == gt["rank"]
and (rank == A.shape[0]) == gt["observable"]
)
elif kind == "state_feedback":
A = np.asarray(gt["A"], dtype=float)
B = np.asarray(gt["B"], dtype=float)
K = np.asarray(gt["K"], dtype=float)
Acl = A - B @ K
poles = np.linalg.eigvals(Acl)
valid = (
np.allclose(Acl, gt["Acl"])
and same_roots(poles, complex_values(gt["eigenvalues"]))
and bool(np.all(np.real(poles) < 0)) == gt["stable"]
)
elif kind == "discrete_poles":
radius = max(abs(float(pole)) for pole in gt["poles"])
valid = np.isclose(radius, gt["spectral_radius"]) and (
radius < 1
) == gt["stable"]
elif kind == "first_order_frequency":
gain = float(gt["gain"])
x = float(gt["tau"]) * float(gt["omega"])
magnitude = gain / np.sqrt(1 + x * x)
phase = -np.degrees(np.arctan(x))
valid = np.isclose(magnitude, gt["magnitude"]) and np.isclose(
phase, gt["phase_deg"]
)
elif kind == "transfer_properties":
num = np.asarray(gt["numerator"], dtype=float)
den = np.asarray(gt["denominator"], dtype=float)
poles = np.roots(den)
zeros = np.roots(num)
valid = (
same_roots(poles, np.asarray(gt["poles"], dtype=complex))
and same_roots(zeros, np.asarray([gt["zero"]], dtype=complex))
and np.isclose(num[-1] / den[-1], gt["dc_gain"])
)
else:
return []
except (KeyError, TypeError, ValueError, np.linalg.LinAlgError) as exc:
return [f"{location}: invalid {kind!r} ground truth ({exc})"]
if not valid:
return [f"{location}: independent verification failed for {kind!r}"]
return []
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("paths", nargs="+", type=Path)
parser.add_argument(
"--benchmark",
type=Path,
default=Path("benchmarks/v0.jsonl"),
)
args = parser.parse_args()
held_out_families, errors = benchmark_families(args.benchmark)
records: list[dict[str, Any]] = []
for path in args.paths:
loaded, load_errors = load_jsonl(path)
records.extend(loaded)
errors.extend(load_errors)
seen_ids: dict[str, str] = {}
seen_prompts: dict[str, str] = {}
family_counts: Counter[str] = Counter()
stability_checks = 0
numeric_checks = 0
for record in records:
errors.extend(validate_messages(record))
errors.extend(validate_metadata(record, held_out_families))
metadata = record.get("metadata")
messages = record.get("messages")
if not isinstance(metadata, dict) or not isinstance(messages, list):
continue
record_id = metadata.get("id")
if isinstance(record_id, str):
if record_id in seen_ids:
errors.append(
f"{record['_location']}: duplicate id also found at "
f"{seen_ids[record_id]}"
)
else:
seen_ids[record_id] = record["_location"]
family = metadata.get("family")
if isinstance(family, str):
family_counts[family] += 1
if family == "continuous_lti_eigenvalue_stability":
stability_checks += 1
errors.extend(validate_stability_record(record))
ground_truth = metadata.get("ground_truth")
if isinstance(ground_truth, dict) and ground_truth.get("kind") in {
"second_order",
"observability",
"state_feedback",
"discrete_poles",
"first_order_frequency",
"transfer_properties",
}:
numeric_checks += 1
errors.extend(validate_numeric_ground_truth(record))
user_messages = [
message.get("content")
for message in messages
if isinstance(message, dict) and message.get("role") == "user"
]
if user_messages and isinstance(user_messages[0], str):
prompt_hash = normalized_hash(user_messages[0])
if prompt_hash in seen_prompts:
errors.append(
f"{record['_location']}: duplicate prompt also found at "
f"{seen_prompts[prompt_hash]}"
)
else:
seen_prompts[prompt_hash] = record["_location"]
print(f"records checked: {len(records)}")
print(f"unique ids: {len(seen_ids)}")
print(f"benchmark families held out: {len(held_out_families)}")
print(f"independent stability checks: {stability_checks}")
print(f"other independent numeric checks: {numeric_checks}")
print("family counts:")
for family, count in sorted(family_counts.items()):
print(f"- {family}: {count}")
if errors:
print("\nValidation failed:", file=sys.stderr)
for error in errors:
print(f"- {error}", file=sys.stderr)
return 1
print("validation passed")
return 0
if __name__ == "__main__":
raise SystemExit(main())