Validate compact gravity application probe records
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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",))
|
||||
|
||||
Reference in New Issue
Block a user