physverify 0.6.0__tar.gz → 0.8.0__tar.gz

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 (92) hide show
  1. physverify-0.8.0/PKG-INFO +132 -0
  2. physverify-0.8.0/README.md +88 -0
  3. {physverify-0.6.0 → physverify-0.8.0}/pyproject.toml +23 -4
  4. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/__init__.py +2 -3
  5. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/agent.py +117 -19
  6. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/butcher.py +113 -5
  7. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/checks.py +1 -1
  8. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/cli.py +90 -5
  9. physverify-0.8.0/src/physverify/confirm.py +338 -0
  10. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/constants.py +4 -1
  11. physverify-0.8.0/src/physverify/flow.py +272 -0
  12. physverify-0.8.0/src/physverify/fluxes.py +58 -0
  13. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/identities.py +112 -10
  14. physverify-0.8.0/src/physverify/isolate.py +109 -0
  15. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/lint.py +87 -9
  16. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/mms.py +10 -2
  17. physverify-0.8.0/src/physverify/operators.py +215 -0
  18. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/order.py +62 -7
  19. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/probes.py +10 -4
  20. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/references.py +20 -0
  21. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/report.py +16 -1
  22. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/scan.py +97 -7
  23. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/smoke.py +5 -2
  24. physverify-0.8.0/src/physverify/specscan.py +67 -0
  25. physverify-0.8.0/src/physverify/stencils.py +134 -0
  26. physverify-0.8.0/src/physverify/steps.py +97 -0
  27. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/stochastic/calibrate.py +0 -2
  28. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/stochastic/checks.py +1 -2
  29. physverify-0.8.0/src/physverify/stochastic/directions.py +84 -0
  30. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/stochastic/exact.py +2 -2
  31. physverify-0.8.0/src/physverify/stochastic/ito.py +87 -0
  32. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/stochastic/stats.py +49 -3
  33. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/studies.py +21 -5
  34. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/symbolic.py +1 -1
  35. physverify-0.8.0/src/physverify.egg-info/PKG-INFO +132 -0
  36. {physverify-0.6.0 → physverify-0.8.0}/src/physverify.egg-info/SOURCES.txt +21 -0
  37. {physverify-0.6.0 → physverify-0.8.0}/src/physverify.egg-info/requires.txt +2 -0
  38. {physverify-0.6.0 → physverify-0.8.0}/tests/test_agent.py +53 -2
  39. physverify-0.8.0/tests/test_awkward_input.py +151 -0
  40. {physverify-0.6.0 → physverify-0.8.0}/tests/test_batch8_lessons.py +27 -2
  41. {physverify-0.6.0 → physverify-0.8.0}/tests/test_butcher_properties.py +10 -2
  42. {physverify-0.6.0 → physverify-0.8.0}/tests/test_cli.py +13 -0
  43. physverify-0.8.0/tests/test_confirm.py +319 -0
  44. physverify-0.8.0/tests/test_corpus_ledger.py +27 -0
  45. physverify-0.8.0/tests/test_correctness_fixes.py +142 -0
  46. physverify-0.8.0/tests/test_directions.py +94 -0
  47. {physverify-0.6.0 → physverify-0.8.0}/tests/test_docs_examples.py +16 -0
  48. {physverify-0.6.0 → physverify-0.8.0}/tests/test_figures.py +11 -2
  49. physverify-0.8.0/tests/test_helpers_2026_10.py +145 -0
  50. {physverify-0.6.0 → physverify-0.8.0}/tests/test_ising.py +7 -1
  51. physverify-0.8.0/tests/test_new_checks_2026_11.py +142 -0
  52. {physverify-0.6.0 → physverify-0.8.0}/tests/test_new_problems.py +1 -2
  53. physverify-0.8.0/tests/test_operators.py +112 -0
  54. {physverify-0.6.0 → physverify-0.8.0}/tests/test_order.py +34 -1
  55. physverify-0.8.0/tests/test_quadrature.py +76 -0
  56. physverify-0.8.0/tests/test_roadmap_0_7.py +89 -0
  57. physverify-0.8.0/tests/test_sandbox_installs.py +52 -0
  58. {physverify-0.6.0 → physverify-0.8.0}/tests/test_scan.py +151 -0
  59. physverify-0.8.0/tests/test_scan_flow.py +150 -0
  60. {physverify-0.6.0 → physverify-0.8.0}/tests/test_seeded_bugs.py +1 -1
  61. {physverify-0.6.0 → physverify-0.8.0}/tests/test_v040_helpers.py +12 -1
  62. physverify-0.6.0/PKG-INFO +0 -95
  63. physverify-0.6.0/README.md +0 -53
  64. physverify-0.6.0/src/physverify.egg-info/PKG-INFO +0 -95
  65. physverify-0.6.0/tests/test_awkward_input.py +0 -74
  66. {physverify-0.6.0 → physverify-0.8.0}/LICENSE +0 -0
  67. {physverify-0.6.0 → physverify-0.8.0}/setup.cfg +0 -0
  68. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/__main__.py +0 -0
  69. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/demo.py +1 -1
  70. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/loaders.py +0 -0
  71. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/pytest_plugin.py +0 -0
  72. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/scaffold.py +0 -0
  73. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/steppers.py +0 -0
  74. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/stochastic/__init__.py +0 -0
  75. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/stochastic/ising.py +0 -0
  76. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/stochastic/reference.py +0 -0
  77. {physverify-0.6.0 → physverify-0.8.0}/src/physverify/stochastic/sampler.py +0 -0
  78. {physverify-0.6.0 → physverify-0.8.0}/src/physverify.egg-info/dependency_links.txt +0 -0
  79. {physverify-0.6.0 → physverify-0.8.0}/src/physverify.egg-info/entry_points.txt +0 -0
  80. {physverify-0.6.0 → physverify-0.8.0}/src/physverify.egg-info/top_level.txt +0 -0
  81. {physverify-0.6.0 → physverify-0.8.0}/tests/test_batch7_lessons.py +0 -0
  82. {physverify-0.6.0 → physverify-0.8.0}/tests/test_corpus_lessons.py +1 -1
  83. {physverify-0.6.0 → physverify-0.8.0}/tests/test_method_bugs.py +0 -0
  84. {physverify-0.6.0 → physverify-0.8.0}/tests/test_mms.py +0 -0
  85. {physverify-0.6.0 → physverify-0.8.0}/tests/test_probes.py +0 -0
  86. {physverify-0.6.0 → physverify-0.8.0}/tests/test_properties.py +1 -1
  87. {physverify-0.6.0 → physverify-0.8.0}/tests/test_real_projects.py +1 -1
  88. {physverify-0.6.0 → physverify-0.8.0}/tests/test_reference_problems.py +1 -1
  89. {physverify-0.6.0 → physverify-0.8.0}/tests/test_references.py +0 -0
  90. {physverify-0.6.0 → physverify-0.8.0}/tests/test_scaffold.py +0 -0
  91. {physverify-0.6.0 → physverify-0.8.0}/tests/test_studies.py +1 -1
  92. {physverify-0.6.0 → physverify-0.8.0}/tests/test_versions.py +0 -0
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.4
2
+ Name: physverify
3
+ Version: 0.8.0
4
+ Summary: Verification checks for physics simulation code: manufactured solutions, order-of-accuracy studies, conserved quantities, limiting cases and exact patch tests.
5
+ Author: Ram Chandra Giri
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/rcgiri-physics/physverify
8
+ Project-URL: Documentation, https://rcgiri-physics.github.io/physverify/
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
+ Requires-Dist: tomli>=1.1; python_version < "3.11"
31
+ Provides-Extra: symbolic
32
+ Requires-Dist: sympy>=1.12; extra == "symbolic"
33
+ Provides-Extra: dev
34
+ Requires-Dist: pytest>=8; extra == "dev"
35
+ Requires-Dist: pytest-cov>=6.1; extra == "dev"
36
+ Requires-Dist: hypothesis>=6; extra == "dev"
37
+ Requires-Dist: sympy>=1.12; extra == "dev"
38
+ Requires-Dist: ruff>=0.6; extra == "dev"
39
+ Provides-Extra: docs
40
+ Requires-Dist: mkdocs<2,>=1.6; extra == "docs"
41
+ Requires-Dist: mkdocs-material<10,>=9.5; extra == "docs"
42
+ Requires-Dist: mkdocstrings[python]<2,>=1.0.8; extra == "docs"
43
+ Dynamic: license-file
44
+
45
+ # physverify: confirm physics claims by running the code
46
+
47
+ [![tests](https://github.com/rcgiri-physics/physverify/actions/workflows/ci.yml/badge.svg)](https://github.com/rcgiri-physics/physverify/actions/workflows/ci.yml) [![PyPI](https://img.shields.io/pypi/v/physverify)](https://pypi.org/project/physverify/) [![Python](https://img.shields.io/pypi/pyversions/physverify)](https://pypi.org/project/physverify/) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.23534512.svg)](https://doi.org/10.5281/zenodo.23534512) [![Docs](https://img.shields.io/badge/docs-site-blue)](https://rcgiri-physics.github.io/physverify/)
48
+
49
+ **A suspected bug in simulation code is a claim. physverify turns it into a run against a reference written from the physics, and gives a verdict with the numbers.**
50
+
51
+ The claim can come from an AI code review, a colleague, a static check or your own doubt. Reading the code again rarely settles it; a run against an exact solution, a conservation law or a convergence order does. physverify supplies those checks for numerical ODE and PDE code (order of convergence, manufactured solutions, conservation, Runge-Kutta tables, exact references, a few Monte Carlo statistics), a sandbox for running code you do not trust, and plain-language verdicts. It needs only NumPy. Current release: 0.8.0 (v0.8.0); actively developed, contributions welcome.
52
+
53
+ ![A claim from a review becomes a run against a reference from the physics, and the run gives a verdict: confirmed, minor, rejected or open](https://raw.githubusercontent.com/rcgiri-physics/physverify/main/docs/images/claim-to-verdict.svg)
54
+
55
+ ## What confirming claims found on 100 repositories
56
+
57
+ Five AI reviewers (three Claude models, Qwen 3.8 Flash and GPT 5.6 Terra) reviewed 100 public physics repositories with one fixed prompt. Fifty had a defect reported earlier; fifty had been tested and found clean. On the clean fifty, 119 review claims were drawn and each was given a check run in the sandbox:
58
+
59
+ ![Of 119 claims checked on repositories tested clean: 40 changed a result, 51 were real but minor, 10 were wrong, 18 stayed open](https://raw.githubusercontent.com/rcgiri-physics/physverify/main/docs/images/claim-outcomes.svg)
60
+
61
+ - **40 claims were real defects that change a result:** 33 distinct defects in 19 of the 50 "clean" repositories. 32 have been reported to their maintainers.
62
+ - **51 were real but minor** (invalid input, display, docstrings, code paths the repository never uses), **10 were wrong** and **18 could not be settled.**
63
+ - Nine of the ten wrong claims came from one model. Where a reading was recorded before the run (20 claims), the run changed 3 verdicts.
64
+ - Against the 67 defects reported earlier, the reviewers named 36 to 49; `physverify scan`, which reads code without running it, named 11.
65
+
66
+ So AI review finds many real defects, including ones earlier testing missed, but a claim is not a finding until something runs. The counts hold for this sample only and are not a bug rate for physics code. The labels come from two automated checkers following written rules, not from people. Tables: `scripts/study100.py` regenerates them from the per-claim file, released with the preprint ([doi:10.5281/zenodo.24224433](https://doi.org/10.5281/zenodo.24224433)).
67
+
68
+ ## Confirmed by maintainers
69
+
70
+ Some defects confirmed or fixed by the projects' own maintainers after a report:
71
+
72
+ | Project | What was found | Outcome |
73
+ |---|---|---|
74
+ | [SymPy](https://github.com/sympy/sympy/issues/30810) | three units differ from their exact definitions | confirmed against primary sources |
75
+ | [Astropy](https://github.com/astropy/astropy/issues/20681) | "surface tension" registered as J m² instead of J/m² | confirmed; fix in progress |
76
+ | [PyGMT](https://github.com/GenericMappingTools/pygmt/issues/4960) | strings with a space treated as datetimes | fixed |
77
+ | [EinsteinPy](https://github.com/einsteinpy/einsteinpy/issues/705) | de Sitter scale factor on a spatial coordinate | fixed |
78
+ | [TESPy](https://github.com/oemof/tespy/issues/1127) | entropy pressure term with the wrong units | fixed |
79
+ | [PyAstronomy](https://github.com/sczesla/PyAstronomy/issues/71) | two constants modules defaulted to different unit systems (CGS and SI), so `planck` and other functions were wrong | fixed |
80
+ | [qmat](https://github.com/Parallel-in-Time/qmat/issues/23) | Runge-Kutta schemes labelled one order too high | fixed; now uses `physverify.butcher` in CI |
81
+
82
+ [The full list](https://rcgiri-physics.github.io/physverify/confirmed/) has more projects and the details.
83
+
84
+ ## Try it
85
+
86
+ ```bash
87
+ pip install physverify
88
+ physverify demo
89
+ ```
90
+
91
+ The demo checks a correct solver (all checks pass), then the same solver with the damping 10% too large (all fail):
92
+
93
+ ![Error against step size: the correct solver follows the slope-2 line, the broken one levels off](https://raw.githubusercontent.com/rcgiri-physics/physverify/main/docs/images/convergence.svg)
94
+
95
+ New to this? [Start here](https://rcgiri-physics.github.io/physverify/start-here/) takes about ten minutes and includes a prompt to paste if you use an AI coding assistant.
96
+
97
+ ## Use it on your own code
98
+
99
+ - **Suspected bugs from a review (yours or an AI's):** write each claim down, pick a reference that does not come from the code, and run the smallest test that tells the two answers apart. [Checking an AI code review](https://rcgiri-physics.github.io/physverify/ai-review/) walks through it with examples.
100
+ - **An ODE solver:** `physverify init verify_solver.py --solver mysolver --order 4` writes a spec; fill in the equations (from the physics, not from your code) and run `physverify run verify_solver.py`.
101
+ - **Errors you already have:** `check_order(hs, errors, expected=2)` gives PASS, FAIL or INCONCLUSIVE with a reason.
102
+ - **A Runge-Kutta table:** `check_tableau(A, b, c, claimed=3)` from `physverify.butcher` checks order, stage order and stability from the coefficients alone.
103
+ - **Code you have not read yet:** `physverify scan PATH` reads the source, runs nothing, and flags a fixed set of patterns (Runge-Kutta tables below their stated order, grid spacings that do not match their grid, near-miss constants, removed NumPy/SciPy features, undefined names, unit tables with the wrong dimension). A clean scan means only that those patterns were not found. `scan` is kept working but no longer gets new rules; the work now goes into confirming claims.
104
+
105
+ Code from other people runs only in the [sandbox](https://github.com/rcgiri-physics/physverify/tree/main/sandbox) (no network, read-only repository).
106
+
107
+ ## Run it in CI
108
+
109
+ A failed check fails the job, like a broken unit test ([CI guide](https://rcgiri-physics.github.io/physverify/ci/)):
110
+
111
+ ```yaml
112
+ - uses: rcgiri-physics/physverify@v0.8.0
113
+ with:
114
+ specs: tests/verify_*.py
115
+ ```
116
+
117
+ [Getting started](https://rcgiri-physics.github.io/physverify/getting-started/) · [Recipes](https://rcgiri-physics.github.io/physverify/recipes/) · [What a PASS means](https://rcgiri-physics.github.io/physverify/assumptions/) · [Troubleshooting](https://rcgiri-physics.github.io/physverify/troubleshooting/) · [API](https://rcgiri-physics.github.io/physverify/api/)
118
+
119
+ ## Other evidence
120
+
121
+ - **Seeded bugs:** of 37 bugs added one at a time to five small solvers, the checks flagged 35 (plain "compare with one answer" tests: 27), with no false alarms. The same person wrote the bugs and the checks, so this shows the tool works as designed, not how it does on other code. `python scripts/reproduce_all.py` regenerates the tables.
122
+ - **Earlier corpus testing (October 2026):** of the first 74 defects reported to authors, 14 were found by a physverify check as such, 11 by running the code at all, and the rest by the reference in each repository's test script or by reading; the [ledger](https://github.com/rcgiri-physics/physverify/blob/main/results/corpus/LEDGER.md) lists them by ID. Details: [results](https://rcgiri-physics.github.io/physverify/results/).
123
+
124
+ ## Limits
125
+
126
+ Works best for ODEs; PDEs only in 1D so far. Your code needs a way to add a source term, or a small wrapper that can itself be wrong. A PASS means these checks found nothing wrong; it does not prove the simulation correct, and it is only as independent as the reference you wrote: see [What a PASS means](https://rcgiri-physics.github.io/physverify/assumptions/).
127
+
128
+ ## Contributing, citing, licence
129
+
130
+ Help is welcome: a trial report on your own solver, seeded bugs written by someone other than the author, adapters, documentation. See [CONTRIBUTING.md](https://github.com/rcgiri-physics/physverify/blob/main/CONTRIBUTING.md), the [code of conduct](https://github.com/rcgiri-physics/physverify/blob/main/CODE_OF_CONDUCT.md), [SUPPORT.md](https://github.com/rcgiri-physics/physverify/blob/main/SUPPORT.md), [SECURITY.md](https://github.com/rcgiri-physics/physverify/blob/main/SECURITY.md) on running untrusted code, and [AGENTS.md](https://github.com/rcgiri-physics/physverify/blob/main/AGENTS.md) for AI assistants. To cite, use [doi:10.5281/zenodo.23534512](https://doi.org/10.5281/zenodo.23534512) or [CITATION.cff](https://github.com/rcgiri-physics/physverify/blob/main/CITATION.cff). Changes: [CHANGELOG.md](https://github.com/rcgiri-physics/physverify/blob/main/CHANGELOG.md); plans: [ROADMAP.md](https://github.com/rcgiri-physics/physverify/blob/main/ROADMAP.md). The methods are standard and other tools do parts of this, some better (NodePy for Runge-Kutta analysis); see [related work and credits](https://rcgiri-physics.github.io/physverify/related-work/). MIT licence, copyright (c) 2026 Ram Chandra Giri.
131
+
132
+ <!-- mcp-name: io.github.rcgiri-physics/physverify -->
@@ -0,0 +1,88 @@
1
+ # physverify: confirm physics claims by running the code
2
+
3
+ [![tests](https://github.com/rcgiri-physics/physverify/actions/workflows/ci.yml/badge.svg)](https://github.com/rcgiri-physics/physverify/actions/workflows/ci.yml) [![PyPI](https://img.shields.io/pypi/v/physverify)](https://pypi.org/project/physverify/) [![Python](https://img.shields.io/pypi/pyversions/physverify)](https://pypi.org/project/physverify/) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.23534512.svg)](https://doi.org/10.5281/zenodo.23534512) [![Docs](https://img.shields.io/badge/docs-site-blue)](https://rcgiri-physics.github.io/physverify/)
4
+
5
+ **A suspected bug in simulation code is a claim. physverify turns it into a run against a reference written from the physics, and gives a verdict with the numbers.**
6
+
7
+ The claim can come from an AI code review, a colleague, a static check or your own doubt. Reading the code again rarely settles it; a run against an exact solution, a conservation law or a convergence order does. physverify supplies those checks for numerical ODE and PDE code (order of convergence, manufactured solutions, conservation, Runge-Kutta tables, exact references, a few Monte Carlo statistics), a sandbox for running code you do not trust, and plain-language verdicts. It needs only NumPy. Current release: 0.8.0 (v0.8.0); actively developed, contributions welcome.
8
+
9
+ ![A claim from a review becomes a run against a reference from the physics, and the run gives a verdict: confirmed, minor, rejected or open](https://raw.githubusercontent.com/rcgiri-physics/physverify/main/docs/images/claim-to-verdict.svg)
10
+
11
+ ## What confirming claims found on 100 repositories
12
+
13
+ Five AI reviewers (three Claude models, Qwen 3.8 Flash and GPT 5.6 Terra) reviewed 100 public physics repositories with one fixed prompt. Fifty had a defect reported earlier; fifty had been tested and found clean. On the clean fifty, 119 review claims were drawn and each was given a check run in the sandbox:
14
+
15
+ ![Of 119 claims checked on repositories tested clean: 40 changed a result, 51 were real but minor, 10 were wrong, 18 stayed open](https://raw.githubusercontent.com/rcgiri-physics/physverify/main/docs/images/claim-outcomes.svg)
16
+
17
+ - **40 claims were real defects that change a result:** 33 distinct defects in 19 of the 50 "clean" repositories. 32 have been reported to their maintainers.
18
+ - **51 were real but minor** (invalid input, display, docstrings, code paths the repository never uses), **10 were wrong** and **18 could not be settled.**
19
+ - Nine of the ten wrong claims came from one model. Where a reading was recorded before the run (20 claims), the run changed 3 verdicts.
20
+ - Against the 67 defects reported earlier, the reviewers named 36 to 49; `physverify scan`, which reads code without running it, named 11.
21
+
22
+ So AI review finds many real defects, including ones earlier testing missed, but a claim is not a finding until something runs. The counts hold for this sample only and are not a bug rate for physics code. The labels come from two automated checkers following written rules, not from people. Tables: `scripts/study100.py` regenerates them from the per-claim file, released with the preprint ([doi:10.5281/zenodo.24224433](https://doi.org/10.5281/zenodo.24224433)).
23
+
24
+ ## Confirmed by maintainers
25
+
26
+ Some defects confirmed or fixed by the projects' own maintainers after a report:
27
+
28
+ | Project | What was found | Outcome |
29
+ |---|---|---|
30
+ | [SymPy](https://github.com/sympy/sympy/issues/30810) | three units differ from their exact definitions | confirmed against primary sources |
31
+ | [Astropy](https://github.com/astropy/astropy/issues/20681) | "surface tension" registered as J m² instead of J/m² | confirmed; fix in progress |
32
+ | [PyGMT](https://github.com/GenericMappingTools/pygmt/issues/4960) | strings with a space treated as datetimes | fixed |
33
+ | [EinsteinPy](https://github.com/einsteinpy/einsteinpy/issues/705) | de Sitter scale factor on a spatial coordinate | fixed |
34
+ | [TESPy](https://github.com/oemof/tespy/issues/1127) | entropy pressure term with the wrong units | fixed |
35
+ | [PyAstronomy](https://github.com/sczesla/PyAstronomy/issues/71) | two constants modules defaulted to different unit systems (CGS and SI), so `planck` and other functions were wrong | fixed |
36
+ | [qmat](https://github.com/Parallel-in-Time/qmat/issues/23) | Runge-Kutta schemes labelled one order too high | fixed; now uses `physverify.butcher` in CI |
37
+
38
+ [The full list](https://rcgiri-physics.github.io/physverify/confirmed/) has more projects and the details.
39
+
40
+ ## Try it
41
+
42
+ ```bash
43
+ pip install physverify
44
+ physverify demo
45
+ ```
46
+
47
+ The demo checks a correct solver (all checks pass), then the same solver with the damping 10% too large (all fail):
48
+
49
+ ![Error against step size: the correct solver follows the slope-2 line, the broken one levels off](https://raw.githubusercontent.com/rcgiri-physics/physverify/main/docs/images/convergence.svg)
50
+
51
+ New to this? [Start here](https://rcgiri-physics.github.io/physverify/start-here/) takes about ten minutes and includes a prompt to paste if you use an AI coding assistant.
52
+
53
+ ## Use it on your own code
54
+
55
+ - **Suspected bugs from a review (yours or an AI's):** write each claim down, pick a reference that does not come from the code, and run the smallest test that tells the two answers apart. [Checking an AI code review](https://rcgiri-physics.github.io/physverify/ai-review/) walks through it with examples.
56
+ - **An ODE solver:** `physverify init verify_solver.py --solver mysolver --order 4` writes a spec; fill in the equations (from the physics, not from your code) and run `physverify run verify_solver.py`.
57
+ - **Errors you already have:** `check_order(hs, errors, expected=2)` gives PASS, FAIL or INCONCLUSIVE with a reason.
58
+ - **A Runge-Kutta table:** `check_tableau(A, b, c, claimed=3)` from `physverify.butcher` checks order, stage order and stability from the coefficients alone.
59
+ - **Code you have not read yet:** `physverify scan PATH` reads the source, runs nothing, and flags a fixed set of patterns (Runge-Kutta tables below their stated order, grid spacings that do not match their grid, near-miss constants, removed NumPy/SciPy features, undefined names, unit tables with the wrong dimension). A clean scan means only that those patterns were not found. `scan` is kept working but no longer gets new rules; the work now goes into confirming claims.
60
+
61
+ Code from other people runs only in the [sandbox](https://github.com/rcgiri-physics/physverify/tree/main/sandbox) (no network, read-only repository).
62
+
63
+ ## Run it in CI
64
+
65
+ A failed check fails the job, like a broken unit test ([CI guide](https://rcgiri-physics.github.io/physverify/ci/)):
66
+
67
+ ```yaml
68
+ - uses: rcgiri-physics/physverify@v0.8.0
69
+ with:
70
+ specs: tests/verify_*.py
71
+ ```
72
+
73
+ [Getting started](https://rcgiri-physics.github.io/physverify/getting-started/) · [Recipes](https://rcgiri-physics.github.io/physverify/recipes/) · [What a PASS means](https://rcgiri-physics.github.io/physverify/assumptions/) · [Troubleshooting](https://rcgiri-physics.github.io/physverify/troubleshooting/) · [API](https://rcgiri-physics.github.io/physverify/api/)
74
+
75
+ ## Other evidence
76
+
77
+ - **Seeded bugs:** of 37 bugs added one at a time to five small solvers, the checks flagged 35 (plain "compare with one answer" tests: 27), with no false alarms. The same person wrote the bugs and the checks, so this shows the tool works as designed, not how it does on other code. `python scripts/reproduce_all.py` regenerates the tables.
78
+ - **Earlier corpus testing (October 2026):** of the first 74 defects reported to authors, 14 were found by a physverify check as such, 11 by running the code at all, and the rest by the reference in each repository's test script or by reading; the [ledger](https://github.com/rcgiri-physics/physverify/blob/main/results/corpus/LEDGER.md) lists them by ID. Details: [results](https://rcgiri-physics.github.io/physverify/results/).
79
+
80
+ ## Limits
81
+
82
+ Works best for ODEs; PDEs only in 1D so far. Your code needs a way to add a source term, or a small wrapper that can itself be wrong. A PASS means these checks found nothing wrong; it does not prove the simulation correct, and it is only as independent as the reference you wrote: see [What a PASS means](https://rcgiri-physics.github.io/physverify/assumptions/).
83
+
84
+ ## Contributing, citing, licence
85
+
86
+ Help is welcome: a trial report on your own solver, seeded bugs written by someone other than the author, adapters, documentation. See [CONTRIBUTING.md](https://github.com/rcgiri-physics/physverify/blob/main/CONTRIBUTING.md), the [code of conduct](https://github.com/rcgiri-physics/physverify/blob/main/CODE_OF_CONDUCT.md), [SUPPORT.md](https://github.com/rcgiri-physics/physverify/blob/main/SUPPORT.md), [SECURITY.md](https://github.com/rcgiri-physics/physverify/blob/main/SECURITY.md) on running untrusted code, and [AGENTS.md](https://github.com/rcgiri-physics/physverify/blob/main/AGENTS.md) for AI assistants. To cite, use [doi:10.5281/zenodo.23534512](https://doi.org/10.5281/zenodo.23534512) or [CITATION.cff](https://github.com/rcgiri-physics/physverify/blob/main/CITATION.cff). Changes: [CHANGELOG.md](https://github.com/rcgiri-physics/physverify/blob/main/CHANGELOG.md); plans: [ROADMAP.md](https://github.com/rcgiri-physics/physverify/blob/main/ROADMAP.md). The methods are standard and other tools do parts of this, some better (NodePy for Runge-Kutta analysis); see [related work and credits](https://rcgiri-physics.github.io/physverify/related-work/). MIT licence, copyright (c) 2026 Ram Chandra Giri.
87
+
88
+ <!-- mcp-name: io.github.rcgiri-physics/physverify -->
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "physverify"
7
- version = "0.6.0"
8
- description = "Verification checks for physics simulation code: manufactured solutions, order-of-accuracy studies and a seeded-bug benchmark."
7
+ version = "0.8.0"
8
+ description = "Verification checks for physics simulation code: manufactured solutions, order-of-accuracy studies, conserved quantities, limiting cases and exact patch tests."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  license = "MIT"
@@ -38,8 +38,8 @@ Changelog = "https://github.com/rcgiri-physics/physverify/blob/main/CHANGELOG.md
38
38
 
39
39
  [project.optional-dependencies]
40
40
  symbolic = ["sympy>=1.12"]
41
- dev = ["pytest>=8", "pytest-cov>=6.1", "hypothesis>=6", "sympy>=1.12"]
42
- docs = ["mkdocs>=1.6,<2", "mkdocs-material>=9.5,<10"]
41
+ dev = ["pytest>=8", "pytest-cov>=6.1", "hypothesis>=6", "sympy>=1.12", "ruff>=0.6"]
42
+ docs = ["mkdocs>=1.6,<2", "mkdocs-material>=9.5,<10", "mkdocstrings[python]>=1.0.8,<2"]
43
43
 
44
44
  [project.scripts]
45
45
  physverify = "physverify.cli:main"
@@ -53,3 +53,22 @@ where = ["src"]
53
53
  [tool.pytest.ini_options]
54
54
  testpaths = ["tests"]
55
55
  pythonpath = ["src", "benchmark"]
56
+
57
+ [tool.ruff]
58
+ extend-exclude = ["build"]
59
+
60
+ [tool.ruff.lint]
61
+ select = ["F", "B", "UP", "I"]
62
+ # zip() truncating to the shorter input is relied on in places; strict= is a separate review
63
+ ignore = ["B905"]
64
+
65
+ [tool.ruff.lint.isort]
66
+ known-first-party = ["physverify", "pvbench"]
67
+
68
+ [tool.ruff.lint.per-file-ignores]
69
+ # results/ cites line numbers in the seeded-bug problems, so their imports stay where they are
70
+ "benchmark/pvbench/problems/*" = ["F401", "I001"]
71
+ # the import itself is what this script checks
72
+ "sandbox/probe.py" = ["F401", "I001"]
73
+ # short teaching examples; the closure is called inside its loop
74
+ "docs/examples/*" = ["B023"]
@@ -2,11 +2,10 @@
2
2
 
3
3
  from .checks import Check, OrderResult, Result, SkipCheck, requires
4
4
  from .mms import ManufacturedODE, mms_order_study, zero_source
5
- from .order import check_order, fit_order, pairwise_orders
6
- from .order import check_exact
5
+ from .order import check_exact, check_order, fit_order, pairwise_orders
7
6
  from .studies import invariant_study, ode_checks, patch_study, reference_study
8
7
 
9
- __version__ = "0.6.0"
8
+ __version__ = "0.8.0"
10
9
 
11
10
  __all__ = [
12
11
  "Check",
@@ -10,8 +10,11 @@ Hook (`physverify hook`)
10
10
  assistant. Otherwise it exits 0 and prints nothing. See docs/agent.md.
11
11
 
12
12
  MCP server (`physverify mcp`)
13
- Model Context Protocol over stdio (newline-delimited JSON-RPC 2.0), with one
14
- tool, `run_checks`, that takes spec paths and returns the JSON report. It is
13
+ Model Context Protocol over stdio (newline-delimited JSON-RPC 2.0), with two
14
+ tools: `run_checks`, that takes spec paths and returns the JSON report, and
15
+ `confirm_claim`, that judges one claim file (docs/confirm.md). Both run the code
16
+ in a fresh Python process with a time limit (physverify.isolate), so an edit to
17
+ the solver between two calls is always seen. It is
15
18
  written with the standard library only, so it has no extra dependency; it
16
19
  implements the parts of the protocol a tool server needs, not the whole
17
20
  specification. It serves both protocol eras on the same process:
@@ -32,6 +35,7 @@ import json
32
35
  import sys
33
36
  from pathlib import Path
34
37
 
38
+ RUN_TIMEOUT = 600.0 # seconds for one run_checks call in its own process
35
39
  SKIP_DIRS = {"venv", ".venv", "env", "node_modules", "build", "dist", "__pycache__", "site-packages"}
36
40
  MAX_REPORTED = 10
37
41
 
@@ -112,14 +116,18 @@ def hook_main(stdin=None, stderr=None) -> int:
112
116
  event = json.load(stdin)
113
117
  except (json.JSONDecodeError, ValueError):
114
118
  return 0 # not a hook event; never block the assistant on our own input problem
115
- tool_input = event.get("tool_input") or {}
119
+ if not isinstance(event, dict):
120
+ return 0
121
+ tool_input = event.get("tool_input")
122
+ tool_input = tool_input if isinstance(tool_input, dict) else {}
116
123
  edited = tool_input.get("file_path") or tool_input.get("notebook_path") or ""
117
- if edited and not edited.endswith(".py"):
124
+ if not isinstance(edited, str) or (edited and not edited.endswith(".py")):
118
125
  return 0
119
126
  cwd = Path(event.get("cwd") or ".").resolve()
120
- if edited and not Path(edited).resolve().is_relative_to(cwd):
127
+ edited_path = (cwd / edited).resolve() if edited else None # a relative path is relative to the session folder
128
+ if edited_path is not None and not edited_path.is_relative_to(cwd):
121
129
  return 0 # an edit outside the session folder must not run that folder's code (GCVE-110-VULN-PYPI-2026-000391)
122
- root = find_project_root(Path(edited).parent if edited else cwd)
130
+ root = find_project_root(edited_path.parent if edited_path is not None else cwd)
123
131
  if not root.is_relative_to(cwd):
124
132
  root = cwd # never climb above the session folder
125
133
  specs = find_specs(root)
@@ -141,14 +149,17 @@ META_VERSION = "io.modelcontextprotocol/protocolVersion"
141
149
  META_CAPABILITIES = "io.modelcontextprotocol/clientCapabilities"
142
150
  META_SERVER_INFO = "io.modelcontextprotocol/serverInfo"
143
151
 
144
- INVALID_PARAMS = -32602
152
+ PARSE_ERROR = -32700
153
+ INVALID_REQUEST = -32600
145
154
  METHOD_NOT_FOUND = -32601
155
+ INVALID_PARAMS = -32602
156
+ INTERNAL_ERROR = -32603
146
157
  UNSUPPORTED_PROTOCOL_VERSION = -32022
147
158
 
148
159
  CACHE_HINTS = {"ttlMs": 300_000, "cacheScope": "public"} # the tool list does not change while the server runs
149
160
  INSTRUCTIONS = (
150
161
  "Run physverify checks on physics simulation code after writing or editing it. "
151
- "The tool executes the project's verify_*.py spec files (Python), so use it only on projects you trust."
162
+ "The tools execute the project's verify_*.py spec files and claim files (Python), so use them only on projects you trust."
152
163
  )
153
164
 
154
165
  TOOLS = [
@@ -166,9 +177,31 @@ TOOLS = [
166
177
  "properties": {
167
178
  "specs": {"type": "array", "items": {"type": "string"}, "description": "spec file paths"},
168
179
  "project": {"type": "string", "description": "project folder to search for verify_*.py"},
180
+ "timeout": {"type": "number", "description": f"seconds for the whole run (default {RUN_TIMEOUT:g})"},
181
+ },
182
+ },
183
+ },
184
+ {
185
+ "name": "confirm_claim",
186
+ "description": (
187
+ "Judge one suspected defect with physverify confirm. Takes a claim file that defines CLAIM, "
188
+ "control() and checks() with a reference written from the physics (docs/confirm.md), runs it "
189
+ "in a fresh process and returns the verdict: confirmed, rejected or not decidable, with the "
190
+ "reason and each check's status and message. Appends the result to a ledger file. Runs the "
191
+ "code, so it needs trusted=true or the sandbox (PHYSVERIFY_REPO set)."
192
+ ),
193
+ "inputSchema": {
194
+ "type": "object",
195
+ "properties": {
196
+ "claim": {"type": "string", "description": "path of the claim file"},
197
+ "trusted": {"type": "boolean", "description": "the code under test is trusted (required outside the sandbox)"},
198
+ "ledger": {"type": "string", "description": "ledger file (default confirm_ledger.jsonl)"},
199
+ "timeout": {"type": "number", "description": "seconds for the whole run (default 300)"},
200
+ "allow_dependent_reference": {"type": "string", "description": "reason, recorded, to run although the reference seems to use the code under test"},
169
201
  },
202
+ "required": ["claim"],
170
203
  },
171
- }
204
+ },
172
205
  ]
173
206
 
174
207
 
@@ -186,12 +219,68 @@ def _run_checks_tool(arguments: dict) -> dict:
186
219
  specs = find_specs(Path(arguments.get("project") or "."))
187
220
  if not specs:
188
221
  return {"content": [{"type": "text", "text": "No spec files found (verify_*.py defining checks())."}], "isError": True}
189
- report = run_specs(specs)
222
+ from .confirm import parse_timeout
223
+
224
+ try:
225
+ timeout = parse_timeout(RUN_TIMEOUT if arguments.get("timeout") is None else arguments["timeout"])
226
+ except ValueError as exc:
227
+ return {"content": [{"type": "text", "text": f"run_checks: {exc}"}], "isError": True}
228
+ report = run_specs_isolated(specs, timeout)
190
229
  failed = bool(report["summary"]["not_ok"] or report["load_errors"])
191
230
  text = (failure_summary(report) + "\n\n" if failed else "") + json.dumps(report, indent=2)
192
231
  return {"content": [{"type": "text", "text": text}], "isError": False}
193
232
 
194
233
 
234
+ def run_specs_isolated(specs, timeout: float | None = RUN_TIMEOUT) -> dict:
235
+ """run_specs in a fresh Python process (physverify.isolate): the solver is imported as it is on disk now,
236
+ and a run longer than `timeout` seconds is stopped and reported as a load error of every spec."""
237
+ from .isolate import run_isolated
238
+ from .report import to_json
239
+
240
+ specs = [Path(s).resolve() for s in specs]
241
+ run = run_isolated("specs", specs, timeout)
242
+ if run["status"] == "done":
243
+ return run["result"]
244
+ why = (f"timed out after {timeout:g} s; the run was stopped" if run["status"] == "timeout" else run["error"])
245
+ if run["stderr_tail"]:
246
+ why += "; last output: " + run["stderr_tail"][-500:]
247
+ report = json.loads(to_json([], ", ".join(str(s) for s in specs)))
248
+ report["load_errors"] = [{"spec": str(s), "error": why} for s in specs]
249
+ return report
250
+
251
+
252
+ def _confirm_claim_tool(arguments: dict) -> dict:
253
+ """confirm_claim: the same runner and verdict rules as `physverify confirm`."""
254
+ from .confirm import (
255
+ DEFAULT_LEDGER,
256
+ DEFAULT_TIMEOUT,
257
+ Refused,
258
+ confirm_claim,
259
+ format_verdict,
260
+ parse_timeout,
261
+ )
262
+
263
+ claim = arguments.get("claim")
264
+ if not isinstance(claim, str) or not claim:
265
+ return {"content": [{"type": "text", "text": "confirm_claim needs `claim`, the path of a claim file."}], "isError": True}
266
+ try:
267
+ timeout = parse_timeout(DEFAULT_TIMEOUT if arguments.get("timeout") is None else arguments["timeout"])
268
+ except ValueError as exc:
269
+ return {"content": [{"type": "text", "text": f"confirm_claim: {exc}"}], "isError": True}
270
+ try:
271
+ record = confirm_claim(claim, trusted=arguments.get("trusted") is True,
272
+ ledger=arguments.get("ledger") or DEFAULT_LEDGER,
273
+ timeout=timeout,
274
+ allow_dependent_reference=arguments.get("allow_dependent_reference") or None)
275
+ except Refused as exc:
276
+ return {"content": [{"type": "text", "text": str(exc)}], "isError": True}
277
+ return {"content": [{"type": "text", "text": format_verdict(record) + "\n\n" + json.dumps(record, indent=2)}],
278
+ "isError": False}
279
+
280
+
281
+ TOOL_HANDLERS = {"run_checks": _run_checks_tool, "confirm_claim": _confirm_claim_tool}
282
+
283
+
195
284
  def handle(message: dict):
196
285
  """Answer one JSON-RPC message; returns the response dict, or None for a notification."""
197
286
  from . import __version__
@@ -235,9 +324,10 @@ def handle(message: dict):
235
324
  if method == "tools/list":
236
325
  return modern({"tools": TOOLS}, cacheable=True)
237
326
  if method == "tools/call":
238
- if params.get("name") != "run_checks":
327
+ tool = TOOL_HANDLERS.get(params.get("name"))
328
+ if tool is None:
239
329
  return _error(msg_id, INVALID_PARAMS, f"unknown tool {params.get('name')!r}")
240
- return modern(_run_checks_tool(params.get("arguments") or {}))
330
+ return modern(tool(params.get("arguments") or {}))
241
331
  return _error(msg_id, METHOD_NOT_FOUND, f"method not found: {method}")
242
332
 
243
333
  # Legacy era, after the handshake: results carry no resultType or cache hints.
@@ -246,12 +336,23 @@ def handle(message: dict):
246
336
  if method == "tools/list":
247
337
  return ok({"tools": TOOLS})
248
338
  if method == "tools/call":
249
- if params.get("name") != "run_checks":
339
+ tool = TOOL_HANDLERS.get(params.get("name"))
340
+ if tool is None:
250
341
  return _error(msg_id, INVALID_PARAMS, f"unknown tool {params.get('name')!r}")
251
- return ok(_run_checks_tool(params.get("arguments") or {}))
342
+ return ok(tool(params.get("arguments") or {}))
252
343
  return _error(msg_id, METHOD_NOT_FOUND, f"method not found: {method}")
253
344
 
254
345
 
346
+ def _respond(message):
347
+ """handle(), but a message that is not an object, or an exception, becomes an error response; the server keeps running."""
348
+ if not isinstance(message, dict): # a batch, a number, a string: not a request this server answers
349
+ return _error(None, INVALID_REQUEST, "invalid request: expected a JSON object")
350
+ try:
351
+ return handle(message)
352
+ except Exception as exc: # report the failure to the client
353
+ return _error(message.get("id"), INTERNAL_ERROR, f"{type(exc).__name__}: {exc}")
354
+
355
+
255
356
  def mcp_main(stdin=None, stdout=None) -> int:
256
357
  stdin = stdin or sys.stdin
257
358
  stdout = stdout or sys.stdout
@@ -262,12 +363,9 @@ def mcp_main(stdin=None, stdout=None) -> int:
262
363
  try:
263
364
  message = json.loads(line)
264
365
  except json.JSONDecodeError:
265
- response = {"jsonrpc": "2.0", "id": None, "error": {"code": -32700, "message": "parse error"}}
366
+ response = _error(None, PARSE_ERROR, "parse error")
266
367
  else:
267
- try:
268
- response = handle(message)
269
- except Exception as exc: # keep the server alive; report the failure to the client
270
- response = {"jsonrpc": "2.0", "id": message.get("id"), "error": {"code": -32603, "message": f"{type(exc).__name__}: {exc}"}}
368
+ response = _respond(message)
271
369
  if response is not None:
272
370
  stdout.write(json.dumps(response) + "\n")
273
371
  stdout.flush()