physverify 0.1.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.
- physverify-0.1.0/LICENSE +21 -0
- physverify-0.1.0/PKG-INFO +122 -0
- physverify-0.1.0/README.md +85 -0
- physverify-0.1.0/pyproject.toml +54 -0
- physverify-0.1.0/setup.cfg +4 -0
- physverify-0.1.0/src/physverify/__init__.py +26 -0
- physverify-0.1.0/src/physverify/__main__.py +3 -0
- physverify-0.1.0/src/physverify/adapters/__init__.py +14 -0
- physverify-0.1.0/src/physverify/adapters/double_pendulum.py +55 -0
- physverify-0.1.0/src/physverify/adapters/lorenz.py +152 -0
- physverify-0.1.0/src/physverify/adapters/projectile_v1.py +92 -0
- physverify-0.1.0/src/physverify/adapters/projectile_v2.py +94 -0
- physverify-0.1.0/src/physverify/adapters/three_body.py +283 -0
- physverify-0.1.0/src/physverify/agent.py +265 -0
- physverify-0.1.0/src/physverify/checks.py +59 -0
- physverify-0.1.0/src/physverify/cli.py +105 -0
- physverify-0.1.0/src/physverify/compare.py +104 -0
- physverify-0.1.0/src/physverify/loaders.py +49 -0
- physverify-0.1.0/src/physverify/matrix.py +160 -0
- physverify-0.1.0/src/physverify/mms.py +118 -0
- physverify-0.1.0/src/physverify/order.py +131 -0
- physverify-0.1.0/src/physverify/problems/__init__.py +6 -0
- physverify-0.1.0/src/physverify/problems/_common.py +89 -0
- physverify-0.1.0/src/physverify/problems/heat1d.py +217 -0
- physverify-0.1.0/src/physverify/problems/kepler.py +176 -0
- physverify-0.1.0/src/physverify/problems/pendulum.py +201 -0
- physverify-0.1.0/src/physverify/problems/projectile.py +240 -0
- physverify-0.1.0/src/physverify/problems/sod.py +291 -0
- physverify-0.1.0/src/physverify/pytest_plugin.py +75 -0
- physverify-0.1.0/src/physverify/report.py +189 -0
- physverify-0.1.0/src/physverify/steppers.py +38 -0
- physverify-0.1.0/src/physverify/stochastic/__init__.py +1 -0
- physverify-0.1.0/src/physverify/stochastic/calibrate.py +202 -0
- physverify-0.1.0/src/physverify/stochastic/checks.py +378 -0
- physverify-0.1.0/src/physverify/stochastic/exact.py +205 -0
- physverify-0.1.0/src/physverify/stochastic/ising.py +170 -0
- physverify-0.1.0/src/physverify/stochastic/matrix.py +151 -0
- physverify-0.1.0/src/physverify/stochastic/reference.py +67 -0
- physverify-0.1.0/src/physverify/stochastic/sampler.py +253 -0
- physverify-0.1.0/src/physverify/stochastic/stats.py +137 -0
- physverify-0.1.0/src/physverify/studies.py +146 -0
- physverify-0.1.0/src/physverify/symbolic.py +89 -0
- physverify-0.1.0/src/physverify.egg-info/PKG-INFO +122 -0
- physverify-0.1.0/src/physverify.egg-info/SOURCES.txt +57 -0
- physverify-0.1.0/src/physverify.egg-info/dependency_links.txt +1 -0
- physverify-0.1.0/src/physverify.egg-info/entry_points.txt +5 -0
- physverify-0.1.0/src/physverify.egg-info/requires.txt +9 -0
- physverify-0.1.0/src/physverify.egg-info/top_level.txt +1 -0
- physverify-0.1.0/tests/test_agent.py +196 -0
- physverify-0.1.0/tests/test_cli.py +82 -0
- physverify-0.1.0/tests/test_ising.py +171 -0
- physverify-0.1.0/tests/test_mms.py +73 -0
- physverify-0.1.0/tests/test_new_problems.py +116 -0
- physverify-0.1.0/tests/test_order.py +84 -0
- physverify-0.1.0/tests/test_properties.py +54 -0
- physverify-0.1.0/tests/test_real_projects.py +121 -0
- physverify-0.1.0/tests/test_reference_problems.py +133 -0
- physverify-0.1.0/tests/test_seeded_bugs.py +98 -0
- physverify-0.1.0/tests/test_studies.py +86 -0
physverify-0.1.0/LICENSE
ADDED
|
@@ -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,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,85 @@
|
|
|
1
|
+
# physverify
|
|
2
|
+
|
|
3
|
+
**A tool that checks whether a physics simulation is solving the right equations correctly, not just running without errors.**
|
|
4
|
+
|
|
5
|
+
Status: early version (0.1.0). Nobody besides the author has used it yet, so feedback is very welcome.
|
|
6
|
+
|
|
7
|
+
## The problem it addresses
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
## How it works, in plain words
|
|
12
|
+
|
|
13
|
+
physverify uses a few standard ideas from numerical analysis:
|
|
14
|
+
|
|
15
|
+
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*.
|
|
16
|
+
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.
|
|
17
|
+
3. **Check conserved quantities.** Energy, mass or momentum should stay constant (or drift in a predictable way).
|
|
18
|
+
4. **Check known special cases.** For example, a projectile with no air resistance has a range you can write down by hand.
|
|
19
|
+
5. **Exact "patch" tests.** Some solutions should be reproduced to computer precision. This catches small bugs that the other checks let through.
|
|
20
|
+
|
|
21
|
+
Each check reports **PASS**, **FAIL**, **EXACT** or **INCONCLUSIVE**.
|
|
22
|
+
|
|
23
|
+
There is also a separate set of checks for random (Monte Carlo) simulations, using the 2D Ising model as the example.
|
|
24
|
+
|
|
25
|
+
## Does it actually catch bugs?
|
|
26
|
+
|
|
27
|
+
To test it, 37 bugs were added one at a time to five small solvers (projectile, double pendulum, planet orbit, heat equation, shock tube):
|
|
28
|
+
|
|
29
|
+
- **35 of 37** bugs were caught. Plain "compare with one answer" tests caught 27.
|
|
30
|
+
- **0 false alarms** on the bug-free versions.
|
|
31
|
+
- The 2 missed bugs are explained in the [technical details](https://github.com/rcgiri-physics/physverify/blob/main/docs/technical.md).
|
|
32
|
+
|
|
33
|
+
For the Monte Carlo checks, all 20 seeded bugs were caught with no false alarms.
|
|
34
|
+
|
|
35
|
+
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.
|
|
36
|
+
|
|
37
|
+
## Try it
|
|
38
|
+
|
|
39
|
+
You need Python 3.10 or newer.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install physverify
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The example below lives in the repository, so clone it to follow along:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git clone https://github.com/rcgiri-physics/physverify.git
|
|
49
|
+
cd physverify
|
|
50
|
+
physverify run examples/verify_damped_oscillator.py
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
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.
|
|
54
|
+
|
|
55
|
+
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.
|
|
56
|
+
|
|
57
|
+
Other ways to run it:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
physverify run my_spec.py --html report.html # a web page with convergence plots
|
|
61
|
+
physverify run my_spec.py --json - # machine-readable output
|
|
62
|
+
pytest my_spec.py # each check becomes a pytest test
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## What it can and cannot do
|
|
66
|
+
|
|
67
|
+
- Works best for ordinary differential equations (orbits, pendulums, projectiles).
|
|
68
|
+
- Partial differential equations: only 1D examples so far (heat equation, shock tube).
|
|
69
|
+
- Your code needs a way to accept an extra source term, or a small wrapper ("adapter") around it. The wrapper can itself contain mistakes.
|
|
70
|
+
- Passing all checks does not prove the code is correct, only that these tests found nothing wrong.
|
|
71
|
+
|
|
72
|
+
## More
|
|
73
|
+
|
|
74
|
+
- [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).
|
|
75
|
+
- [docs/technical.md](https://github.com/rcgiri-physics/physverify/blob/main/docs/technical.md): full details, benchmark tables, limits and related work.
|
|
76
|
+
- [docs/patch.md](https://github.com/rcgiri-physics/physverify/blob/main/docs/patch.md): what the exact "patch" checks assume.
|
|
77
|
+
- [docs/ising.md](https://github.com/rcgiri-physics/physverify/blob/main/docs/ising.md): checks for Monte Carlo codes.
|
|
78
|
+
- [docs/agent.md](https://github.com/rcgiri-physics/physverify/blob/main/docs/agent.md): using it with an AI coding assistant.
|
|
79
|
+
- [CHANGELOG.md](https://github.com/rcgiri-physics/physverify/blob/main/CHANGELOG.md)
|
|
80
|
+
|
|
81
|
+
Run the project's own tests with `pip install -e .[dev]` then `python -m pytest -q` (about 1.5 minutes).
|
|
82
|
+
|
|
83
|
+
## Licence
|
|
84
|
+
|
|
85
|
+
MIT. Copyright (c) 2026 Ram Chandra Giri.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "physverify"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Verification checks for physics simulation code: manufactured solutions, order-of-accuracy studies and a seeded-bug benchmark."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Ram Chandra Giri" }]
|
|
14
|
+
keywords = ["verification", "method of manufactured solutions", "numerical methods", "physics", "simulation", "testing"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Topic :: Scientific/Engineering :: Physics",
|
|
18
|
+
"Framework :: Pytest",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
25
|
+
"Operating System :: OS Independent",
|
|
26
|
+
"Intended Audience :: Science/Research",
|
|
27
|
+
"Intended Audience :: Education",
|
|
28
|
+
"Topic :: Software Development :: Testing",
|
|
29
|
+
"Development Status :: 3 - Alpha",
|
|
30
|
+
]
|
|
31
|
+
dependencies = ["numpy>=1.24"]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/rcgiri-physics/physverify"
|
|
35
|
+
Documentation = "https://github.com/rcgiri-physics/physverify/blob/main/docs/technical.md"
|
|
36
|
+
Issues = "https://github.com/rcgiri-physics/physverify/issues"
|
|
37
|
+
Changelog = "https://github.com/rcgiri-physics/physverify/blob/main/CHANGELOG.md"
|
|
38
|
+
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
symbolic = ["sympy>=1.12"]
|
|
41
|
+
dev = ["pytest>=8", "hypothesis>=6", "sympy>=1.12"]
|
|
42
|
+
|
|
43
|
+
[project.scripts]
|
|
44
|
+
physverify = "physverify.cli:main"
|
|
45
|
+
|
|
46
|
+
[project.entry-points.pytest11]
|
|
47
|
+
physverify = "physverify.pytest_plugin"
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.packages.find]
|
|
50
|
+
where = ["src"]
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
testpaths = ["tests"]
|
|
54
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""physverify: verification checks for physics simulation code."""
|
|
2
|
+
|
|
3
|
+
from .checks import Check, OrderResult, Result
|
|
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
|
|
7
|
+
from .studies import invariant_study, ode_checks, patch_study, reference_study
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Check",
|
|
13
|
+
"ManufacturedODE",
|
|
14
|
+
"OrderResult",
|
|
15
|
+
"Result",
|
|
16
|
+
"check_exact",
|
|
17
|
+
"check_order",
|
|
18
|
+
"fit_order",
|
|
19
|
+
"invariant_study",
|
|
20
|
+
"mms_order_study",
|
|
21
|
+
"ode_checks",
|
|
22
|
+
"pairwise_orders",
|
|
23
|
+
"patch_study",
|
|
24
|
+
"reference_study",
|
|
25
|
+
"zero_source",
|
|
26
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Adapters that connect physverify to existing simulation code (read-only)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def sibling_project(env_var: str, folder: str) -> Path:
|
|
10
|
+
"""Folder of another project: $env_var if set, else a sibling of this repository."""
|
|
11
|
+
if os.environ.get(env_var):
|
|
12
|
+
return Path(os.environ[env_var])
|
|
13
|
+
repo_root = Path(__file__).resolve().parents[3]
|
|
14
|
+
return repo_root.parent / folder
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Adapter for double-pendulum-simulation/src/mechanics.py.
|
|
2
|
+
|
|
3
|
+
Only the right-hand side (get_derivatives) and the energy (calculate_energy)
|
|
4
|
+
are tested. They are stepped with physverify's own RK4, because the project's
|
|
5
|
+
solvers.py needs SciPy. So these checks say nothing about solvers.py.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from ..checks import Check
|
|
13
|
+
from ..loaders import load_module_from_path
|
|
14
|
+
from ..problems import pendulum as P
|
|
15
|
+
from ..problems._common import make_integrate
|
|
16
|
+
from ..studies import ode_checks
|
|
17
|
+
from . import sibling_project
|
|
18
|
+
|
|
19
|
+
ENV_VAR = "PHYSVERIFY_PENDULUM_DIR"
|
|
20
|
+
FOLDER = "double-pendulum-simulation"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def source_path():
|
|
24
|
+
return sibling_project(ENV_VAR, FOLDER) / "src" / "mechanics.py"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def load():
|
|
28
|
+
return load_module_from_path("double_pendulum_mechanics_under_test", source_path())
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def checks(module=None) -> list[Check]:
|
|
32
|
+
module = module or load()
|
|
33
|
+
params = dict(P.PARAMS)
|
|
34
|
+
|
|
35
|
+
def rhs(t, y):
|
|
36
|
+
return np.asarray(module.get_derivatives(t, y, params), dtype=float)
|
|
37
|
+
|
|
38
|
+
def energy(y):
|
|
39
|
+
return module.calculate_energy(y, **params)
|
|
40
|
+
|
|
41
|
+
out = ode_checks(
|
|
42
|
+
"mechanics.py RHS + physverify RK4",
|
|
43
|
+
make_integrate(rhs, frozenset(), "rk4"),
|
|
44
|
+
rhs_ref=P.rhs_reference(params),
|
|
45
|
+
mms=P.manufactured(),
|
|
46
|
+
y0_free=P.Y0_FREE,
|
|
47
|
+
t_end=P.MMS_T_END,
|
|
48
|
+
ns=P.NS_RK4,
|
|
49
|
+
order=4.0,
|
|
50
|
+
quantity=energy,
|
|
51
|
+
patch=P.PATCH,
|
|
52
|
+
integrate_trajectory=make_integrate(rhs, frozenset(), "rk4", trajectory=True),
|
|
53
|
+
)
|
|
54
|
+
out.append(P.frequency_check(rhs, label="mechanics.py small-oscillation frequencies"))
|
|
55
|
+
return out
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Adapter for lorenz-attractor-simulation/src/mechanics.py (lorenz_derivatives).
|
|
2
|
+
|
|
3
|
+
The project integrates with SciPy's solve_ivp, so only the right-hand side is
|
|
4
|
+
under test here; it is stepped with physverify's own RK4. The flow is chaotic
|
|
5
|
+
(largest Lyapunov exponent about 0.906), so trajectories are compared only up
|
|
6
|
+
to t = 1, well inside one Lyapunov time's worth of growth (exp(0.9) < 3).
|
|
7
|
+
Beyond that the project is checked through exact properties of the flow:
|
|
8
|
+
fixed points, the constant divergence -(sigma + 1 + beta), and the Jacobian
|
|
9
|
+
that scripts/calculate_lyapunov.py writes by hand.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import ast
|
|
15
|
+
import math
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
from ..checks import EXACT, FAIL, PASS, Check, Result
|
|
21
|
+
from ..loaders import load_module_from_path
|
|
22
|
+
from ..mms import ManufacturedODE
|
|
23
|
+
from ..problems._common import make_integrate
|
|
24
|
+
from ..studies import ode_checks
|
|
25
|
+
from . import sibling_project
|
|
26
|
+
|
|
27
|
+
ENV_VAR = "PHYSVERIFY_LORENZ_DIR"
|
|
28
|
+
FOLDER = "lorenz-attractor-simulation"
|
|
29
|
+
PARAMS = {"sigma": 10.0, "rho": 28.0, "beta": 8.0 / 3.0}
|
|
30
|
+
Y0_FREE = np.array([1.0, 1.0, 1.0])
|
|
31
|
+
T_END = 1.0
|
|
32
|
+
NS_RK4 = (100, 200, 400, 800)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def source_path():
|
|
36
|
+
return sibling_project(ENV_VAR, FOLDER) / "src" / "mechanics.py"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def lyapunov_path():
|
|
40
|
+
return sibling_project(ENV_VAR, FOLDER) / "scripts" / "calculate_lyapunov.py"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def load():
|
|
44
|
+
return load_module_from_path("lorenz_mechanics_under_test", source_path())
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def rhs_reference(p=PARAMS):
|
|
48
|
+
"""From docs/theory.md, written in matrix-free form independently of the code."""
|
|
49
|
+
s, r, b = p["sigma"], p["rho"], p["beta"]
|
|
50
|
+
|
|
51
|
+
def f(t, u):
|
|
52
|
+
x, y, z = u
|
|
53
|
+
return np.array([s * y - s * x, r * x - x * z - y, x * y - b * z])
|
|
54
|
+
|
|
55
|
+
return f
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def manufactured() -> ManufacturedODE:
|
|
59
|
+
def u(t):
|
|
60
|
+
return np.array([2.0 * np.sin(t), 1.0 + np.cos(1.7 * t), 20.0 + 3.0 * np.sin(0.6 * t)])
|
|
61
|
+
|
|
62
|
+
return ManufacturedODE(u, rhs_reference())
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def fixed_points(p=PARAMS):
|
|
66
|
+
c = math.sqrt(p["beta"] * (p["rho"] - 1.0))
|
|
67
|
+
return [np.zeros(3), np.array([c, c, p["rho"] - 1.0]), np.array([-c, -c, p["rho"] - 1.0])]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def central_jacobian(f, u, h=1e-3):
|
|
71
|
+
"""Centred differences are exact (to round-off) for a quadratic field like Lorenz."""
|
|
72
|
+
jac = np.zeros((3, 3))
|
|
73
|
+
for j in range(3):
|
|
74
|
+
e = np.zeros(3)
|
|
75
|
+
e[j] = h
|
|
76
|
+
jac[:, j] = (np.asarray(f(0.0, u + e)) - np.asarray(f(0.0, u - e))) / (2 * h)
|
|
77
|
+
return jac
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
SAMPLE_POINTS = [np.array(v) for v in ([1.0, 1.0, 1.0], [-8.3, 4.1, 27.0], [12.0, -3.0, 5.5], [0.0, 0.0, 0.0])]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def fixed_point_check(rhs) -> Check:
|
|
84
|
+
def run():
|
|
85
|
+
worst = max(float(np.max(np.abs(rhs(0.0, p)))) for p in fixed_points())
|
|
86
|
+
if worst <= 1e-12 * 30:
|
|
87
|
+
return Result(EXACT, f"all three fixed points are zeros of the field (largest |f| = {worst:.1e})")
|
|
88
|
+
return Result(FAIL, f"field is {worst:.3g} at a fixed point")
|
|
89
|
+
|
|
90
|
+
return Check("limit", "fixed points 0 and (+-sqrt(beta(rho-1)), ., rho-1)", run)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def divergence_check(rhs) -> Check:
|
|
94
|
+
expected = -(PARAMS["sigma"] + 1.0 + PARAMS["beta"])
|
|
95
|
+
|
|
96
|
+
def run():
|
|
97
|
+
divs = [float(np.trace(central_jacobian(rhs, p))) for p in SAMPLE_POINTS]
|
|
98
|
+
worst = max(abs(d - expected) for d in divs)
|
|
99
|
+
if worst <= 1e-9:
|
|
100
|
+
return Result(EXACT, f"divergence {divs[0]:.12f} = -(sigma+1+beta) at {len(divs)} points")
|
|
101
|
+
return Result(FAIL, f"divergence differs from {expected:.6f} by {worst:.3g}")
|
|
102
|
+
|
|
103
|
+
return Check("invariant", "flow divergence is constant -(sigma+1+beta)", run)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def jacobian_check(rhs, jacobian) -> Check:
|
|
107
|
+
def run():
|
|
108
|
+
worst = max(
|
|
109
|
+
float(np.max(np.abs(np.asarray(jacobian(p, PARAMS)) - central_jacobian(rhs, p)))) for p in SAMPLE_POINTS
|
|
110
|
+
)
|
|
111
|
+
if worst <= 1e-9:
|
|
112
|
+
return Result(EXACT, f"hand-written Jacobian matches the field's derivative (max diff {worst:.1e})")
|
|
113
|
+
return Result(FAIL, f"hand-written Jacobian differs from the field's derivative by {worst:.3g}")
|
|
114
|
+
|
|
115
|
+
return Check("limit", "calculate_lyapunov.py Jacobian vs derivative of the field", run)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def load_function(path, name):
|
|
119
|
+
"""One top-level function from a script, without running the script's imports.
|
|
120
|
+
|
|
121
|
+
The script appends to sys.path and imports SciPy at module level; only the
|
|
122
|
+
function's own source is executed here, with numpy as np in scope.
|
|
123
|
+
"""
|
|
124
|
+
tree = ast.parse(Path(path).read_text(encoding="utf-8"))
|
|
125
|
+
node = next(n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name == name)
|
|
126
|
+
namespace = {"np": np}
|
|
127
|
+
exec(compile(ast.Module([node], []), str(path), "exec"), namespace)
|
|
128
|
+
return namespace[name]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def checks(module=None) -> list[Check]:
|
|
132
|
+
module = module or load()
|
|
133
|
+
|
|
134
|
+
def rhs(t, y):
|
|
135
|
+
return np.asarray(module.lorenz_derivatives(t, y, PARAMS), dtype=float)
|
|
136
|
+
|
|
137
|
+
out = ode_checks(
|
|
138
|
+
"mechanics.py RHS + physverify RK4",
|
|
139
|
+
make_integrate(rhs, frozenset(), "rk4"),
|
|
140
|
+
rhs_ref=rhs_reference(),
|
|
141
|
+
mms=manufactured(),
|
|
142
|
+
y0_free=Y0_FREE,
|
|
143
|
+
t_end=T_END,
|
|
144
|
+
ns=NS_RK4,
|
|
145
|
+
order=4.0,
|
|
146
|
+
patch=(np.array([1.0, -2.0, 15.0]), np.array([0.5, 1.5, -2.0])),
|
|
147
|
+
integrate_trajectory=make_integrate(rhs, frozenset(), "rk4", trajectory=True),
|
|
148
|
+
)
|
|
149
|
+
out += [fixed_point_check(rhs), divergence_check(rhs)]
|
|
150
|
+
if lyapunov_path().is_file():
|
|
151
|
+
out.append(jacobian_check(rhs, load_function(lyapunov_path(), "lorenz_jacobian")))
|
|
152
|
+
return out
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Adapter for projectile-simulation/src/projectile_sim_v1.py (class Projectile, forward Euler).
|
|
2
|
+
|
|
3
|
+
simulate() returns only positions and runs until landing, with no source hook,
|
|
4
|
+
so there is no MMS or patch check (row 25 of the audit plan). Positions at a
|
|
5
|
+
fixed time are compared with the closed-form vacuum and linear-drag solutions
|
|
6
|
+
from the README and with an RK4 reference for quadratic drag; the landing range
|
|
7
|
+
is compared with the vacuum formula v0^2 sin(2 angle) / g. Forward Euler is
|
|
8
|
+
first order by design, so order 1 is the pass mark.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import math
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
from ..checks import Check
|
|
18
|
+
from ..loaders import load_module_from_path
|
|
19
|
+
from ..order import check_order
|
|
20
|
+
from ..steppers import integrate_fixed, rk4
|
|
21
|
+
from . import sibling_project
|
|
22
|
+
|
|
23
|
+
ENV_VAR = "PHYSVERIFY_PROJECTILE_DIR"
|
|
24
|
+
FOLDER = "projectile-simulation"
|
|
25
|
+
MASS, RADIUS, V0, ANGLE = 0.145, 0.037, 30.0, 45.0
|
|
26
|
+
T_END = 2.0
|
|
27
|
+
NS = (200, 400, 800, 1600)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def source_path():
|
|
31
|
+
return sibling_project(ENV_VAR, FOLDER) / "src" / "projectile_sim_v1.py"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load():
|
|
35
|
+
return load_module_from_path("projectile_sim_v1_under_test", source_path(), stubs=("matplotlib", "matplotlib.pyplot"))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def exact_position(mode, t, k, g=9.81):
|
|
39
|
+
vx0 = V0 * math.cos(math.radians(ANGLE))
|
|
40
|
+
vy0 = V0 * math.sin(math.radians(ANGLE))
|
|
41
|
+
if mode == "vacuum":
|
|
42
|
+
return np.array([vx0 * t, vy0 * t - 0.5 * g * t * t])
|
|
43
|
+
if mode == "linear":
|
|
44
|
+
decay = 1.0 - math.exp(-k * t)
|
|
45
|
+
return np.array([vx0 / k * decay, (vy0 + g / k) / k * decay - g * t / k])
|
|
46
|
+
# quadratic: no closed form; RK4 on the README's equations with a fine step
|
|
47
|
+
def f(_t, z):
|
|
48
|
+
speed = math.hypot(z[2], z[3])
|
|
49
|
+
return np.array([z[2], z[3], -k * speed * z[2], -g - k * speed * z[3]])
|
|
50
|
+
|
|
51
|
+
return integrate_fixed(f, [0.0, 0.0, vx0, vy0], 0.0, t, 20000, rk4)[:2]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def drag_constant(mode, viscosity=1.81e-5, cd=0.47, rho=1.225):
|
|
55
|
+
if mode == "linear":
|
|
56
|
+
return 6.0 * math.pi * viscosity * RADIUS / MASS
|
|
57
|
+
if mode == "quadratic":
|
|
58
|
+
return 0.5 * rho * math.pi * RADIUS**2 * cd / MASS
|
|
59
|
+
return 0.0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def position_check(module, mode) -> Check:
|
|
63
|
+
def run():
|
|
64
|
+
ball = module.Projectile(mass=MASS, radius=RADIUS)
|
|
65
|
+
expected = exact_position(mode, T_END, drag_constant(mode))
|
|
66
|
+
hs, errs = [], []
|
|
67
|
+
for n in NS:
|
|
68
|
+
_, xs, ys = ball.simulate(V0, ANGLE, mode=mode, dt=T_END / n)
|
|
69
|
+
hs.append(T_END / n)
|
|
70
|
+
errs.append(float(np.max(np.abs(np.array([xs[n], ys[n]]) - expected))))
|
|
71
|
+
return check_order(hs, errs, 1.0, scale=float(np.max(np.abs(expected))))
|
|
72
|
+
|
|
73
|
+
return Check("reference", f"v1 {mode}/euler position at t = {T_END:g}", run)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def range_check(module) -> Check:
|
|
77
|
+
def run():
|
|
78
|
+
ball = module.Projectile(mass=MASS, radius=RADIUS)
|
|
79
|
+
exact = V0 * V0 * math.sin(math.radians(2 * ANGLE)) / 9.81
|
|
80
|
+
hs, errs = [], []
|
|
81
|
+
for dt in (0.02, 0.01, 0.005, 0.0025):
|
|
82
|
+
_, xs, _ = ball.simulate(V0, ANGLE, mode="vacuum", dt=dt)
|
|
83
|
+
hs.append(dt)
|
|
84
|
+
errs.append(abs(xs[-1] - exact))
|
|
85
|
+
return check_order(hs, errs, 1.0, scale=exact)
|
|
86
|
+
|
|
87
|
+
return Check("limit", "v1 vacuum/euler range", run)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def checks(module=None) -> list[Check]:
|
|
91
|
+
module = module or load()
|
|
92
|
+
return [position_check(module, m) for m in ("vacuum", "linear", "quadratic")] + [range_check(module)]
|