Gate saved models on solver convergence and the virial milestone
This commit is contained in:
203
experiments/check_plateau_physics.py
Normal file
203
experiments/check_plateau_physics.py
Normal file
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Report a plateau endpoint and gate strict solver convergence plus eight virials.
|
||||
|
||||
Usage: check_plateau_physics.py RESULT_FOLDER
|
||||
Exit 0: solver_and_virial_milestone passes. Exit 3: it does not pass.
|
||||
Exit 2: required endpoint data is missing or malformed. Standard-library only.
|
||||
Local errors remain measurements; passing this gate is not full physical validation.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
THRESHOLD = 1.0e-5
|
||||
QUADRATURES = (14, 18)
|
||||
VIRIALS = {
|
||||
"pressure_from_enthalpy_energy": "enthalpy_virial_error",
|
||||
"pressure_from_enthalpy_force": "enthalpy_force_virial_error",
|
||||
"pressure_from_density_energy": "virial_error",
|
||||
"pressure_from_density_force": "force_virial_error",
|
||||
}
|
||||
SIGN_METRICS = (
|
||||
"minimum_density", "maximum_density", "negative_density_samples",
|
||||
"negative_density_mass_fraction", "negative_density_volume_fraction",
|
||||
"minimum_enthalpy", "maximum_enthalpy", "negative_enthalpy_samples",
|
||||
"negative_enthalpy_volume_fraction",
|
||||
)
|
||||
CONSERVATION_METRICS = (
|
||||
"mass", "mass_absolute_error", "angular_momentum_z", "angular_momentum_absolute_error",
|
||||
)
|
||||
LOCAL_METRICS = (
|
||||
"eos_density_mismatch_maximum", "eos_density_mismatch_maximum_scaled",
|
||||
"eos_density_mismatch_rms", "eos_density_mismatch_rms_scaled",
|
||||
"bernoulli_raw_rms", "bernoulli_raw_rms_scaled",
|
||||
"bernoulli_mean_subtracted_rms", "bernoulli_mean_subtracted_rms_scaled",
|
||||
"bernoulli_sampled_range", "hydrostatic_gradient_rms",
|
||||
)
|
||||
PRESSURE_METRICS = (
|
||||
"pressure_from_density_defined_everywhere", "pressure_integral",
|
||||
"pressure_from_enthalpy_defined_everywhere", "enthalpy_pressure_integral",
|
||||
)
|
||||
REQUIRED_METRICS = (
|
||||
*VIRIALS.values(), *SIGN_METRICS, *CONSERVATION_METRICS,
|
||||
*LOCAL_METRICS, *PRESSURE_METRICS, "complete_volume_measurement",
|
||||
)
|
||||
|
||||
|
||||
def measurement(value):
|
||||
"""Keep JSON standard-compliant without losing the reason for a null value."""
|
||||
result = {"value": value if math.isfinite(value) else None, "finite": math.isfinite(value)}
|
||||
if not result["finite"]:
|
||||
result["nonfinite_value"] = "nan" if math.isnan(value) else ("+inf" if value > 0 else "-inf")
|
||||
return result
|
||||
|
||||
|
||||
def read_metadata(path):
|
||||
result = {}
|
||||
for line in path.read_text().splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
if "=" not in line:
|
||||
raise ValueError(f"Malformed metadata line in {path}: {line!r}")
|
||||
key, value = line.split("=", 1)
|
||||
if not key or key in result:
|
||||
raise ValueError(f"Empty or duplicate metadata key in {path}: {key!r}")
|
||||
result[key] = value
|
||||
if result.get("converged") not in ("0", "1"):
|
||||
raise ValueError("metadata.txt must contain a completed converged=0 or converged=1 result")
|
||||
if "accepted_steps" not in result or int(result["accepted_steps"]) < 0:
|
||||
raise ValueError("metadata.txt must contain a nonnegative accepted_steps count")
|
||||
return result
|
||||
|
||||
|
||||
def read_physical(path):
|
||||
result = {q: {} for q in QUADRATURES}
|
||||
with path.open(newline="") as stream:
|
||||
reader = csv.DictReader(stream)
|
||||
if reader.fieldnames != ["quadrature", "metric", "value"]:
|
||||
raise ValueError("final_physical.csv requires the header quadrature,metric,value")
|
||||
for row in reader:
|
||||
if None in row or any(value is None for value in row.values()):
|
||||
raise ValueError("Malformed row in final_physical.csv")
|
||||
q = int(row["quadrature"])
|
||||
if q not in result:
|
||||
continue
|
||||
key = row["metric"]
|
||||
if not key or key in result[q]:
|
||||
raise ValueError(f"Empty or duplicate physical metric at quadrature {q}: {key!r}")
|
||||
result[q][key] = float(row["value"])
|
||||
for q, values in result.items():
|
||||
missing = [key for key in REQUIRED_METRICS if key not in values]
|
||||
if missing:
|
||||
raise ValueError(f"Missing quadrature {q} endpoint metrics: {', '.join(missing)}")
|
||||
for key in ("complete_volume_measurement", "pressure_from_density_defined_everywhere",
|
||||
"pressure_from_enthalpy_defined_everywhere"):
|
||||
if values[key] not in (0.0, 1.0):
|
||||
raise ValueError(f"Invalid boolean metric at quadrature {q}: {key}")
|
||||
for key in VIRIALS.values():
|
||||
if math.isfinite(values[key]) and values[key] < 0:
|
||||
raise ValueError(f"A virial error must be nonnegative: quadrature {q}, {key}")
|
||||
return result
|
||||
|
||||
|
||||
def pressure_report(values, kind):
|
||||
flag = values[f"pressure_from_{kind}_defined_everywhere"] == 1.0
|
||||
integral = values["pressure_integral" if kind == "density" else "enthalpy_pressure_integral"]
|
||||
return {
|
||||
"defined_at_all_samples": flag,
|
||||
"integral": measurement(integral),
|
||||
"valid": flag and math.isfinite(integral),
|
||||
}
|
||||
|
||||
|
||||
def negative_samples_detected(values, kind):
|
||||
minimum = values[f"minimum_{kind}"]
|
||||
count = values[f"negative_{kind}_samples"]
|
||||
if not math.isfinite(minimum) or not math.isfinite(count):
|
||||
return None
|
||||
return minimum < 0.0 or count > 0.0
|
||||
|
||||
|
||||
def assess(folder):
|
||||
meta = read_metadata(folder / "metadata.txt")
|
||||
physical = read_physical(folder / "final_physical.csv")
|
||||
per_quadrature = {}
|
||||
all_virials_pass = True
|
||||
for q, values in physical.items():
|
||||
complete = values["complete_volume_measurement"] == 1.0
|
||||
pressure = {kind: pressure_report(values, kind) for kind in ("enthalpy", "density")}
|
||||
virials = {}
|
||||
for name, key in VIRIALS.items():
|
||||
value = values[key]
|
||||
kind = "density" if "density" in name else "enthalpy"
|
||||
check = measurement(value)
|
||||
if not complete or not pressure[kind]["valid"] or not math.isfinite(value):
|
||||
check["status"] = "undefined"
|
||||
check["reason"] = (
|
||||
"incomplete_volume_measurement" if not complete else
|
||||
"pressure_not_defined_or_finite" if not pressure[kind]["valid"] else
|
||||
"nonfinite_virial"
|
||||
)
|
||||
else:
|
||||
check["status"] = "pass" if value <= THRESHOLD else "fail"
|
||||
all_virials_pass = all_virials_pass and check["status"] == "pass"
|
||||
virials[name] = check
|
||||
per_quadrature[str(q)] = {
|
||||
"complete_volume_measurement": complete,
|
||||
"virials": virials,
|
||||
"pressure": pressure,
|
||||
"sampled_signs": {
|
||||
"negative_density_detected": negative_samples_detected(values, "density"),
|
||||
"negative_enthalpy_detected": negative_samples_detected(values, "enthalpy"),
|
||||
**{key: measurement(values[key]) for key in SIGN_METRICS},
|
||||
},
|
||||
"conservation": {key: measurement(values[key]) for key in CONSERVATION_METRICS},
|
||||
"local_measurements_without_acceptance_tolerances": {
|
||||
key: measurement(values[key]) for key in LOCAL_METRICS
|
||||
},
|
||||
}
|
||||
strict_converged = meta["converged"] == "1"
|
||||
passed = strict_converged and all_virials_pass
|
||||
report = {
|
||||
"schema_version": 1,
|
||||
"result_folder": str(folder.resolve()),
|
||||
"solver_and_virial_milestone": "pass" if passed else "fail",
|
||||
"strict_solver": {
|
||||
"converged": strict_converged,
|
||||
"accepted_steps": int(meta["accepted_steps"]),
|
||||
"failure": meta.get("failure"),
|
||||
"absolute_tolerance": meta.get("atol"),
|
||||
"relative_tolerance": meta.get("rtol"),
|
||||
},
|
||||
"virial_threshold": THRESHOLD,
|
||||
"all_eight_virial_checks_pass": all_virials_pass,
|
||||
"quadratures": per_quadrature,
|
||||
"interpretation": [
|
||||
"The milestone requires strict recorded solver convergence and all four defined, finite virial errors <= 1e-5 at both quadratures.",
|
||||
"The milestone is not full physical validation. Mass/J errors and local EOS/Bernoulli measurements have no acceptance tolerances here.",
|
||||
"Density and enthalpy signs are quadrature samples; sampled extrema and local errors are not certified pointwise bounds.",
|
||||
"An undefined pressure-from-density virial is kept separate from a finite virial that exceeds the threshold.",
|
||||
],
|
||||
}
|
||||
return report, 0 if passed else 3
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("result_folder", type=Path)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
report, code = assess(args.result_folder)
|
||||
except (OSError, ValueError, csv.Error) as error:
|
||||
report, code = {"schema_version": 1, "input_error": str(error)}, 2
|
||||
print(json.dumps(report, indent=2, allow_nan=False))
|
||||
return code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
30
experiments/check_plateau_physics_README.md
Normal file
30
experiments/check_plateau_physics_README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Plateau endpoint milestone checker
|
||||
|
||||
Run the standalone standard-library script with one completed result folder:
|
||||
|
||||
```sh
|
||||
python3 experiments/check_plateau_physics.py /absolute/path/to/plateau-result
|
||||
```
|
||||
|
||||
It reads `metadata.txt` and `final_physical.csv` and prints standard JSON. Exit codes:
|
||||
|
||||
- **0:** `solver_and_virial_milestone=pass`: recorded strict solver convergence and all eight defined, finite virial errors at or below 1e-5.
|
||||
- **3:** those milestone checks do not all pass, including an undefined pressure integral.
|
||||
- **2:** missing or malformed required endpoint data, including an incomplete quadrature table or missing convergence outcome.
|
||||
|
||||
The eight checks comprise energy and force virials with both P(h) and P(rho), each at quadrature orders 14 and 18. An undefined check is distinguished from a finite check exceeding the threshold. Pressure validity, density/enthalpy signs, negative-density budgets, mass/J errors, and local EOS/Bernoulli measurements are reported separately. Nonfinite numbers become JSON null with a retained finite flag and NaN/infinity classification.
|
||||
|
||||
Passing this gate is **not full physical validation**. No new tolerance is assigned to mass/J or local errors. Sampled signs and extrema are not certified pointwise bounds. The script trusts the diagnostic's recorded convergence flag and physical measurements; it does not reconstruct a model, validate provenance, or independently recompute integrals. It does not inspect or modify production code.
|
||||
|
||||
Verification on the September 16 campaign:
|
||||
|
||||
| Completed case | Expected and observed exit | Distinction checked |
|
||||
| --- | ---: | --- |
|
||||
| n=1, J=0.05, no central constraint, restart 160 | 0 | Strict convergence and all virials pass |
|
||||
| Same model, restart 40 | 3 | Virials pass, strict convergence fails |
|
||||
| n=1.5, J=0, no central constraint, restart 160 | 3 | P(h) virials pass; P(rho) is undefined |
|
||||
| n=3 initial seed | 3 | P(h) virials exceed the threshold |
|
||||
|
||||
Threshold equality, one virial just above threshold, and missing required data
|
||||
were also checked. Detailed verification JSON and reports remain with the raw
|
||||
campaign artifacts in the audit workspace.
|
||||
@@ -129,3 +129,9 @@ folders should remain outside source commits; retain compact result tables and
|
||||
reproduction instructions with each hypothesis branch. The controls omit the
|
||||
central phase at n=1 and n=1.5. A separate n=3 control retains it to fix homology;
|
||||
the production model API does not insert it implicitly.
|
||||
|
||||
Check a completed endpoint with
|
||||
`python3 experiments/check_plateau_physics.py /absolute/path/to/result-folder`.
|
||||
This gates strict recorded solver convergence and all eight finite virial errors
|
||||
at 1e-5, while reporting local field errors separately. See
|
||||
`check_plateau_physics_README.md` for the exit codes and interpretation limits.
|
||||
|
||||
@@ -125,7 +125,10 @@ def main():
|
||||
state_files = {}
|
||||
for i, word in enumerate(case['arguments']):
|
||||
if word == '--probe-state':
|
||||
path = Path(case['arguments'][i + 1]).resolve()
|
||||
path = Path(case['arguments'][i + 1])
|
||||
if not path.is_absolute():
|
||||
path = source / path
|
||||
path = path.resolve()
|
||||
state_files[str(path)] = digest(path)
|
||||
print('START', name, flush=True)
|
||||
start = time.monotonic()
|
||||
@@ -140,6 +143,11 @@ def main():
|
||||
record['timed_out'] = True
|
||||
record['exit_code'] = None
|
||||
record.update(seconds=time.monotonic() - start, finished_utc=utc())
|
||||
record['output_hashes'] = {
|
||||
name: digest(folder / name)
|
||||
for name in ('accepted_state.txt', 'layout.csv', 'metadata.txt', 'final_physical.csv')
|
||||
if (folder / name).is_file()
|
||||
}
|
||||
write_json(output / (name + '_run.json'), record)
|
||||
summaries.append({**record, **summarize(folder)})
|
||||
write_json(output / 'summary.json', summaries)
|
||||
|
||||
Reference in New Issue
Block a user