Validate compact gravity application probe records

This commit is contained in:
2026-09-17 10:23:43 -04:00
parent 0bf260f04d
commit 01169861c6
4 changed files with 164 additions and 6 deletions

View File

@@ -20,7 +20,7 @@ The direct inverse capability recognizes the standard normalized physical precon
`tests/solver/gravity_compact_applications.cpp` compares compact and explicit root-reference actions for nontrivial directions, nonuniform row/state factors, and both standard and optional-central-border typed forms. It checks factor refresh, stale root and child inverse rejection, unknown inverse fallback, and collective fallback when only rank zero supplies a noncanonical map. Existing generic tests retain empty-local-rank and true-defect failure coverage.
`plateau_gravity_application_diagnostics.hpp` supplies `plateau_diagnostics::ProbeGravityApplications(subsystem, outputDirectory)`. Call after preparing the full normalized operator, refreshing its inverse, and preparing/refreshing the subsystem. It writes `gravity_applications.csv` with three deterministic nontrivial global directions, selected paths, relative/absolute compact-versus-full errors, and separate application times. It neither prepares a candidate nor changes accepted state. The reference policy also allows direct solve comparisons outside the probe.
`plateau_gravity_application_diagnostics.hpp` supplies `plateau_diagnostics::ProbeGravityApplications(subsystem, outputDirectory)`. Call after preparing the full normalized operator, refreshing its inverse, and preparing/refreshing the subsystem. It writes `gravity_applications.csv` with three deterministic nontrivial global directions, selected paths, relative/absolute compact-versus-full errors, and separate application times. It neither prepares a candidate nor changes accepted state. The reference policy also allows direct solve comparisons outside the probe. `check_numerical_strategies.py` automatically checks this CSV when present; `--require applications` explicitly requires it. Its default-fixture operator/inverse comparison uses `error <= 2e-12 * max(1, reference_norm)` and rejects nonfinite measurements. Fallback paths are reported without failing selection.
Recommended bounded real checks: run the helper once on the frozen n=1.5 fixture and once on a fixture with the central-density border; repeat the smaller viable fixture on two ranks. Require operator relative error near floating-point roundoff, then inspect inverse equivalence and timings separately. Run both compact and reference inner solves at the same true-defect tolerance before interpreting speed or iteration changes. The frozen strategy probe's existing inner report already carries operator/inverse application times; expose those alongside `UsesCompactOperator()` and `UsesCompactPreconditioner()` when recording campaign results.

View File

