physverify 0.4.1__tar.gz → 0.4.2__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.
- physverify-0.4.2/PKG-INFO +89 -0
- physverify-0.4.2/README.md +48 -0
- {physverify-0.4.1 → physverify-0.4.2}/pyproject.toml +4 -3
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/__init__.py +1 -1
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/checks.py +1 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/cli.py +15 -1
- physverify-0.4.2/src/physverify/demo.py +75 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/matrix.py +121 -5
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/order.py +13 -2
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/report.py +20 -2
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/scaffold.py +8 -0
- physverify-0.4.2/src/physverify.egg-info/PKG-INFO +89 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify.egg-info/SOURCES.txt +6 -1
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify.egg-info/requires.txt +5 -0
- physverify-0.4.2/tests/test_awkward_input.py +74 -0
- physverify-0.4.2/tests/test_docs_examples.py +28 -0
- physverify-0.4.2/tests/test_figures.py +14 -0
- physverify-0.4.2/tests/test_versions.py +21 -0
- physverify-0.4.1/PKG-INFO +0 -220
- physverify-0.4.1/README.md +0 -183
- physverify-0.4.1/src/physverify.egg-info/PKG-INFO +0 -220
- {physverify-0.4.1 → physverify-0.4.2}/LICENSE +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/setup.cfg +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/__main__.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/adapters/__init__.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/adapters/double_pendulum.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/adapters/lorenz.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/adapters/projectile_v1.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/adapters/projectile_v2.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/adapters/three_body.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/agent.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/butcher.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/compare.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/constants.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/loaders.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/method_bugs.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/mms.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/problems/__init__.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/problems/_common.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/problems/heat1d.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/problems/kepler.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/problems/pendulum.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/problems/projectile.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/problems/sod.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/pytest_plugin.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/references.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/smoke.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/steppers.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/stochastic/__init__.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/stochastic/calibrate.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/stochastic/checks.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/stochastic/exact.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/stochastic/ising.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/stochastic/matrix.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/stochastic/reference.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/stochastic/sampler.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/stochastic/stats.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/studies.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify/symbolic.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify.egg-info/dependency_links.txt +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify.egg-info/entry_points.txt +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/src/physverify.egg-info/top_level.txt +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/tests/test_agent.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/tests/test_batch7_lessons.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/tests/test_butcher_properties.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/tests/test_cli.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/tests/test_corpus_lessons.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/tests/test_ising.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/tests/test_method_bugs.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/tests/test_mms.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/tests/test_new_problems.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/tests/test_order.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/tests/test_properties.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/tests/test_real_projects.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/tests/test_reference_problems.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/tests/test_references.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/tests/test_scaffold.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/tests/test_seeded_bugs.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/tests/test_studies.py +0 -0
- {physverify-0.4.1 → physverify-0.4.2}/tests/test_v040_helpers.py +0 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: physverify
|
|
3
|
+
Version: 0.4.2
|
|
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://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
|
+
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: pytest-cov>=6.1; extra == "dev"
|
|
35
|
+
Requires-Dist: hypothesis>=6; extra == "dev"
|
|
36
|
+
Requires-Dist: sympy>=1.12; extra == "dev"
|
|
37
|
+
Provides-Extra: docs
|
|
38
|
+
Requires-Dist: mkdocs<2,>=1.6; extra == "docs"
|
|
39
|
+
Requires-Dist: mkdocs-material<10,>=9.5; extra == "docs"
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# physverify
|
|
43
|
+
|
|
44
|
+
[](https://github.com/rcgiri-physics/physverify/actions/workflows/ci.yml) [](https://pypi.org/project/physverify/) [](https://pypi.org/project/physverify/) [](https://doi.org/10.5281/zenodo.23534512) [](https://rcgiri-physics.github.io/physverify/)
|
|
45
|
+
|
|
46
|
+
**Checks that a simulation solves the equations it claims, at the order of accuracy it claims, not just that it runs.**
|
|
47
|
+
|
|
48
|
+
physverify is actively evolving (v0.4.2), welcoming contributions and test cases from the computational physics community. Scope: numerical solutions of ODEs and PDEs (time integration, convergence, conservation), plus a few Monte Carlo checks. It needs only NumPy.
|
|
49
|
+
|
|
50
|
+

|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install physverify
|
|
54
|
+
physverify demo
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The demo checks a correct solver (all checks pass), then the same solver with the damping 10% too large (all fail):
|
|
58
|
+
|
|
59
|
+

|
|
60
|
+
|
|
61
|
+
## Use it on your own code
|
|
62
|
+
|
|
63
|
+
- **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`.
|
|
64
|
+
- **A Runge-Kutta table:** `check_tableau(A, b, c, claimed=3)` from `physverify.butcher` checks its order, stage order and stability from the coefficients alone.
|
|
65
|
+
- **Errors you already have:** `check_order(hs, errors, expected=2)` gives PASS, FAIL or INCONCLUSIVE with a reason.
|
|
66
|
+
|
|
67
|
+
## Run it in CI
|
|
68
|
+
|
|
69
|
+
A failed check fails the job, like a broken unit test ([CI guide](https://rcgiri-physics.github.io/physverify/ci/): options, pre-commit, pytest):
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
- uses: rcgiri-physics/physverify@v0.4.2
|
|
73
|
+
with:
|
|
74
|
+
specs: tests/verify_*.py
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
[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/)
|
|
78
|
+
|
|
79
|
+
## Does it catch bugs?
|
|
80
|
+
|
|
81
|
+
Of 37 bugs seeded into five small solvers, it flagged 35 (plain "compare with one answer" tests: 27), with no false alarms. The bugs and checks were written by the same person, so this shows the tool works as designed, not how it does on other people's code. On public physics repositories it found real bugs, for example Runge-Kutta tables with a wrong order label; the counts are not a bug rate. Details: [results](https://rcgiri-physics.github.io/physverify/results/), and [results/README.md](https://github.com/rcgiri-physics/physverify/blob/main/results/README.md) to regenerate every table.
|
|
82
|
+
|
|
83
|
+
## Limits
|
|
84
|
+
|
|
85
|
+
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/).
|
|
86
|
+
|
|
87
|
+
## Contributing, citing, licence
|
|
88
|
+
|
|
89
|
+
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). 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). MIT licence, copyright (c) 2026 Ram Chandra Giri.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# physverify
|
|
2
|
+
|
|
3
|
+
[](https://github.com/rcgiri-physics/physverify/actions/workflows/ci.yml) [](https://pypi.org/project/physverify/) [](https://pypi.org/project/physverify/) [](https://doi.org/10.5281/zenodo.23534512) [](https://rcgiri-physics.github.io/physverify/)
|
|
4
|
+
|
|
5
|
+
**Checks that a simulation solves the equations it claims, at the order of accuracy it claims, not just that it runs.**
|
|
6
|
+
|
|
7
|
+
physverify is actively evolving (v0.4.2), welcoming contributions and test cases from the computational physics community. Scope: numerical solutions of ODEs and PDEs (time integration, convergence, conservation), plus a few Monte Carlo checks. It needs only NumPy.
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install physverify
|
|
13
|
+
physverify demo
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The demo checks a correct solver (all checks pass), then the same solver with the damping 10% too large (all fail):
|
|
17
|
+
|
|
18
|
+

|
|
19
|
+
|
|
20
|
+
## Use it on your own code
|
|
21
|
+
|
|
22
|
+
- **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`.
|
|
23
|
+
- **A Runge-Kutta table:** `check_tableau(A, b, c, claimed=3)` from `physverify.butcher` checks its order, stage order and stability from the coefficients alone.
|
|
24
|
+
- **Errors you already have:** `check_order(hs, errors, expected=2)` gives PASS, FAIL or INCONCLUSIVE with a reason.
|
|
25
|
+
|
|
26
|
+
## Run it in CI
|
|
27
|
+
|
|
28
|
+
A failed check fails the job, like a broken unit test ([CI guide](https://rcgiri-physics.github.io/physverify/ci/): options, pre-commit, pytest):
|
|
29
|
+
|
|
30
|
+
```yaml
|
|
31
|
+
- uses: rcgiri-physics/physverify@v0.4.2
|
|
32
|
+
with:
|
|
33
|
+
specs: tests/verify_*.py
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
[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/)
|
|
37
|
+
|
|
38
|
+
## Does it catch bugs?
|
|
39
|
+
|
|
40
|
+
Of 37 bugs seeded into five small solvers, it flagged 35 (plain "compare with one answer" tests: 27), with no false alarms. The bugs and checks were written by the same person, so this shows the tool works as designed, not how it does on other people's code. On public physics repositories it found real bugs, for example Runge-Kutta tables with a wrong order label; the counts are not a bug rate. Details: [results](https://rcgiri-physics.github.io/physverify/results/), and [results/README.md](https://github.com/rcgiri-physics/physverify/blob/main/results/README.md) to regenerate every table.
|
|
41
|
+
|
|
42
|
+
## Limits
|
|
43
|
+
|
|
44
|
+
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/).
|
|
45
|
+
|
|
46
|
+
## Contributing, citing, licence
|
|
47
|
+
|
|
48
|
+
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). 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). MIT licence, copyright (c) 2026 Ram Chandra Giri.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "physverify"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.2"
|
|
8
8
|
description = "Verification checks for physics simulation code: manufactured solutions, order-of-accuracy studies and a seeded-bug benchmark."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -32,13 +32,14 @@ dependencies = ["numpy>=1.24"]
|
|
|
32
32
|
|
|
33
33
|
[project.urls]
|
|
34
34
|
Homepage = "https://github.com/rcgiri-physics/physverify"
|
|
35
|
-
Documentation = "https://
|
|
35
|
+
Documentation = "https://rcgiri-physics.github.io/physverify/"
|
|
36
36
|
Issues = "https://github.com/rcgiri-physics/physverify/issues"
|
|
37
37
|
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", "hypothesis>=6", "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"]
|
|
42
43
|
|
|
43
44
|
[project.scripts]
|
|
44
45
|
physverify = "physverify.cli:main"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Command line.
|
|
2
2
|
|
|
3
3
|
physverify run SPEC.py [SPEC.py ...] [--json FILE|-] [--html FILE] [--markdown FILE] [--quiet]
|
|
4
|
+
physverify demo (a correct and a broken solver, no files needed)
|
|
4
5
|
physverify init [SPEC.py] [--solver MODULE] [--function NAME] [--dim N] [--order P]
|
|
5
6
|
physverify matrix [OUT.md]
|
|
6
7
|
physverify compare [OUT.md]
|
|
@@ -42,7 +43,7 @@ def expand_specs(patterns) -> list[str]:
|
|
|
42
43
|
|
|
43
44
|
|
|
44
45
|
def _cmd_run(args) -> int:
|
|
45
|
-
from .report import load_spec, run_checks, to_html, to_json, to_markdown, to_text
|
|
46
|
+
from .report import NOTE, load_spec, run_checks, to_html, to_json, to_markdown, to_text
|
|
46
47
|
|
|
47
48
|
try:
|
|
48
49
|
args.specs = expand_specs(args.specs)
|
|
@@ -56,6 +57,9 @@ def _cmd_run(args) -> int:
|
|
|
56
57
|
except Exception as exc:
|
|
57
58
|
print(f"physverify: cannot load {spec}: {type(exc).__name__}: {exc}", file=sys.stderr)
|
|
58
59
|
return 2
|
|
60
|
+
if not checks:
|
|
61
|
+
print(f"physverify: {spec}: checks() returned no checks, so nothing was tested", file=sys.stderr)
|
|
62
|
+
return 2
|
|
59
63
|
outcomes += run_checks(checks, args.budget)
|
|
60
64
|
source = ", ".join(str(Path(s)) for s in args.specs)
|
|
61
65
|
if args.json == "-":
|
|
@@ -63,6 +67,7 @@ def _cmd_run(args) -> int:
|
|
|
63
67
|
else:
|
|
64
68
|
if not args.quiet:
|
|
65
69
|
print(to_text(outcomes))
|
|
70
|
+
print(NOTE)
|
|
66
71
|
if args.json:
|
|
67
72
|
Path(args.json).write_text(to_json(outcomes, source), encoding="utf-8")
|
|
68
73
|
if args.html:
|
|
@@ -87,6 +92,12 @@ def _cmd_init(args) -> int:
|
|
|
87
92
|
return 0
|
|
88
93
|
|
|
89
94
|
|
|
95
|
+
def _cmd_demo(args) -> int:
|
|
96
|
+
from .demo import main as demo_main
|
|
97
|
+
|
|
98
|
+
return demo_main()
|
|
99
|
+
|
|
100
|
+
|
|
90
101
|
def _cmd_matrix(args) -> int:
|
|
91
102
|
from . import matrix
|
|
92
103
|
|
|
@@ -149,6 +160,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
149
160
|
" and a check still running at twice the budget is reported INCONCLUSIVE")
|
|
150
161
|
run.set_defaults(func=_cmd_run)
|
|
151
162
|
|
|
163
|
+
d = sub.add_parser("demo", help="run the checks on a correct and on a broken example solver (no files needed)")
|
|
164
|
+
d.set_defaults(func=_cmd_demo)
|
|
165
|
+
|
|
152
166
|
i = sub.add_parser("init", help="write a starter spec for a fixed-step ODE solver")
|
|
153
167
|
i.add_argument("spec", nargs="?", default="verify_solver.py", help="spec file to write (default verify_solver.py)")
|
|
154
168
|
i.add_argument("--solver", default="solver", help="module that holds the solver, importable from the spec's folder (default solver)")
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""`physverify demo`: the damped-oscillator example, shipped in the package.
|
|
2
|
+
|
|
3
|
+
It runs the checks on a correct midpoint solver, then on a copy whose damping
|
|
4
|
+
is 10% too large, so a first run needs only `pip install physverify`.
|
|
5
|
+
The same solver and spec, as files to copy, are in examples/ in the repository.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from .studies import ode_checks
|
|
13
|
+
from .mms import ManufacturedODE
|
|
14
|
+
|
|
15
|
+
OMEGA, GAMMA = 3.0, 0.4
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def make_solver(damping_factor: float = 1.0):
|
|
19
|
+
"""Midpoint method for x'' = -omega^2 x - 2 gamma x', with an additive source term."""
|
|
20
|
+
|
|
21
|
+
def rhs(t, y):
|
|
22
|
+
damping = 2.0 * GAMMA * damping_factor
|
|
23
|
+
return np.array([y[1], -(OMEGA**2) * y[0] - damping * y[1]])
|
|
24
|
+
|
|
25
|
+
def integrate(source, y0, t_end, n_steps, trajectory=False):
|
|
26
|
+
h = t_end / n_steps
|
|
27
|
+
y = np.array(y0, dtype=float)
|
|
28
|
+
states = [y]
|
|
29
|
+
for i in range(n_steps):
|
|
30
|
+
t = i * h
|
|
31
|
+
k1 = rhs(t, y) + source(t)
|
|
32
|
+
mid = y + 0.5 * h * k1
|
|
33
|
+
y = y + h * (rhs(t + 0.5 * h, mid) + source(t + 0.5 * h))
|
|
34
|
+
states.append(y)
|
|
35
|
+
return np.array(states) if trajectory else y
|
|
36
|
+
|
|
37
|
+
return integrate
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def reference_rhs(t, y):
|
|
41
|
+
"""The equations, written from the physics, not copied from the solver."""
|
|
42
|
+
return np.array([y[1], -(OMEGA**2) * y[0] - 2.0 * GAMMA * y[1]])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def checks(damping_factor: float = 1.0):
|
|
46
|
+
integrate = make_solver(damping_factor)
|
|
47
|
+
mms = ManufacturedODE(lambda t: np.array([np.sin(1.3 * t) + 0.2, 1.04 * np.cos(1.3 * t)]), reference_rhs)
|
|
48
|
+
return ode_checks(
|
|
49
|
+
"midpoint damped oscillator",
|
|
50
|
+
integrate,
|
|
51
|
+
rhs_ref=reference_rhs,
|
|
52
|
+
mms=mms,
|
|
53
|
+
y0_free=np.array([1.0, 0.0]),
|
|
54
|
+
t_end=1.0,
|
|
55
|
+
ns=(10, 20, 40, 80, 160),
|
|
56
|
+
order=2.0,
|
|
57
|
+
patch=(np.array([0.5, -0.3]), np.array([0.8, 1.1])),
|
|
58
|
+
integrate_trajectory=lambda s, y0, t, n: integrate(s, y0, t, n, trajectory=True),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def main() -> int:
|
|
63
|
+
"""Exit 0 when the correct solver passes and the broken one fails, as they should."""
|
|
64
|
+
from .report import NOTE, run_checks, to_text
|
|
65
|
+
|
|
66
|
+
print("1. A correct midpoint solver for a damped oscillator:\n")
|
|
67
|
+
good = run_checks(checks())
|
|
68
|
+
print(to_text(good))
|
|
69
|
+
print("\n2. The same solver with the damping 10% too large (a typical slip):\n")
|
|
70
|
+
bad = run_checks(checks(damping_factor=1.1))
|
|
71
|
+
print(to_text(bad))
|
|
72
|
+
ok = not any(o.result.failed for o in good) and any(o.result.failed for o in bad)
|
|
73
|
+
print(f"\n{NOTE}")
|
|
74
|
+
print("\nTo check your own solver, start a spec with: physverify init verify_solver.py --solver mysolver --order 2")
|
|
75
|
+
return 0 if ok else 1
|
|
@@ -2,17 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
python -m physverify.matrix results/detection_matrix.md
|
|
4
4
|
|
|
5
|
+
This also writes detection_matrix.json (every check's full status, message and
|
|
6
|
+
run time, with how and where it was made) and run_time.md next to it.
|
|
7
|
+
|
|
5
8
|
The bugs were written by the author of this package, so the catch rate below is
|
|
6
9
|
a measure of these bugs, not of bugs in general.
|
|
7
10
|
"""
|
|
8
11
|
|
|
9
12
|
from __future__ import annotations
|
|
10
13
|
|
|
14
|
+
import json
|
|
11
15
|
import sys
|
|
16
|
+
import time
|
|
17
|
+
from collections import Counter
|
|
12
18
|
from dataclasses import dataclass, field
|
|
13
19
|
from pathlib import Path
|
|
14
20
|
|
|
15
|
-
from .checks import FAMILIES, Check, Result, run_check
|
|
21
|
+
from .checks import FAIL, FAMILIES, Check, Result, run_check
|
|
16
22
|
from .problems import heat1d, kepler, pendulum, projectile, sod
|
|
17
23
|
|
|
18
24
|
PROBLEMS = {m.NAME: m for m in (projectile, pendulum, kepler, heat1d, sod)}
|
|
@@ -23,6 +29,7 @@ class Row:
|
|
|
23
29
|
problem: str
|
|
24
30
|
bug: str | None # None is the bug-free baseline
|
|
25
31
|
results: list = field(default_factory=list) # list of (Check, Result)
|
|
32
|
+
seconds: list = field(default_factory=list) # run time of each check, when measured
|
|
26
33
|
|
|
27
34
|
def flagged(self) -> dict:
|
|
28
35
|
"""family -> labels of the checks that did not pass"""
|
|
@@ -32,6 +39,10 @@ class Row:
|
|
|
32
39
|
out.setdefault(check.family, []).append(check.label)
|
|
33
40
|
return out
|
|
34
41
|
|
|
42
|
+
def flagged_by_fail(self) -> bool:
|
|
43
|
+
"""At least one check gave FAIL, a clean detection (not only INCONCLUSIVE or ERROR)."""
|
|
44
|
+
return any(result.status == FAIL for _, result in self.results)
|
|
45
|
+
|
|
35
46
|
def families_present(self) -> set:
|
|
36
47
|
return {check.family for check, _ in self.results}
|
|
37
48
|
|
|
@@ -40,14 +51,23 @@ def evaluate(module, bugs=frozenset()) -> list:
|
|
|
40
51
|
return [(check, run_check(check)) for check in module.checks(bugs)]
|
|
41
52
|
|
|
42
53
|
|
|
54
|
+
def evaluate_timed(problem, bug) -> Row:
|
|
55
|
+
row = Row(problem, bug)
|
|
56
|
+
for check in PROBLEMS[problem].checks(frozenset([bug]) if bug else frozenset()):
|
|
57
|
+
start = time.perf_counter()
|
|
58
|
+
row.results.append((check, run_check(check)))
|
|
59
|
+
row.seconds.append(time.perf_counter() - start)
|
|
60
|
+
return row
|
|
61
|
+
|
|
62
|
+
|
|
43
63
|
def build_rows(problems=None) -> list:
|
|
44
64
|
rows = []
|
|
45
65
|
for name, module in PROBLEMS.items():
|
|
46
66
|
if problems and name not in problems:
|
|
47
67
|
continue
|
|
48
|
-
rows.append(
|
|
68
|
+
rows.append(evaluate_timed(name, None))
|
|
49
69
|
for bug in module.BUGS:
|
|
50
|
-
rows.append(
|
|
70
|
+
rows.append(evaluate_timed(name, bug))
|
|
51
71
|
return rows
|
|
52
72
|
|
|
53
73
|
|
|
@@ -74,6 +94,7 @@ def to_markdown(rows) -> str:
|
|
|
74
94
|
caught = [r for r in bug_rows if r.flagged()]
|
|
75
95
|
missed = [r for r in bug_rows if not r.flagged()]
|
|
76
96
|
false_alarms = [r for r in rows if r.bug is None and r.flagged()]
|
|
97
|
+
by_fail = [r for r in caught if r.flagged_by_fail()]
|
|
77
98
|
by_family = {f: sum(1 for r in bug_rows if f in r.flagged()) for f in FAMILIES}
|
|
78
99
|
only_one = {f: sum(1 for r in bug_rows if set(r.flagged()) == {f}) for f in FAMILIES}
|
|
79
100
|
summary = [
|
|
@@ -81,6 +102,11 @@ def to_markdown(rows) -> str:
|
|
|
81
102
|
f"Bugs seeded: {len(bug_rows)}. Flagged by at least one check: {len(caught)}. Missed by all: {len(missed)}"
|
|
82
103
|
+ (" (" + ", ".join(f"{r.problem}/{r.bug}" for r in missed) + ")" if missed else "")
|
|
83
104
|
+ ".",
|
|
105
|
+
f"Of the {len(caught)} flagged: {len(by_fail)} by at least one FAIL, {len(caught) - len(by_fail)} only by"
|
|
106
|
+
" INCONCLUSIVE or ERROR"
|
|
107
|
+
+ (" (" + ", ".join(f"{r.problem}/{r.bug}" for r in caught if not r.flagged_by_fail()) + ")"
|
|
108
|
+
if len(by_fail) < len(caught) else "")
|
|
109
|
+
+ ".",
|
|
84
110
|
f"Bug-free baselines with a false alarm: {len(false_alarms)} of {len(rows) - len(bug_rows)}.",
|
|
85
111
|
"Bugs flagged per family: " + ", ".join(f"{f} {n}" for f, n in by_family.items()) + ".",
|
|
86
112
|
"Bugs flagged by that family alone: " + ", ".join(f"{f} {n}" for f, n in only_one.items()) + ".",
|
|
@@ -88,6 +114,73 @@ def to_markdown(rows) -> str:
|
|
|
88
114
|
return head + "\n".join(lines) + "\n" + "\n".join(summary) + "\n"
|
|
89
115
|
|
|
90
116
|
|
|
117
|
+
def provenance() -> dict:
|
|
118
|
+
"""How and where a result file was made; kept out of the Markdown so the committed tables stay comparable."""
|
|
119
|
+
import datetime
|
|
120
|
+
import platform
|
|
121
|
+
import subprocess
|
|
122
|
+
|
|
123
|
+
import numpy
|
|
124
|
+
|
|
125
|
+
from . import __version__
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
commit = subprocess.run(["git", "rev-parse", "HEAD"], capture_output=True, text=True, timeout=10,
|
|
129
|
+
cwd=Path(__file__).resolve().parent).stdout.strip() or None
|
|
130
|
+
except (OSError, subprocess.SubprocessError):
|
|
131
|
+
commit = None
|
|
132
|
+
return {
|
|
133
|
+
"physverify": __version__,
|
|
134
|
+
"git_commit": commit,
|
|
135
|
+
"python": platform.python_version(),
|
|
136
|
+
"numpy": numpy.__version__,
|
|
137
|
+
"platform": platform.platform(),
|
|
138
|
+
"date": datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds"),
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def to_json(rows) -> str:
|
|
143
|
+
"""One record per (problem, bug, check) with the full status, message, prediction and run time."""
|
|
144
|
+
records = []
|
|
145
|
+
for row in rows:
|
|
146
|
+
predicted = sorted(PROBLEMS[row.problem].BUGS[row.bug].expected) if row.bug else []
|
|
147
|
+
seconds = row.seconds or [None] * len(row.results)
|
|
148
|
+
for (check, result), sec in zip(row.results, seconds):
|
|
149
|
+
records.append({
|
|
150
|
+
"problem": row.problem,
|
|
151
|
+
"bug": row.bug,
|
|
152
|
+
"family": check.family,
|
|
153
|
+
"check": check.label,
|
|
154
|
+
"status": result.status,
|
|
155
|
+
"message": result.message,
|
|
156
|
+
"predicted_to_flag": check.family in predicted,
|
|
157
|
+
"seconds": None if sec is None else round(sec, 4),
|
|
158
|
+
})
|
|
159
|
+
return json.dumps({"provenance": provenance(), "records": records}, indent=1) + "\n"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def run_time(rows) -> str:
|
|
163
|
+
"""Total run time per check family and problem, over the baseline and every bug (not deterministic)."""
|
|
164
|
+
totals: dict = {}
|
|
165
|
+
for row in rows:
|
|
166
|
+
for (check, _), sec in zip(row.results, row.seconds):
|
|
167
|
+
totals.setdefault(row.problem, {}).setdefault(check.family, 0.0)
|
|
168
|
+
totals[row.problem][check.family] += sec
|
|
169
|
+
lines = [
|
|
170
|
+
"# Run time per check family",
|
|
171
|
+
"",
|
|
172
|
+
"Seconds, summed over the baseline and every seeded bug of each problem, on the machine recorded in",
|
|
173
|
+
"detection_matrix.json (`provenance`). Generated by `python scripts/reproduce_all.py`; it varies between runs.",
|
|
174
|
+
"",
|
|
175
|
+
"| Problem | " + " | ".join(FAMILIES) + " | Total |",
|
|
176
|
+
"|---|" + "---|" * (len(FAMILIES) + 1),
|
|
177
|
+
]
|
|
178
|
+
for problem, fam in totals.items():
|
|
179
|
+
cells = " | ".join(f"{fam[f]:.2f}" if f in fam else "n/a" for f in FAMILIES)
|
|
180
|
+
lines.append(f"| {problem} | {cells} | {sum(fam.values()):.2f} |")
|
|
181
|
+
return "\n".join(lines) + "\n"
|
|
182
|
+
|
|
183
|
+
|
|
91
184
|
def details(rows) -> str:
|
|
92
185
|
"""Per-check messages, for reading why a cell is what it is."""
|
|
93
186
|
out = []
|
|
@@ -99,13 +192,28 @@ def details(rows) -> str:
|
|
|
99
192
|
return "\n".join(out)
|
|
100
193
|
|
|
101
194
|
|
|
195
|
+
REPO = "https://github.com/rcgiri-physics/physverify/blob/main/"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def bug_switches(problem: str, bug: str) -> list:
|
|
199
|
+
"""(path, line number, code) of every line that switches the bug on: `"bug" in bugs`."""
|
|
200
|
+
root = Path(__file__).resolve().parents[2]
|
|
201
|
+
files = [Path(PROBLEMS[problem].__file__), Path(__file__).resolve().parent / "problems" / "_common.py"]
|
|
202
|
+
out = []
|
|
203
|
+
for f in files:
|
|
204
|
+
for n, line in enumerate(f.read_text(encoding="utf-8").splitlines(), 1):
|
|
205
|
+
if f'"{bug}" in bugs' in line:
|
|
206
|
+
out.append((f.resolve().relative_to(root).as_posix(), n, line.strip()))
|
|
207
|
+
return out
|
|
208
|
+
|
|
209
|
+
|
|
102
210
|
def catalogue(rows) -> str:
|
|
103
211
|
"""One entry per seeded bug: what is changed, the expected symptom, predicted and observed families."""
|
|
104
212
|
out = [
|
|
105
213
|
"# Seeded-bug catalogue",
|
|
106
214
|
"",
|
|
107
215
|
"Generated by `python -m physverify.matrix results/detection_matrix.md`. Each bug is switched on by",
|
|
108
|
-
"name in the problem's solver
|
|
216
|
+
"name in the problem's solver; the lines that switch it on are linked under each bug.",
|
|
109
217
|
"Predictions and symptoms were written from reasoning before the checks were run.",
|
|
110
218
|
"",
|
|
111
219
|
]
|
|
@@ -121,8 +229,14 @@ def catalogue(rows) -> str:
|
|
|
121
229
|
f"- Expected symptom: {info.symptom or '(not recorded)'}",
|
|
122
230
|
f"- Predicted to be flagged by: {', '.join(sorted(info.expected)) or 'nothing (blind spot)'}",
|
|
123
231
|
f"- Flagged by: {', '.join(flagged) or 'nothing'}",
|
|
124
|
-
"",
|
|
232
|
+
"- Outcome of its " + str(len(row.results)) + " checks: " + ", ".join(
|
|
233
|
+
f"{n} {s}" for s, n in sorted(Counter(r.status for _, r in row.results).items())),
|
|
125
234
|
]
|
|
235
|
+
switches = bug_switches(row.problem, row.bug)
|
|
236
|
+
if switches:
|
|
237
|
+
out.append("- Where it is switched on:")
|
|
238
|
+
out += [f" - [{path}:{n}]({REPO}{path}#L{n}): `{code}`" for path, n, code in switches]
|
|
239
|
+
out.append("")
|
|
126
240
|
return "\n".join(out)
|
|
127
241
|
|
|
128
242
|
|
|
@@ -153,6 +267,8 @@ def main(argv=None) -> int:
|
|
|
153
267
|
path.with_name(path.stem + "_details.md").write_text(details(rows), encoding="utf-8")
|
|
154
268
|
path.with_name("heat1d_by_check.md").write_text(by_check_markdown(rows, "heat1d"), encoding="utf-8")
|
|
155
269
|
path.with_name("bug_catalogue.md").write_text(catalogue(rows), encoding="utf-8")
|
|
270
|
+
path.with_suffix(".json").write_text(to_json(rows), encoding="utf-8")
|
|
271
|
+
path.with_name("run_time.md").write_text(run_time(rows), encoding="utf-8")
|
|
156
272
|
return 0
|
|
157
273
|
|
|
158
274
|
|
|
@@ -54,7 +54,8 @@ def check_exact(hs, errors, *, floor: float = 1e-12, scale: float = 1.0) -> Orde
|
|
|
54
54
|
errs = np.asarray(errors, dtype=float)
|
|
55
55
|
order = np.argsort(-hs)
|
|
56
56
|
hs, errs = hs[order], errs[order]
|
|
57
|
-
base = dict(expected=float("inf"), hs=tuple(hs), errors=tuple(errs)
|
|
57
|
+
base = dict(expected=float("inf"), hs=tuple(hs), errors=tuple(errs),
|
|
58
|
+
acceptance=f"every error at most {floor:.2g} (round-off)")
|
|
58
59
|
if not np.all(np.isfinite(errs)):
|
|
59
60
|
return OrderResult(FAIL, "non-finite error: the solution diverged or produced NaN", **base)
|
|
60
61
|
worst = float(errs.max())
|
|
@@ -100,9 +101,19 @@ def check_order(
|
|
|
100
101
|
floor = effective_floor(floor, scale)
|
|
101
102
|
hs = np.asarray(hs, dtype=float)
|
|
102
103
|
errs = np.asarray(errors, dtype=float)
|
|
104
|
+
if hs.shape != errs.shape:
|
|
105
|
+
return OrderResult(INCONCLUSIVE, f"{hs.size} step sizes but {errs.size} errors; they must match",
|
|
106
|
+
expected=float(expected), hs=tuple(hs), errors=tuple(errs))
|
|
103
107
|
order = np.argsort(-hs) # coarse to fine
|
|
104
108
|
hs, errs = hs[order], errs[order]
|
|
105
|
-
base = dict(expected=float(expected), hs=tuple(hs), errors=tuple(errs)
|
|
109
|
+
base = dict(expected=float(expected), hs=tuple(hs), errors=tuple(errs),
|
|
110
|
+
acceptance=f"fitted order within [{expected - tol_low:g}, {expected + tol_high:g}] on errors above {floor:.2g}")
|
|
111
|
+
|
|
112
|
+
if not np.all(np.isfinite(hs) & (hs > 0)):
|
|
113
|
+
return OrderResult(INCONCLUSIVE, "step sizes must be positive and finite; check how h is computed", **base)
|
|
114
|
+
if len(np.unique(hs)) < 2:
|
|
115
|
+
return OrderResult(INCONCLUSIVE, "all step sizes are the same, so no order can be measured; use at least two"
|
|
116
|
+
" different resolutions", **base)
|
|
106
117
|
|
|
107
118
|
if not np.all(np.isfinite(errs)):
|
|
108
119
|
return OrderResult(FAIL, "non-finite error: the solution diverged or produced NaN", **base)
|
|
@@ -54,6 +54,17 @@ def load_spec(path):
|
|
|
54
54
|
return checks
|
|
55
55
|
|
|
56
56
|
|
|
57
|
+
SCHEMA_VERSION = 1 # of the --json output; see docs/api.md
|
|
58
|
+
NOTE = "A PASS means these checks found nothing wrong; it does not prove the simulation correct."
|
|
59
|
+
CLAIMS = { # what each family tests, for the JSON output
|
|
60
|
+
"mms": "the code converges at the claimed order to a manufactured solution of the reference equations",
|
|
61
|
+
"reference": "the code converges at the claimed order to an independently computed reference solution",
|
|
62
|
+
"invariant": "the drift of a conserved quantity shrinks as the resolution is refined",
|
|
63
|
+
"limit": "the code reproduces a known limiting or special case",
|
|
64
|
+
"patch": "the code reproduces a solution it should represent exactly, to round-off",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
57
68
|
def _number(x):
|
|
58
69
|
x = float(x)
|
|
59
70
|
return x if math.isfinite(x) else None
|
|
@@ -67,12 +78,15 @@ def outcome_to_dict(o: Outcome) -> dict:
|
|
|
67
78
|
"ok": o.result.ok,
|
|
68
79
|
"message": o.result.message,
|
|
69
80
|
"seconds": round(o.seconds, 4),
|
|
81
|
+
"claim": CLAIMS.get(o.check.family, ""),
|
|
70
82
|
}
|
|
71
83
|
if isinstance(o.result, OrderResult):
|
|
72
84
|
d["expected_order"] = _number(o.result.expected)
|
|
73
85
|
d["observed_order"] = _number(o.result.fitted)
|
|
74
86
|
d["h"] = [_number(h) for h in o.result.hs]
|
|
75
87
|
d["error"] = [_number(e) for e in o.result.errors]
|
|
88
|
+
if o.result.acceptance:
|
|
89
|
+
d["acceptance"] = o.result.acceptance
|
|
76
90
|
return d
|
|
77
91
|
|
|
78
92
|
|
|
@@ -90,10 +104,12 @@ def to_json(outcomes, source: str = "") -> str:
|
|
|
90
104
|
from . import __version__
|
|
91
105
|
|
|
92
106
|
payload = {
|
|
107
|
+
"schema_version": SCHEMA_VERSION,
|
|
93
108
|
"physverify": __version__,
|
|
94
109
|
"source": source,
|
|
95
110
|
"summary": summary(outcomes),
|
|
96
111
|
"checks": [outcome_to_dict(o) for o in outcomes],
|
|
112
|
+
"note": NOTE,
|
|
97
113
|
}
|
|
98
114
|
return json.dumps(payload, indent=2)
|
|
99
115
|
|
|
@@ -101,7 +117,8 @@ def to_json(outcomes, source: str = "") -> str:
|
|
|
101
117
|
def to_text(outcomes) -> str:
|
|
102
118
|
lines = [f"{o.check.family:10s} {o.result.status:12s} {o.check.label}: {o.result.message}" for o in outcomes]
|
|
103
119
|
s = summary(outcomes)
|
|
104
|
-
|
|
120
|
+
skipped = f", {s['skipped']} skipped" if s["skipped"] else ""
|
|
121
|
+
lines.append(f"{s['ok']} of {s['total']} checks passed{skipped}")
|
|
105
122
|
return "\n".join(lines)
|
|
106
123
|
|
|
107
124
|
|
|
@@ -125,6 +142,7 @@ def to_markdown(outcomes, source: str = "") -> str:
|
|
|
125
142
|
lines.append(
|
|
126
143
|
f"| {cell(o.check.family)} | {cell(o.result.status)} | {cell(o.check.label)} | {observed} | {cell(o.result.message)} |"
|
|
127
144
|
)
|
|
145
|
+
lines += ["", f"_{NOTE}_"]
|
|
128
146
|
return "\n".join(lines) + "\n"
|
|
129
147
|
|
|
130
148
|
|
|
@@ -210,5 +228,5 @@ def to_html(outcomes, title: str = "physverify report", source: str = "") -> str
|
|
|
210
228
|
'<div class="wrap"><table><thead><tr><th>Status</th><th>Family</th><th>Check</th><th>Message</th>'
|
|
211
229
|
"<th>Observed order</th><th>Convergence</th></tr></thead><tbody>"
|
|
212
230
|
+ "".join(rows)
|
|
213
|
-
+
|
|
231
|
+
+ f'</tbody></table></div><p class="meta">{html.escape(NOTE)}</p></body></html>\n'
|
|
214
232
|
)
|
|
@@ -24,6 +24,14 @@ taking n_steps fixed steps from t = 0 and adding source(t), an array of
|
|
|
24
24
|
length {dim}, to the right-hand side wherever the solver evaluates it. If the
|
|
25
25
|
solver has no such hook, write a small wrapper in this file that adds one and
|
|
26
26
|
pass the wrapper to ode_checks instead.
|
|
27
|
+
|
|
28
|
+
Before trusting a PASS:
|
|
29
|
+
- reference_rhs written from the physics, not copied from the solver?
|
|
30
|
+
- units and signs stated, and matching the solver's conventions?
|
|
31
|
+
- initial and boundary conditions checked separately?
|
|
32
|
+
- the test reaches the states where bugs hide (large amplitudes, boundaries)?
|
|
33
|
+
- a second, independent check (a conserved quantity or a known limit)?
|
|
34
|
+
See https://rcgiri-physics.github.io/physverify/assumptions/
|
|
27
35
|
"""
|
|
28
36
|
|
|
29
37
|
import numpy as np
|