168 lines
9.5 KiB
Python
168 lines
9.5 KiB
Python
"""Audit completed supplemental surface observations without running solvers.
|
|
|
|
Pass the campaign directory containing captures/, focused/, supplemental/ and
|
|
supplemental_inputs/. Writes surface_audit_checks.json in that directory.
|
|
This records method invariants and sampled values, not physical acceptance.
|
|
"""
|
|
import argparse
|
|
import csv
|
|
import hashlib
|
|
import json
|
|
import math
|
|
from pathlib import Path
|
|
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("campaign_root", type=Path)
|
|
ROOT = parser.parse_args().campaign_root.resolve()
|
|
SUPPLEMENTAL = ROOT / "supplemental"
|
|
|
|
|
|
def sha(path):
|
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
|
|
|
|
def rows(path):
|
|
with path.open() as source:
|
|
data = list(csv.DictReader(source))
|
|
if any(None in r or any(v is None for v in r.values()) for r in data):
|
|
raise ValueError(f"Malformed CSV row: {path}")
|
|
return data
|
|
|
|
|
|
def metrics(path):
|
|
data = rows(path)
|
|
if len({r["metric"] for r in data}) != len(data):
|
|
raise ValueError(f"Duplicate metric: {path}")
|
|
return {r["metric"]: float(r["value"]) for r in data}
|
|
|
|
|
|
def vector(path):
|
|
tokens = path.read_text().split()
|
|
data = list(map(float, tokens[1:]))
|
|
if len(data) != int(tokens[0]) or not all(map(math.isfinite, data)):
|
|
raise ValueError(f"Malformed/nonfinite vector: {path}")
|
|
return data
|
|
|
|
|
|
def clean(value):
|
|
if isinstance(value, dict):
|
|
return {k: clean(v) for k, v in value.items()}
|
|
if isinstance(value, list):
|
|
return [clean(v) for v in value]
|
|
if isinstance(value, float) and not math.isfinite(value):
|
|
return None
|
|
return value
|
|
|
|
|
|
provenance = json.loads((SUPPLEMENTAL / "provenance.json").read_text())
|
|
derivation = json.loads((ROOT / "supplemental_inputs/between_bounds_derivation.json").read_text())
|
|
base = vector(Path(derivation["normalized_base"]))
|
|
direction = vector(Path(derivation["direction"]))
|
|
candidate = vector(Path(derivation["candidate"]))
|
|
if not len(base) == len(direction) == len(candidate):
|
|
raise ValueError("Midpoint candidate vector dimensions differ")
|
|
alpha = derivation["alpha"]
|
|
bounds = metrics(ROOT / "focused/n1_J0p10_iteration_2/frozen_enthalpy_bound.csv")
|
|
result = {
|
|
"source_commit": provenance["commit"],
|
|
"binary_hash_matches": sha(SUPPLEMENTAL / "plateau_diagnostic") == provenance["binary_sha256"],
|
|
"midpoint": {
|
|
"alpha": alpha,
|
|
"vector_size": len(candidate),
|
|
"maximum_reconstruction_error": max(abs(b + alpha * d - c) for b, d, c in zip(base, direction, candidate)),
|
|
"candidate_hash_matches": sha(Path(derivation["candidate"])) == derivation["candidate_sha256"],
|
|
"bounds_match_original_scan": bounds["closure_alpha_h"] == derivation["closure_bound"] and bounds["pressure_alpha_h"] == derivation["pressure_bound"],
|
|
"strictly_between_bounds": derivation["closure_bound"] < alpha < derivation["pressure_bound"],
|
|
"exact_arithmetic_midpoint": alpha == 0.5 * (derivation["closure_bound"] + derivation["pressure_bound"]),
|
|
},
|
|
"completed": [],
|
|
"pending": [],
|
|
}
|
|
for case in json.loads((SUPPLEMENTAL / "cases.json").read_text()):
|
|
case_id = case["id"]
|
|
manifest = SUPPLEMENTAL / f"{case_id}_run.json"
|
|
if not manifest.exists():
|
|
result["pending"].append(case_id)
|
|
continue
|
|
run = json.loads(manifest.read_text())
|
|
if "exit_code" not in run:
|
|
result["pending"].append(case_id)
|
|
continue
|
|
folder = SUPPLEMENTAL / case_id
|
|
args = case["arguments"]
|
|
physical_path = Path(args[args.index("--probe-state") + 1])
|
|
direction_path = Path(args[args.index("--probe-direction") + 1])
|
|
capture_index = int(physical_path.parent.name.split("_")[-1])
|
|
original_focused = ROOT / "focused" / f"n1_J0p10_iteration_{capture_index}"
|
|
capture_root = physical_path.parent.parent
|
|
capture_manifest = json.loads((capture_root.parent / f"{capture_root.name}_run.json").read_text())
|
|
summary = metrics(folder / "surface_trace_summary.csv")
|
|
carrier = rows(folder / "surface_trace_rows.csv")
|
|
limits = rows(folder / "surface_trace_limits.csv")
|
|
nodes = rows(folder / "surface_trace_nodes.csv")
|
|
physical_base = vector(physical_path)
|
|
normalized_direction = vector(direction_path)
|
|
capture_metadata = dict(line.split("=", 1) for line in (physical_path.parent / "capture.txt").read_text().splitlines())
|
|
replay_alpha = alpha if "between_bounds" in case_id else float(capture_metadata["first_trial_alpha"])
|
|
data = {
|
|
"id": case_id,
|
|
"exit_code": run["exit_code"],
|
|
"seconds": run["seconds"],
|
|
"input_hash_count": len(run["input_state_hashes"]),
|
|
"input_hash_mismatches": [p for p, h in run["input_state_hashes"].items() if sha(Path(p)) != h],
|
|
"output_hash_count": len(run["output_hashes"]),
|
|
"output_hash_mismatches": [p for p, h in run["output_hashes"].items() if sha(folder / p) != h],
|
|
"unhashed_data_files": sorted(str(p.relative_to(folder)) for p in folder.rglob("*") if p.is_file() and p.suffix in (".txt", ".csv", ".json") and str(p.relative_to(folder)) not in run["output_hashes"]),
|
|
"auxiliary_capture_input_hashes_match": all(sha(physical_path.parent / name) == capture_manifest["output_hashes"][str((physical_path.parent / name).relative_to(capture_root))] for name in ("normalized_base.txt", "normalized_residual.txt")),
|
|
"frozen": metrics(folder / "frozen_input_checks.csv"),
|
|
"frozen_residual_byte_identical_to_original_focus": (folder / "frozen_normalized_residual.txt").read_bytes() == (original_focused / "frozen_normalized_residual.txt").read_bytes(),
|
|
"enthalpy_bound_byte_identical_to_original_focus": (folder / "frozen_enthalpy_bound.csv").read_bytes() == (original_focused / "frozen_enthalpy_bound.csv").read_bytes(),
|
|
"direction_copy_byte_identical": direction_path.read_bytes() == (folder / "probe_normalized_correction.txt").read_bytes(),
|
|
"physical_base_copy_byte_identical": physical_path.read_bytes() == (folder / "frozen_physical_base.txt").read_bytes(),
|
|
"candidate_replay": rows(folder / "replayed_candidate.csv")[0],
|
|
"surface_summary": summary,
|
|
"csv_rows": {p.name: len(rows(p)) for p in sorted(folder.glob("*.csv"))},
|
|
"carrier_rows_complete": len(carrier) == int(summary["surface_row_count"]) and {int(r["surface_index"]) for r in carrier} == set(range(len(carrier))),
|
|
"carrier_reduced_indices_unique": len({r["reduced_h_index"] for r in carrier}) == len(carrier),
|
|
"carrier_root_offsets_correct": all(int(r["root_index"]) == int(summary["h_root_offset"]) + int(r["reduced_h_index"]) for r in carrier),
|
|
"carrier_base_matches_physical_input": all(float(r["base_h"]) == physical_base[int(r["root_index"])] for r in carrier),
|
|
"all_input_carrier_normalized_direction_coefficients_zero": all(normalized_direction[int(r["root_index"])] == 0 for r in carrier),
|
|
"carrier_unit_step_arithmetic_error": max(abs(float(r["base_h"]) + float(r["direction_dh"]) - float(r["h_plus_dh"])) for r in carrier),
|
|
"carrier_recomputed": {},
|
|
"carrier_summary_differences": {},
|
|
"limit_families_complete": {r["family"] for r in limits} == {"closure", "pressure"} and len(limits) == 2,
|
|
"limit_partition_max_errors": {},
|
|
"nodes_by_family": {},
|
|
}
|
|
for name in ("base_h", "direction_dh", "h_plus_dh"):
|
|
values = [float(r[name]) for r in carrier]
|
|
data["carrier_recomputed"][name] = {
|
|
"minimum": min(values), "maximum": max(values), "maximum_absolute": max(map(abs, values)),
|
|
"norm": math.sqrt(math.fsum(v * v for v in values)),
|
|
"negative_count": sum(v < 0 for v in values), "nonzero_count": sum(v != 0 for v in values),
|
|
}
|
|
data["carrier_summary_differences"][name] = {stat: value - summary[f"{name}_{stat}"] for stat, value in data["carrier_recomputed"][name].items()}
|
|
for suffix in ("h", "dh", "h_plus_dh"):
|
|
data["limit_partition_max_errors"][suffix] = max(abs(float(r[f"full_{suffix}"]) - float(r[f"boundary_{suffix}"]) - float(r[f"interior_{suffix}"])) for r in limits)
|
|
for limit in limits:
|
|
family = limit["family"]
|
|
subset = [r for r in nodes if r["family"] == family]
|
|
data["nodes_by_family"][family] = {
|
|
"rows": len(subset),
|
|
"element_matches_limit": all(r["element"] == limit["element"] for r in subset),
|
|
"node_indices_complete": len(subset) > 0 and len(subset) == int(subset[0]["node_count"]) and {int(r["local_node"]) for r in subset} == set(range(len(subset))),
|
|
"fe_orders": sorted({int(r["fe_order"]) for r in subset}),
|
|
"geometry_types": sorted({int(r["geometry_type"]) for r in subset}),
|
|
"base_minimum": min(float(r["h"]) for r in subset),
|
|
"union_prediction_minimum": min(float(r["h_plus_alpha_union_dh"]) for r in subset),
|
|
"unit_step_minimum": min(float(r["h_plus_dh"]) for r in subset),
|
|
"replay_alpha": replay_alpha,
|
|
"replay_candidate_nodal_prediction_minimum": min(float(r["h"]) + replay_alpha * float(r["dh"]) for r in subset),
|
|
"union_prediction_max_arithmetic_error": max(abs(float(r["h"]) + float(r["alpha_union"]) * float(r["dh"]) - float(r["h_plus_alpha_union_dh"])) for r in subset),
|
|
"unit_step_max_arithmetic_error": max(abs(float(r["h"]) + float(r["dh"]) - float(r["h_plus_dh"])) for r in subset),
|
|
}
|
|
result["completed"].append(data)
|
|
|
|
(ROOT / "surface_audit_checks.json").write_text(json.dumps(clean(result), indent=2) + "\n")
|
|
print(json.dumps({"completed": [r["id"] for r in result["completed"]], "pending": result["pending"]}, indent=2))
|