@@ -15,6 +15,7 @@ import sys
EPSILON = sys.float_info.epsilon
APPLICATION_AGREEMENT_TOLERANCE = 2.0e-12
SIGN_STATUSES = {"certified_nonnegative", "unresolved", "negative", "non_finite"}
@@ -139,6 +140,57 @@ class Assessment:
("accepted_physical_difference", "accepted_normalized_difference", "accepted_residual_difference", "restored_correction_difference"),
("restored_residual_difference",))
def applications(self):
fields = ("direction", "compact_operator", "compact_inverse",
"operator_norm", "operator_error", "operator_relative_error",
"compact_operator_seconds", "root_operator_seconds",
"inverse_norm", "inverse_error", "inverse_relative_error",
"compact_inverse_seconds", "root_inverse_seconds")
rows = read_rows(self.folder / "gravity_applications.csv", fields)
outcome = {"recorded_directions": len(rows), "measurements": [],
"relative_tolerance": APPLICATION_AGREEMENT_TOLERANCE,
"absolute_floor": APPLICATION_AGREEMENT_TOLERANCE,
"inverse_equivalence_assumption": "Default independent material/gravity recipe; not a universal requirement for custom compact inverses"}
self.outcomes["gravity_applications"] = outcome
if not rows:
self.incomplete("gravity_applications_available", reason="No application directions recorded")
return
directions = set()
paths = set()
for row in rows:
direction = int(row["direction"])
if direction < 0 or direction in directions:
raise ValueError("Gravity application direction indices must be unique and nonnegative")
directions.add(direction)
compact_operator = flag(row, "compact_operator")
compact_inverse = flag(row, "compact_inverse")
paths.add((compact_operator, compact_inverse))
measurement = {"direction": direction,
"operator_path": "compact" if compact_operator else "root_restriction",
"inverse_path": "compact" if compact_inverse else "root_restriction"}
for kind in ("operator", "inverse"):
prefix = f"application_{direction}_{kind}"
scale, error, relative = (number(row, f"{kind}_{suffix}") for suffix in ("norm", "error", "relative_error"))
actual_seconds, reference_seconds = (number(row, f"{path}_{kind}_seconds") for path in ("compact", "root"))
self.check(f"{prefix}_finite_nonnegative", all(nonnegative(value) for value in
(scale, error, relative, actual_seconds, reference_seconds)))
# Match the real-FEM fixture's error/max(1, reference norm)
# comparison. Near a null action this is an absolute floor;
# a large reported relative error must not replace this gate.
tolerance = APPLICATION_AGREEMENT_TOLERANCE * max(1.0, scale)
self.check(f"{prefix}_same_action", nonnegative(scale) and nonnegative(error) and error <= tolerance,
error=error, reference_norm=scale, allowed=tolerance)
expected_relative = error / scale if scale > 0.0 else (0.0 if error == 0.0 else math.inf)
self.check(f"{prefix}_relative_error_arithmetic", nonnegative(relative) and
math.isclose(relative, expected_relative, rel_tol=1e-12, abs_tol=1e-30))
measurement[kind] = {"reference_norm": scale, "error": error, "relative_error": relative,
"allowed": tolerance, "selected_seconds": actual_seconds,
"root_seconds": reference_seconds}
outcome["measurements"].append(measurement)
self.check("gravity_application_paths_consistent", len(paths) == 1)
outcome["compact_operator_directions"] = sum(flag(row, "compact_operator") for row in rows)
outcome["compact_inverse_directions"] = sum(flag(row, "compact_inverse") for row in rows)
def gravity(self):
summary = metrics(self.folder / "numerical_strategy_summary.csv")
self.restore(summary, ("accepted_state_change", "accepted_residual_change", "accepted_physical_change"),
@@ -277,14 +329,20 @@ def assess(folder, required=(), expect_negative=False, restoration_tolerance=0.0
sections.add("polynomial")
if (folder / "numerical_strategy_summary.csv").exists():
sections.add("gravity")
if (folder / "gravity_applications.csv").exists():
sections.add("applications")
if not sections:
assessment.incomplete("probe_artifacts_available", reason="No recognized frozen strategy outputs")
for section in sorted(sections):
try:
if section == "polynomial":
assessment.polynomial(expect_negative)
else:
elif section == "gravity":
assessment.gravity()
elif section == "applications":
assessment.applications()
else:
raise ValueError(f"Unknown required probe: {section}")
except FileNotFoundError as error:
assessment.incomplete(f"{section}_artifacts_complete", error=str(error))
except KeyError as error:
@@ -302,7 +360,7 @@ def assess(folder, required=(), expect_negative=False, restoration_tolerance=0.0
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("result_folder", type=Path)
parser.add_argument("--require", choices=("polynomial", "gravity"), action="append", default=[])
parser.add_argument("--require", choices=("polynomial", "gravity", "applications"), action="append", default=[])
parser.add_argument("--expect-negative-base", action="store_true")
parser.add_argument("--restoration-tolerance", type=float, default=0.0)
args = parser.parse_args()

View File

@@ -4,13 +4,16 @@ Run the checker on each completed frozen-probe directory:
```sh
python3 experiments/check_numerical_strategies.py RESULT_FOLDER --require gravity
python3 experiments/check_numerical_strategies.py RESULT_FOLDER --require applications
python3 experiments/check_numerical_strategies.py RESULT_FOLDER --require polynomial
python3 experiments/check_numerical_strategies.py LATE_ROTATING_FOLDER --require polynomial --expect-negative-base
python3 experiments/test_check_numerical_strategies.py
```
Both sections are discovered automatically when their CSV files exist. Repeating
`--require` demands both sections. Exit 0 means the requested checks passed;
Polynomial, gravity-strategy, and gravity-application sections are discovered
automatically when their CSV files exist. Repeating `--require` demands each
named section. Older campaigns without `gravity_applications.csv` remain valid
unless `--require applications` is supplied. Exit 0 means the requested checks passed;
3 means an invariant or numerical expectation failed; 2 means evidence is
incomplete. An honestly reported failed inner solve, unavailable trial, or
unresolved certificate must not turn into a passing verification. Expected
@@ -40,6 +43,26 @@ evidence rather than an automatic claim of a bad Jacobian: the retained-residual
amplification of the remaining gravity error has not been bounded. The recorded
roundoff scales are heuristics, not rigorous forward-error bounds.
When `gravity_applications.csv` is present, every recorded direction must satisfy
compact-versus-root normalized operator agreement and the default fixture's
inverse agreement. The gate matches the real-FEM test:
`error <= 2e-12 * max(1, reference_norm)`, a relative tolerance with an absolute
floor for small actions. Norms, absolute/relative errors, and both application
times must be finite and nonnegative; relative-error columns must also agree
with the absolute measurements. The reported compact/fallback flags must be
valid and consistent across this one prepared subsystem's directions. Fallback
is a valid reported outcome, including for custom recipes. An empty set of
directions or a missing explicitly required file is incomplete evidence.
Inverse equality is an expectation for the default independent material/gravity
recipe used by these fixtures, not a mathematical requirement on every possible
custom compact preconditioner. A custom inverse deliberately different from the
root restriction needs its own comparison expectation; this checker must not
be cited as accepting that difference. Operator equality always remains required
because both paths must apply the same equations and normalization. The JSON
retains the selected paths and separate application times without imposing a
speedup threshold.
This script deliberately does not duplicate physical acceptance. For each
end-to-end model, run `check_plateau_physics.py` on its endpoint. That separate
gate requires recorded strict nonlinear convergence and all eight defined virial
@@ -54,7 +77,7 @@ Use one numerical process at a time and preserve all capped failures as results.
| Layer | Cases | Bound / acceptance |
|---|---|---|
| Algebra and lifecycle | Gravity subsystem, Schur reduction, polynomial certificate tests; one and two MPI ranks | 2 minutes per test group; true-defect, stale-state, empty-local, rollback, type-contract coverage |
| Python checker | Synthetic passing, false-convergence, mutation, missing-data, negative-base, and NaN records | 30 seconds; standard-library unit tests |
| Python checker | Synthetic passing, false-convergence, mutation, missing-data, negative-base, compact/fallback agreement, and NaN records | 30 seconds; standard-library unit tests |
| Positivity frozen states | n=1 J=0.1 captured iteration 2 and iteration 11; a certified seed/control | 10 minutes per probe; iteration 11 must be negative and reject repair with `invalid_base`; no claim that the earlier base is positive before checking it |
| Gravity curvature | n=1.5 J=0 captured iteration 1 | 15 minutes; alpha = 1, 0.1, 0.01, 0.001; gate fresh remainder cancellation and small-step order |
| Reduced gravity | Same n=1.5 state, plus constrained n=3 saved seed direction | 15 minutes per probe; completion and three central-difference pairs; n=3 direction remains an approximate diagnostic correction |

View File

@@ -80,6 +80,17 @@ def polynomial_fixture(folder):
return certificates, repair, trials
def application_fixture(folder):
rows = [{"direction": index, "compact_operator": 1, "compact_inverse": 1,
"operator_norm": 5.0, "operator_error": 5e-15, "operator_relative_error": 1e-15,
"compact_operator_seconds": .001, "root_operator_seconds": .2,
"inverse_norm": 0.25, "inverse_error": 2.5e-16, "inverse_relative_error": 1e-15,
"compact_inverse_seconds": .004, "root_inverse_seconds": .01}
for index in range(3)]
write_rows(folder, "gravity_applications.csv", rows)
return rows
class NumericalStrategyChecks(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory()
@@ -88,6 +99,72 @@ class NumericalStrategyChecks(unittest.TestCase):
def tearDown(self):
self.temporary.cleanup()
def test_application_evidence_is_discovered_and_passes(self):
application_fixture(self.folder)
report, code = assess(self.folder)
self.assertEqual(code, 0, report)
self.assertEqual(report["outcomes"]["gravity_applications"]["recorded_directions"], 3)
self.assertEqual(report["outcomes"]["gravity_applications"]["compact_operator_directions"], 3)
def test_application_fallback_is_a_valid_outcome(self):
rows = application_fixture(self.folder)
for row in rows:
row["compact_operator"], row["compact_inverse"] = 0, 0
write_rows(self.folder, "gravity_applications.csv", rows)
report, code = assess(self.folder, required=("applications",))
self.assertEqual(code, 0, report)
outcome = report["outcomes"]["gravity_applications"]
self.assertEqual(outcome["compact_operator_directions"], 0)
self.assertEqual(outcome["compact_inverse_directions"], 0)
self.assertEqual(outcome["measurements"][0]["operator_path"], "root_restriction")
def test_wrong_operator_or_default_inverse_application_fails(self):
for kind in ("operator", "inverse"):
with self.subTest(kind=kind):
rows = application_fixture(self.folder)
rows[0][f"{kind}_error"] = 1e-5
rows[0][f"{kind}_relative_error"] = 1e-5 / rows[0][f"{kind}_norm"]
write_rows(self.folder, "gravity_applications.csv", rows)
report, code = assess(self.folder)
self.assertEqual(code, 3, report)
self.assertIn(f"application_0_{kind}_same_action",
[check["name"] for check in report["checks"] if check["status"] == "fail"])
def test_application_nan_timing_and_flags_cannot_pass(self):
for field, value in (("operator_error", float("nan")), ("inverse_relative_error", float("nan")),
("root_operator_seconds", float("inf")), ("compact_inverse_seconds", -1),
("compact_operator", "true")):
with self.subTest(field=field):
rows = application_fixture(self.folder)
rows[0][field] = value
write_rows(self.folder, "gravity_applications.csv", rows)
report, code = assess(self.folder)
self.assertEqual(code, 3, report)
json.dumps(report, allow_nan=False)
def test_application_relative_error_must_match_absolute_measurement(self):
rows = application_fixture(self.folder)
rows[0]["operator_relative_error"] = 1e-6
write_rows(self.folder, "gravity_applications.csv", rows)
report, code = assess(self.folder)
self.assertEqual(code, 3, report)
def test_required_application_missing_or_empty_is_incomplete(self):
self.assertEqual(assess(self.folder, required=("applications",))[1], 2)
rows = application_fixture(self.folder)
write_rows(self.folder, "gravity_applications.csv", [], fields=list(rows[0]))
self.assertEqual(assess(self.folder, required=("applications",))[1], 2)
def test_application_absolute_floor_handles_small_actions(self):
rows = application_fixture(self.folder)
for row in rows:
row["operator_norm"] = 1e-8
row["operator_error"] = 1e-13
row["operator_relative_error"] = 1e-5
write_rows(self.folder, "gravity_applications.csv", rows)
report, code = assess(self.folder)
self.assertEqual(code, 0, report)
def test_complete_gravity_evidence_passes(self):
gravity_fixture(self.folder)
report, code = assess(self.folder, required=("gravity",))