Files
MeanField/experiments/plateau_analysis/audit_surface_outputs.py

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))