zeechimp's picture
Create cli.py
7fad806 verified
Raw History Blame Contribute Delete
4.6 kB
"""
Command-line interface for numerical-provenance.
Usage:
numerical-provenance --demo # run the full demo
numerical-provenance --selftest # run the regression checks
numerical-provenance --strict --demo # abort on self-test failure
"""
from __future__ import annotations
import argparse
import sys
from .model import (
jacobi_short,
jacobi_converged,
jacobi_adaptive,
jacobi_loose,
cross_check,
PValue,
Disagreement,
report,
assert_converged,
UnconvergedError,
_build_reference_matrix,
_section,
)
def _selftest(verbose: bool = True) -> tuple:
"""Verify the discipline holds. Returns (passed, total)."""
checks = []
A = _build_reference_matrix(n=20)
# 1. Converged solver converges
conv = jacobi_converged(A)
checks.append(("jacobi_converged converges", conv.converged))
# 2. Short solver does not
short = jacobi_short(A)
checks.append(("jacobi_short does not converge", not short.converged))
# 3. Arithmetic propagates taint
mixed = (short + 5.0) / 2.0
checks.append(("arithmetic propagates taint", not mixed.converged))
# 4. report refuses unconverged
r = report(short)
checks.append(("report refuses unconverged", "<refused" in r))
# 5. report accepts converged
r = report(conv)
checks.append(("report accepts converged", "[ok:" in r))
# 6. unwrap refuses without allow
try:
short.unwrap()
checks.append(("unwrap refuses unconverged", False))
except UnconvergedError:
checks.append(("unwrap refuses unconverged", True))
# 7. unwrap allows with opt-in
try:
short.unwrap(allow_unconverged=True)
checks.append(("unwrap allows with opt-in", True))
except Exception:
checks.append(("unwrap allows with opt-in", False))
# 8. cross_check on two converged agrees
result = cross_check([jacobi_converged, jacobi_adaptive], A, tol=1e-6)
checks.append(("cross_check agrees on two converged",
isinstance(result, PValue)))
# 9. cross_check on mixed returns Disagreement
result = cross_check([jacobi_short, jacobi_converged], A, tol=1e-6)
checks.append(("cross_check disagrees on mixed",
isinstance(result, Disagreement)))
# 10. outlier identifies the unconverged solver
if isinstance(result, Disagreement):
_, name, dist = result.outlier()
checks.append(("outlier is jacobi_short", name == "jacobi_short"))
else:
checks.append(("outlier is jacobi_short", False))
# 11. cross_check on tolerance-mismatched pair
result = cross_check([jacobi_converged, jacobi_loose], A, tol=1e-9)
if isinstance(result, Disagreement):
_, name, _ = result.outlier()
checks.append(("outlier is jacobi_loose", name == "jacobi_loose"))
else:
checks.append(("outlier is jacobi_loose", False))
# 12. assert_converged raises on unconverged
try:
assert_converged(short, "test")
checks.append(("assert_converged raises", False))
except AssertionError:
checks.append(("assert_converged raises", True))
passed = sum(1 for _, ok in checks if ok)
if verbose:
print()
print("=" * 74)
print("SELF-TEST")
print("=" * 74)
for name, ok in checks:
print(f" [{'PASS' if ok else 'FAIL'}] {name}")
print()
print(f" {passed}/{len(checks)} correct")
return passed, len(checks)
def main(argv=None) -> int:
p = argparse.ArgumentParser(
prog="numerical-provenance",
description="Every numeric output carries its convergence state.",
)
p.add_argument("--demo", action="store_true",
help="run the full demo")
p.add_argument("--selftest", action="store_true",
help="run the regression checks and exit")
p.add_argument("--strict", action="store_true",
help="abort with exit code 1 if the self-test fails")
p.add_argument("--quiet", action="store_true",
help="suppress the self-test banner")
args = p.parse_args(argv)
passed, total = _selftest(verbose=not args.quiet)
if args.selftest:
return 0 if passed == total else 1
if args.strict and passed < total:
print("\n--strict: self-test failed; aborting.", file=sys.stderr)
return 1
if args.demo:
from .model import demo
demo()
return 0
p.print_help()
return 0
if __name__ == "__main__":
sys.exit(main())