physverify 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. physverify/__init__.py +26 -0
  2. physverify/__main__.py +3 -0
  3. physverify/adapters/__init__.py +14 -0
  4. physverify/adapters/double_pendulum.py +55 -0
  5. physverify/adapters/lorenz.py +152 -0
  6. physverify/adapters/projectile_v1.py +92 -0
  7. physverify/adapters/projectile_v2.py +94 -0
  8. physverify/adapters/three_body.py +283 -0
  9. physverify/agent.py +265 -0
  10. physverify/checks.py +59 -0
  11. physverify/cli.py +105 -0
  12. physverify/compare.py +104 -0
  13. physverify/loaders.py +49 -0
  14. physverify/matrix.py +160 -0
  15. physverify/mms.py +118 -0
  16. physverify/order.py +131 -0
  17. physverify/problems/__init__.py +6 -0
  18. physverify/problems/_common.py +89 -0
  19. physverify/problems/heat1d.py +217 -0
  20. physverify/problems/kepler.py +176 -0
  21. physverify/problems/pendulum.py +201 -0
  22. physverify/problems/projectile.py +240 -0
  23. physverify/problems/sod.py +291 -0
  24. physverify/pytest_plugin.py +75 -0
  25. physverify/report.py +189 -0
  26. physverify/steppers.py +38 -0
  27. physverify/stochastic/__init__.py +1 -0
  28. physverify/stochastic/calibrate.py +202 -0
  29. physverify/stochastic/checks.py +378 -0
  30. physverify/stochastic/exact.py +205 -0
  31. physverify/stochastic/ising.py +170 -0
  32. physverify/stochastic/matrix.py +151 -0
  33. physverify/stochastic/reference.py +67 -0
  34. physverify/stochastic/sampler.py +253 -0
  35. physverify/stochastic/stats.py +137 -0
  36. physverify/studies.py +146 -0
  37. physverify/symbolic.py +89 -0
  38. physverify-0.1.0.dist-info/METADATA +122 -0
  39. physverify-0.1.0.dist-info/RECORD +43 -0
  40. physverify-0.1.0.dist-info/WHEEL +5 -0
  41. physverify-0.1.0.dist-info/entry_points.txt +5 -0
  42. physverify-0.1.0.dist-info/licenses/LICENSE +21 -0
  43. physverify-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,137 @@
1
+ """Statistics for Monte Carlo checks: error bars, autocorrelation and verdicts.
2
+
3
+ Design (docs/plan_ising.md, section 4):
4
+
5
+ * Every estimate is a mean over independent chains (each chain is burned in and
6
+ then averaged). Chains are independent by construction, so the standard error
7
+ is the spread of the chain means over sqrt(n_chains), with no
8
+ autocorrelation model needed. Within one chain, `tau_int` and `blocked_error`
9
+ give the error bar for codes that report one.
10
+ * A check makes m comparisons with exact values. Each uses a two-sided Student-t
11
+ test with n_chains - 1 degrees of freedom at level alpha / m (Bonferroni),
12
+ so the false-alarm rate of a whole check is at most alpha.
13
+ * Verdicts: FAIL when some comparison is significant; INCONCLUSIVE when none is,
14
+ but the test could not have detected a deviation as large as the stated
15
+ resolution (critical value times standard error > resolution); PASS otherwise.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import math
21
+ from dataclasses import dataclass
22
+ from functools import lru_cache
23
+
24
+ import numpy as np
25
+
26
+ from ..checks import FAIL, INCONCLUSIVE, PASS, Result
27
+
28
+ ALPHA_CHECK = 1e-3 # false-alarm rate of one statistical check
29
+
30
+
31
+ def _t_pdf(x, nu):
32
+ c = math.lgamma((nu + 1) / 2) - math.lgamma(nu / 2) - 0.5 * math.log(nu * math.pi)
33
+ return np.exp(c - (nu + 1) / 2 * np.log1p(x * x / nu))
34
+
35
+
36
+ def t_sf(x: float, nu: float) -> float:
37
+ """P(T > x) for Student's t with nu degrees of freedom (x >= 0), by Simpson's rule on the pdf."""
38
+ x = abs(float(x))
39
+ if nu > 1e6:
40
+ return 0.5 * math.erfc(x / math.sqrt(2))
41
+ n = 4000
42
+ grid = np.linspace(0.0, x, n + 1)
43
+ w = np.ones(n + 1)
44
+ w[1:-1:2], w[2:-1:2] = 4, 2
45
+ integral = float((w * _t_pdf(grid, nu)).sum() * x / (3 * n))
46
+ return max(0.0, 0.5 - integral)
47
+
48
+
49
+ @lru_cache(maxsize=4096)
50
+ def t_critical(alpha_two_sided: float, nu: float) -> float:
51
+ """x with P(|T| > x) = alpha, by bisection (cached: checks ask for the same few values)."""
52
+ lo, hi = 0.0, 50.0
53
+ for _ in range(80):
54
+ mid = 0.5 * (lo + hi)
55
+ if 2 * t_sf(mid, nu) > alpha_two_sided:
56
+ lo = mid
57
+ else:
58
+ hi = mid
59
+ return 0.5 * (lo + hi)
60
+
61
+
62
+ def tau_int(series, c: float = 6.0) -> float:
63
+ """Integrated autocorrelation time with Sokal's automatic window (window W >= c * tau)."""
64
+ x = np.asarray(series, dtype=float)
65
+ x = x - x.mean()
66
+ n = len(x)
67
+ var = float(np.dot(x, x)) / n
68
+ if n < 4 or var == 0.0:
69
+ return 0.5
70
+ f = np.fft.rfft(x, 2 * n)
71
+ acf = np.fft.irfft(f * np.conj(f))[:n] / (n * var)
72
+ tau = 0.5
73
+ for w in range(1, n):
74
+ tau += acf[w]
75
+ if w >= c * tau:
76
+ break
77
+ return max(tau, 0.5)
78
+
79
+
80
+ def blocked_error(series) -> float:
81
+ """Standard error of the mean of a correlated series: sqrt(2 tau var / n)."""
82
+ x = np.asarray(series, dtype=float)
83
+ return float(np.sqrt(2.0 * tau_int(x) * x.var() / len(x)))
84
+
85
+
86
+ @dataclass
87
+ class Comparison:
88
+ name: str
89
+ estimate: float
90
+ se: float
91
+ exact: float
92
+ resolution: float
93
+ dof: float
94
+
95
+
96
+ def compare_chain_means(name, per_chain_values, exact, resolution, min_se=0.0) -> Comparison:
97
+ """Mean over independent chains, with the spread of the chain means as its standard error.
98
+
99
+ `min_se` is a floor for the standard error: for the frequency of a rare event the
100
+ chain-to-chain spread can be zero when the event never happens, which would make
101
+ any small expected probability look like a certain deviation.
102
+ """
103
+ v = np.asarray(per_chain_values, dtype=float)
104
+ se = float(v.std(ddof=1) / math.sqrt(len(v))) if len(v) > 1 else float("inf")
105
+ return Comparison(name, float(v.mean()), max(se, float(min_se)), float(exact), float(resolution), len(v) - 1)
106
+
107
+
108
+ def verdict(comparisons, alpha: float = ALPHA_CHECK) -> Result:
109
+ """Bonferroni-corrected t-tests; see the module docstring for PASS, FAIL and INCONCLUSIVE."""
110
+ m = max(len(comparisons), 1)
111
+ worst, worst_ratio, unresolved = None, -1.0, []
112
+ for c in comparisons:
113
+ if not (np.isfinite(c.estimate) and np.isfinite(c.se)):
114
+ return Result(FAIL, f"{c.name}: non-finite estimate")
115
+ crit = t_critical(alpha / m, float(c.dof))
116
+ dev = abs(c.estimate - c.exact)
117
+ if c.se == 0.0:
118
+ ratio = math.inf if dev > 1e-12 * max(1.0, abs(c.exact)) else 0.0
119
+ else:
120
+ ratio = dev / (crit * c.se)
121
+ if ratio > worst_ratio:
122
+ worst, worst_ratio = c, ratio
123
+ if crit * c.se > c.resolution:
124
+ unresolved.append(c.name)
125
+ w = worst
126
+ detail = f"{w.name} = {w.estimate:.5g} +- {w.se:.2g}, exact {w.exact:.5g}"
127
+ level = f"(Bonferroni over {m}, false-alarm rate {alpha:g})"
128
+ if worst_ratio > 1.0:
129
+ sigmas = abs(w.estimate - w.exact) / w.se if w.se > 0 else math.inf
130
+ return Result(FAIL, f"{detail}: off by {sigmas:.1f} standard errors {level}")
131
+ if unresolved:
132
+ return Result(
133
+ INCONCLUSIVE,
134
+ f"no significant deviation, but the error bars cannot resolve a deviation of the stated size for "
135
+ f"{len(unresolved)} of {m} comparisons (first: {unresolved[0]}) {level}",
136
+ )
137
+ return Result(PASS, f"all {m} comparisons agree; largest: {detail} {level}")
physverify/studies.py ADDED
@@ -0,0 +1,146 @@
1
+ """Order studies other than MMS, and a helper that bundles them for an ODE code."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Callable
6
+
7
+ import numpy as np
8
+
9
+ from .checks import FAIL, PASS, Check, OrderResult, Result
10
+ from .mms import ManufacturedODE, max_abs_error, mms_order_study, mms_trajectory_study, zero_source
11
+ from .order import check_exact, check_order
12
+ from .steppers import integrate_fixed, rk4
13
+
14
+
15
+ def reference_study(integrate, rhs_ref, y0, t_end, ns, expected_order, ref_factor=16, **kw) -> OrderResult:
16
+ """Compare with a reference solution from an independent right-hand side.
17
+
18
+ The reference is RK4 on `rhs_ref` with ref_factor times the finest step
19
+ count, so its own error is negligible next to the code under test.
20
+ """
21
+ y_ref = integrate_fixed(rhs_ref, y0, 0.0, t_end, ref_factor * max(ns), rk4)
22
+ kw.setdefault("scale", float(np.max(np.abs(y_ref))))
23
+ hs, errs = [], []
24
+ for n in ns:
25
+ hs.append(t_end / n)
26
+ errs.append(max_abs_error(integrate(zero_source, y0, t_end, n), y_ref))
27
+ return check_order(hs, errs, expected_order, **kw)
28
+
29
+
30
+ def invariant_study(integrate, quantity, y0, t_end, ns, expected_order, **kw) -> OrderResult:
31
+ """Relative drift of a conserved quantity at t_end as the step shrinks.
32
+
33
+ Drift is only required to shrink at least as fast as the scheme's order.
34
+ RK4 on a pendulum, for example, drifts in energy at about order 5, so a
35
+ drift that converges faster than expected is not a failure here.
36
+ """
37
+ kw.setdefault("tol_high", float("inf"))
38
+ q0 = float(quantity(np.asarray(y0, dtype=float)))
39
+ scale = max(abs(q0), 1.0)
40
+ hs, errs = [], []
41
+ for n in ns:
42
+ with np.errstate(all="ignore"):
43
+ y_end = np.asarray(integrate(zero_source, y0, t_end, n), dtype=float)
44
+ drift = abs(float(quantity(y_end)) - q0) / scale
45
+ hs.append(t_end / n)
46
+ errs.append(drift)
47
+ return check_order(hs, errs, expected_order, **kw)
48
+
49
+
50
+ def linear_manufactured(rhs_ref: Callable, y0, slope) -> ManufacturedODE:
51
+ """u(t) = y0 + slope * t, with its forcing taken from the reference operator."""
52
+ y0 = np.asarray(y0, dtype=float)
53
+ slope = np.asarray(slope, dtype=float)
54
+ return ManufacturedODE(lambda t: y0 + slope * t, rhs_ref, du=lambda t: slope)
55
+
56
+
57
+ def patch_study(integrate, rhs_ref, y0, slope, t_end, ns, *, floor=1e-11) -> OrderResult:
58
+ """Exactness test for an ODE code: a manufactured solution linear in t.
59
+
60
+ Every consistent explicit Runge-Kutta method (Euler, midpoint, RK4, ...)
61
+ reproduces u(t) = y0 + slope * t exactly, whatever the step: each stage
62
+ lands on u at its own stage time, so each stage derivative is exactly
63
+ `slope`, and weights that sum to one return y0 + slope * (t + h). So any
64
+ error above round-off is a bug, including bugs whose error shrinks at the
65
+ scheme's own order (an extra step for Euler, a wrong stage time) that every
66
+ order-of-convergence check accepts. See docs/patch.md.
67
+
68
+ It does not see a wrong but consistent stepper (Euler in place of RK4 is
69
+ still exact here): the order studies cover that.
70
+ """
71
+ mms = linear_manufactured(rhs_ref, y0, slope)
72
+ exact = mms.value(t_end)
73
+ hs, errs = [], []
74
+ for n in ns:
75
+ y_end = integrate(mms.forcing, mms.value(0.0), t_end, n)
76
+ hs.append(t_end / n)
77
+ errs.append(max_abs_error(y_end, exact))
78
+ return check_exact(hs, errs, floor=floor, scale=float(np.max(np.abs(exact))))
79
+
80
+
81
+ def smoke_check(label, integrate, rhs_ref, y0, t_end, n, *, rtol=1e-3) -> Check:
82
+ """What an ordinary unit test would do: one run, compared with a reference within a tolerance.
83
+
84
+ Not one of the verification families. It is here so that the benchmark can
85
+ report what a plain test would have caught (see physverify.compare).
86
+ """
87
+
88
+ def run():
89
+ y_ref = integrate_fixed(rhs_ref, y0, 0.0, t_end, 16 * n, rk4)
90
+ y = np.asarray(integrate(zero_source, y0, t_end, n), dtype=float)
91
+ return close_result(y, y_ref, rtol)
92
+
93
+ return Check("smoke", label, run)
94
+
95
+
96
+ def close_result(value, expected, rtol) -> Result:
97
+ value = np.asarray(value, dtype=float)
98
+ expected = np.asarray(expected, dtype=float)
99
+ with np.errstate(all="ignore"):
100
+ err = float(np.max(np.abs(value - expected)) / max(float(np.max(np.abs(expected))), 1e-300))
101
+ if np.isfinite(err) and err <= rtol:
102
+ return Result(PASS, f"relative difference {err:.2g} within {rtol:g}")
103
+ return Result(FAIL, f"relative difference {err:.3g} exceeds {rtol:g}")
104
+
105
+
106
+ def ode_checks(
107
+ label: str,
108
+ integrate: Callable,
109
+ *,
110
+ rhs_ref: Callable,
111
+ mms: ManufacturedODE,
112
+ y0_free,
113
+ t_end: float,
114
+ ns,
115
+ order: float,
116
+ quantity: Callable | None = None,
117
+ floor: float = 1e-12,
118
+ patch: tuple | None = None,
119
+ integrate_trajectory: Callable | None = None,
120
+ ) -> list[Check]:
121
+ """The standard bundle for an ODE code: MMS, reference solution, invariant.
122
+
123
+ patch (y0, slope) of a linear manufactured solution; adds the exactness check
124
+ integrate_trajectory same contract as `integrate` but returning every state; adds an MMS
125
+ check on the largest error over the whole run
126
+ """
127
+ checks = [
128
+ Check("mms", label, lambda: mms_order_study(integrate, mms, t_end, ns, order, floor=floor)),
129
+ Check("reference", label, lambda: reference_study(integrate, rhs_ref, y0_free, t_end, ns, order, floor=floor)),
130
+ ]
131
+ if integrate_trajectory is not None:
132
+ checks.append(
133
+ Check(
134
+ "mms",
135
+ f"{label} (whole trajectory)",
136
+ lambda: mms_trajectory_study(integrate_trajectory, mms, t_end, ns, order, floor=floor),
137
+ )
138
+ )
139
+ if quantity is not None:
140
+ checks.append(
141
+ Check("invariant", label, lambda: invariant_study(integrate, quantity, y0_free, t_end, ns, order, floor=floor))
142
+ )
143
+ if patch is not None:
144
+ p_y0, p_slope = patch
145
+ checks.append(Check("patch", label, lambda: patch_study(integrate, rhs_ref, p_y0, p_slope, t_end, ns)))
146
+ return checks
physverify/symbolic.py ADDED
@@ -0,0 +1,89 @@
1
+ """Manufactured solutions with symbolic derivatives (optional; needs SymPy).
2
+
3
+ pip install physverify[symbolic]
4
+
5
+ Hand-derived source terms are a common place for the verification itself to
6
+ go wrong. Here you write the manufactured solution and the PDE operator once,
7
+ symbolically, and the source term is derived and turned into NumPy functions.
8
+
9
+ Example, the 1D heat equation u_t = alpha u_xx + s:
10
+
11
+ import sympy as sp
12
+ x, t = sp.symbols("x t")
13
+ u = sp.exp(-t) * sp.sin(sp.pi * x)
14
+ ms = manufactured_pde(u, lambda u: sp.diff(u, t) - 0.7 * sp.diff(u, x, 2), (x, t))
15
+ ms.source(xs, 0.3) # NumPy array of s at the points xs, time 0.3
16
+
17
+ The operator must be written independently of the code under test, for the same
18
+ reason as the ODE reference operator (see physverify.mms).
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from dataclasses import dataclass
24
+ from typing import Callable
25
+
26
+ import numpy as np
27
+
28
+
29
+ def _sympy():
30
+ try:
31
+ import sympy
32
+ except ImportError as exc: # pragma: no cover - exercised only without sympy
33
+ raise ImportError("physverify.symbolic needs SymPy: pip install physverify[symbolic]") from exc
34
+ return sympy
35
+
36
+
37
+ @dataclass
38
+ class ManufacturedPDE:
39
+ solution_expr: object
40
+ source_expr: object
41
+ solution: Callable
42
+ source: Callable
43
+
44
+
45
+ def _vectorise(f, n_args):
46
+ """lambdify returns a scalar for a constant expression; broadcast it to the inputs' shape."""
47
+
48
+ def g(*args):
49
+ value = f(*args)
50
+ shape = np.broadcast(*[np.asarray(a) for a in args[:n_args]]).shape
51
+ return np.broadcast_to(np.asarray(value, dtype=float), shape).copy() if shape else float(value)
52
+
53
+ return g
54
+
55
+
56
+ def manufactured_pde(u_expr, operator: Callable, variables) -> ManufacturedPDE:
57
+ """Source s = operator(u) for a manufactured solution u.
58
+
59
+ `operator(u)` returns the PDE residual for u, written with sympy.diff, so that
60
+ u solves operator(u) = s exactly. `variables` is the tuple of SymPy symbols
61
+ the NumPy functions take, in order (for example (x, t) or (x, y, t)).
62
+ """
63
+ sp = _sympy()
64
+ variables = tuple(variables)
65
+ source_expr = sp.simplify(operator(u_expr))
66
+ n = len(variables)
67
+ return ManufacturedPDE(
68
+ u_expr,
69
+ source_expr,
70
+ _vectorise(sp.lambdify(variables, u_expr, "numpy"), n),
71
+ _vectorise(sp.lambdify(variables, source_expr, "numpy"), n),
72
+ )
73
+
74
+
75
+ def manufactured_ode(u_exprs, rhs_ref: Callable, t_symbol):
76
+ """A physverify.ManufacturedODE whose derivative is taken symbolically.
77
+
78
+ `u_exprs` is a list of SymPy expressions in `t_symbol`, one per state component.
79
+ """
80
+ from .mms import ManufacturedODE
81
+
82
+ sp = _sympy()
83
+ u_f = sp.lambdify(t_symbol, list(u_exprs), "numpy")
84
+ du_f = sp.lambdify(t_symbol, [sp.diff(e, t_symbol) for e in u_exprs], "numpy")
85
+ return ManufacturedODE(
86
+ lambda t: np.array(u_f(t), dtype=float),
87
+ rhs_ref,
88
+ du=lambda t: np.array(du_f(t), dtype=float),
89
+ )
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: physverify
3
+ Version: 0.1.0
4
+ Summary: Verification checks for physics simulation code: manufactured solutions, order-of-accuracy studies and a seeded-bug benchmark.
5
+ Author: Ram Chandra Giri
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/rcgiri-physics/physverify
8
+ Project-URL: Documentation, https://github.com/rcgiri-physics/physverify/blob/main/docs/technical.md
9
+ Project-URL: Issues, https://github.com/rcgiri-physics/physverify/issues
10
+ Project-URL: Changelog, https://github.com/rcgiri-physics/physverify/blob/main/CHANGELOG.md
11
+ Keywords: verification,method of manufactured solutions,numerical methods,physics,simulation,testing
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Scientific/Engineering :: Physics
14
+ Classifier: Framework :: Pytest
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Operating System :: OS Independent
22
+ Classifier: Intended Audience :: Science/Research
23
+ Classifier: Intended Audience :: Education
24
+ Classifier: Topic :: Software Development :: Testing
25
+ Classifier: Development Status :: 3 - Alpha
26
+ Requires-Python: >=3.10
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: numpy>=1.24
30
+ Provides-Extra: symbolic
31
+ Requires-Dist: sympy>=1.12; extra == "symbolic"
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest>=8; extra == "dev"
34
+ Requires-Dist: hypothesis>=6; extra == "dev"
35
+ Requires-Dist: sympy>=1.12; extra == "dev"
36
+ Dynamic: license-file
37
+
38
+ # physverify
39
+
40
+ **A tool that checks whether a physics simulation is solving the right equations correctly, not just running without errors.**
41
+
42
+ Status: early version (0.1.0). Nobody besides the author has used it yet, so feedback is very welcome.
43
+
44
+ ## The problem it addresses
45
+
46
+ A simulation can run, produce nice plots and still be wrong: a minus sign in the wrong place, a factor of 2 missing, a time step that is off by one. Ordinary tests often miss these, because nobody knows the "right answer" for a real simulation to compare against.
47
+
48
+ ## How it works, in plain words
49
+
50
+ physverify uses a few standard ideas from numerical analysis:
51
+
52
+ 1. **Make up an answer, then check the code finds it.** Pick a smooth function, work out what extra push (a "source term") would make it an exact solution, give that push to your code, and see if your code reproduces the function. This is called the *Method of Manufactured Solutions*.
53
+ 2. **Check the error shrinks at the right speed.** If your method is supposed to be 4th order, halving the time step should make the error about 16 times smaller. If it only gets 2 times smaller, something is wrong, even if the error looked small.
54
+ 3. **Check conserved quantities.** Energy, mass or momentum should stay constant (or drift in a predictable way).
55
+ 4. **Check known special cases.** For example, a projectile with no air resistance has a range you can write down by hand.
56
+ 5. **Exact "patch" tests.** Some solutions should be reproduced to computer precision. This catches small bugs that the other checks let through.
57
+
58
+ Each check reports **PASS**, **FAIL**, **EXACT** or **INCONCLUSIVE**.
59
+
60
+ There is also a separate set of checks for random (Monte Carlo) simulations, using the 2D Ising model as the example.
61
+
62
+ ## Does it actually catch bugs?
63
+
64
+ To test it, 37 bugs were added one at a time to five small solvers (projectile, double pendulum, planet orbit, heat equation, shock tube):
65
+
66
+ - **35 of 37** bugs were caught. Plain "compare with one answer" tests caught 27.
67
+ - **0 false alarms** on the bug-free versions.
68
+ - The 2 missed bugs are explained in the [technical details](https://github.com/rcgiri-physics/physverify/blob/main/docs/technical.md).
69
+
70
+ For the Monte Carlo checks, all 20 seeded bugs were caught with no false alarms.
71
+
72
+ The bugs and the checks were written by the same person, so these numbers show the tool works as designed. They are not a promise about bugs in other people's code.
73
+
74
+ ## Try it
75
+
76
+ You need Python 3.10 or newer.
77
+
78
+ ```bash
79
+ pip install physverify
80
+ ```
81
+
82
+ The example below lives in the repository, so clone it to follow along:
83
+
84
+ ```bash
85
+ git clone https://github.com/rcgiri-physics/physverify.git
86
+ cd physverify
87
+ physverify run examples/verify_damped_oscillator.py
88
+ ```
89
+
90
+ You should see a list of checks, all passing. Now open `examples/damped_oscillator.py`, change the damping by 10%, and run the same command again: the checks fail.
91
+
92
+ To check your own code you write a short "spec" file. It tells physverify two things: the equations your code is meant to solve (written independently of your code), and how to run your code. [examples/verify_damped_oscillator.py](https://github.com/rcgiri-physics/physverify/blob/main/examples/verify_damped_oscillator.py) is a complete example to copy.
93
+
94
+ Other ways to run it:
95
+
96
+ ```bash
97
+ physverify run my_spec.py --html report.html # a web page with convergence plots
98
+ physverify run my_spec.py --json - # machine-readable output
99
+ pytest my_spec.py # each check becomes a pytest test
100
+ ```
101
+
102
+ ## What it can and cannot do
103
+
104
+ - Works best for ordinary differential equations (orbits, pendulums, projectiles).
105
+ - Partial differential equations: only 1D examples so far (heat equation, shock tube).
106
+ - Your code needs a way to accept an extra source term, or a small wrapper ("adapter") around it. The wrapper can itself contain mistakes.
107
+ - Passing all checks does not prove the code is correct, only that these tests found nothing wrong.
108
+
109
+ ## More
110
+
111
+ - [results/own_work/SUMMARY.md](https://github.com/rcgiri-physics/physverify/blob/main/results/own_work/SUMMARY.md): results on four of the author's own public physics projects (two real bugs found and fixed).
112
+ - [docs/technical.md](https://github.com/rcgiri-physics/physverify/blob/main/docs/technical.md): full details, benchmark tables, limits and related work.
113
+ - [docs/patch.md](https://github.com/rcgiri-physics/physverify/blob/main/docs/patch.md): what the exact "patch" checks assume.
114
+ - [docs/ising.md](https://github.com/rcgiri-physics/physverify/blob/main/docs/ising.md): checks for Monte Carlo codes.
115
+ - [docs/agent.md](https://github.com/rcgiri-physics/physverify/blob/main/docs/agent.md): using it with an AI coding assistant.
116
+ - [CHANGELOG.md](https://github.com/rcgiri-physics/physverify/blob/main/CHANGELOG.md)
117
+
118
+ Run the project's own tests with `pip install -e .[dev]` then `python -m pytest -q` (about 1.5 minutes).
119
+
120
+ ## Licence
121
+
122
+ MIT. Copyright (c) 2026 Ram Chandra Giri.
@@ -0,0 +1,43 @@
1
+ physverify/__init__.py,sha256=viD63SRjqeOtmvyYstDQD-0Ybiq_V6vqAdehw1znImA,657
2
+ physverify/__main__.py,sha256=k1ocEWawweo1qCJWNFAAvyxz3tcY13dzvCenHszij30,48
3
+ physverify/agent.py,sha256=lZKvUYykji8VkFU38mtwsE9XSCKXbPwMnJDnGz4h1qk,11398
4
+ physverify/checks.py,sha256=0BmPL4WRXvcFXGIAhnrWxU284aUjsKGf3WZyb95s3Lo,1699
5
+ physverify/cli.py,sha256=-phU4181-O6lFhIOMEOP2TA7Y0wLO3oQVG-0qWmcjks,3580
6
+ physverify/compare.py,sha256=9dAfKsl5n63VvJPDJ1d9PGqRLumuwMr_Gtg-23kjZN0,3809
7
+ physverify/loaders.py,sha256=pO9p3yzPxZWgVaJr8fw46DQxAtksXl-Ta-cvqlZVzvE,1420
8
+ physverify/matrix.py,sha256=48Eappft-KuHy0DJtJyCbfQPQkLtVQx9f3dyIe-bxAo,6321
9
+ physverify/mms.py,sha256=x7US9wUXCOEPSxja1t1F37crds3b7yFV-kzMV-TGEQY,4145
10
+ physverify/order.py,sha256=x1QKA8rfmbYpCidcQXZvvJ75oYXhMXdJY_XPNPVtPuo,5310
11
+ physverify/pytest_plugin.py,sha256=_cL4iQFIawXPu4WY8n4JtdQ443aa_fntjJ7rxuMLU7c,2539
12
+ physverify/report.py,sha256=uopkT1qP4OVmGp6yDeRcNZe8jc-gXR7SnY3uYbUb_Cs,7672
13
+ physverify/steppers.py,sha256=A2BMgca3_Oq_3kEicblZCGSIaPeq1Da472wrPVcA35c,975
14
+ physverify/studies.py,sha256=bzjmp7zdNBlkWCDqF8V2V4K0cqJ-EfCoYxDWTIJMONw,6105
15
+ physverify/symbolic.py,sha256=8oAuSYzToGKOSseKvjYItQcZKqDSE-J9BWMgJXiOJAo,2955
16
+ physverify/adapters/__init__.py,sha256=jFByVfj91ENm_S6L2BSFd0vpJJQUZi5skCYpPAUJAW0,461
17
+ physverify/adapters/double_pendulum.py,sha256=EUIaimQ4hDcEwi3h3pMKZlBofjinofsjZGjdm8SLAec,1629
18
+ physverify/adapters/lorenz.py,sha256=h5aQLkeS93oUyEJPUa1NduaL74zAxhH6GPOqZqKTZgc,5410
19
+ physverify/adapters/projectile_v1.py,sha256=XSbS6B-gkX0ziEmDEazXILKmR4FUzvyD14_XWfFFg3U,3366
20
+ physverify/adapters/projectile_v2.py,sha256=IJnTfhd9-zdzsD93c0IYekqq0br7KqDs7awZGE95WIg,3579
21
+ physverify/adapters/three_body.py,sha256=Vv7m7uagZtIxGPiQ6ufeZPejcyNPFVFzuhYNbVjTPq0,11028
22
+ physverify/problems/__init__.py,sha256=b0GJzVq3c38csBkDIatyj2Koc3j10Px26uyTXd7cTi8,218
23
+ physverify/problems/_common.py,sha256=3aTKpLUdKKvN5fAJUjNTO-f1DGEFJ57Xu74DftAesP0,2911
24
+ physverify/problems/heat1d.py,sha256=KiZUqJ7FFfiKQK_tGj8PRyytHlLNvh7h7XCq2LQ6k8M,7936
25
+ physverify/problems/kepler.py,sha256=saLS4J9L_0wFPhIc3YYftQjeSGR19BTY67A1P5P2vpI,5626
26
+ physverify/problems/pendulum.py,sha256=s_PCWCBtugBkchmxjHMiR5Sr-t4Uq1314fmBfYaqQkI,7357
27
+ physverify/problems/projectile.py,sha256=3FMjeXYLc36mvApQZC5dviMdbSiT3K3UhBovdoII7yE,9390
28
+ physverify/problems/sod.py,sha256=KyXLbjuHy3d3OFOOYnXSaiNGhepHuNLZpcQDlDHcVo0,11934
29
+ physverify/stochastic/__init__.py,sha256=RrooJVboF5LPwhLTKnf4dezmnNhwaT0wV7ehGV8vkYw,101
30
+ physverify/stochastic/calibrate.py,sha256=1eprgcx0UwoqYoI3fibisUxQF61QJOa07eu_E5FFNMA,8633
31
+ physverify/stochastic/checks.py,sha256=8JS_bvK-MG-zZdJ8uspATPafv4WsvVSLkBCA5yXxSOs,18086
32
+ physverify/stochastic/exact.py,sha256=U7Mg4LmfT__eRLUyTsKHAhPrYKAVU6bQBJK21Y_pEN0,7484
33
+ physverify/stochastic/ising.py,sha256=4LAJ3kKLGzBd_XtjPtb980VU5gsP1YtlYze5CbP-l0U,7936
34
+ physverify/stochastic/matrix.py,sha256=RHHHmWvY5leesSELTLXQ69Sy397blXxsu6VTA-rQKMk,6235
35
+ physverify/stochastic/reference.py,sha256=s3G83INawcnNbv2k1ScHQI3b3-7m2EDxEozegXdqAU4,2264
36
+ physverify/stochastic/sampler.py,sha256=m0jV_qd3G-V2Q4NM653TTbA8QTgz7vj6zKfMX8FSp9c,10548
37
+ physverify/stochastic/stats.py,sha256=G5SEj6iu3BUKQ5hKU89Cj2h201KfGQBexDeGhJUGZ8U,5244
38
+ physverify-0.1.0.dist-info/licenses/LICENSE,sha256=A7qXUpTre2zp1GZSa1XwjM14YGyHnvA2Zh0frie2RKg,1073
39
+ physverify-0.1.0.dist-info/METADATA,sha256=R7V58ro1qXmIcs2pjyblcfYTYtuLJMZK8Fs9rLUBG2U,6664
40
+ physverify-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
41
+ physverify-0.1.0.dist-info/entry_points.txt,sha256=PAskhOwukCAo7xzNdJzVBYCwH0j_qck2_LFoCLkzM7k,101
42
+ physverify-0.1.0.dist-info/top_level.txt,sha256=NbSy2myuT-0wT98BIYVvvAWB7Bb6RG9mgo2Y2qGouJw,11
43
+ physverify-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,5 @@
1
+ [console_scripts]
2
+ physverify = physverify.cli:main
3
+
4
+ [pytest11]
5
+ physverify = physverify.pytest_plugin
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ram Chandra Giri
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ physverify