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.
Files changed (59) hide show
  1. physverify-0.1.0/LICENSE +21 -0
  2. physverify-0.1.0/PKG-INFO +122 -0
  3. physverify-0.1.0/README.md +85 -0
  4. physverify-0.1.0/pyproject.toml +54 -0
  5. physverify-0.1.0/setup.cfg +4 -0
  6. physverify-0.1.0/src/physverify/__init__.py +26 -0
  7. physverify-0.1.0/src/physverify/__main__.py +3 -0
  8. physverify-0.1.0/src/physverify/adapters/__init__.py +14 -0
  9. physverify-0.1.0/src/physverify/adapters/double_pendulum.py +55 -0
  10. physverify-0.1.0/src/physverify/adapters/lorenz.py +152 -0
  11. physverify-0.1.0/src/physverify/adapters/projectile_v1.py +92 -0
  12. physverify-0.1.0/src/physverify/adapters/projectile_v2.py +94 -0
  13. physverify-0.1.0/src/physverify/adapters/three_body.py +283 -0
  14. physverify-0.1.0/src/physverify/agent.py +265 -0
  15. physverify-0.1.0/src/physverify/checks.py +59 -0
  16. physverify-0.1.0/src/physverify/cli.py +105 -0
  17. physverify-0.1.0/src/physverify/compare.py +104 -0
  18. physverify-0.1.0/src/physverify/loaders.py +49 -0
  19. physverify-0.1.0/src/physverify/matrix.py +160 -0
  20. physverify-0.1.0/src/physverify/mms.py +118 -0
  21. physverify-0.1.0/src/physverify/order.py +131 -0
  22. physverify-0.1.0/src/physverify/problems/__init__.py +6 -0
  23. physverify-0.1.0/src/physverify/problems/_common.py +89 -0
  24. physverify-0.1.0/src/physverify/problems/heat1d.py +217 -0
  25. physverify-0.1.0/src/physverify/problems/kepler.py +176 -0
  26. physverify-0.1.0/src/physverify/problems/pendulum.py +201 -0
  27. physverify-0.1.0/src/physverify/problems/projectile.py +240 -0
  28. physverify-0.1.0/src/physverify/problems/sod.py +291 -0
  29. physverify-0.1.0/src/physverify/pytest_plugin.py +75 -0
  30. physverify-0.1.0/src/physverify/report.py +189 -0
  31. physverify-0.1.0/src/physverify/steppers.py +38 -0
  32. physverify-0.1.0/src/physverify/stochastic/__init__.py +1 -0
  33. physverify-0.1.0/src/physverify/stochastic/calibrate.py +202 -0
  34. physverify-0.1.0/src/physverify/stochastic/checks.py +378 -0
  35. physverify-0.1.0/src/physverify/stochastic/exact.py +205 -0
  36. physverify-0.1.0/src/physverify/stochastic/ising.py +170 -0
  37. physverify-0.1.0/src/physverify/stochastic/matrix.py +151 -0
  38. physverify-0.1.0/src/physverify/stochastic/reference.py +67 -0
  39. physverify-0.1.0/src/physverify/stochastic/sampler.py +253 -0
  40. physverify-0.1.0/src/physverify/stochastic/stats.py +137 -0
  41. physverify-0.1.0/src/physverify/studies.py +146 -0
  42. physverify-0.1.0/src/physverify/symbolic.py +89 -0
  43. physverify-0.1.0/src/physverify.egg-info/PKG-INFO +122 -0
  44. physverify-0.1.0/src/physverify.egg-info/SOURCES.txt +57 -0
  45. physverify-0.1.0/src/physverify.egg-info/dependency_links.txt +1 -0
  46. physverify-0.1.0/src/physverify.egg-info/entry_points.txt +5 -0
  47. physverify-0.1.0/src/physverify.egg-info/requires.txt +9 -0
  48. physverify-0.1.0/src/physverify.egg-info/top_level.txt +1 -0
  49. physverify-0.1.0/tests/test_agent.py +196 -0
  50. physverify-0.1.0/tests/test_cli.py +82 -0
  51. physverify-0.1.0/tests/test_ising.py +171 -0
  52. physverify-0.1.0/tests/test_mms.py +73 -0
  53. physverify-0.1.0/tests/test_new_problems.py +116 -0
  54. physverify-0.1.0/tests/test_order.py +84 -0
  55. physverify-0.1.0/tests/test_properties.py +54 -0
  56. physverify-0.1.0/tests/test_real_projects.py +121 -0
  57. physverify-0.1.0/tests/test_reference_problems.py +133 -0
  58. physverify-0.1.0/tests/test_seeded_bugs.py +98 -0
  59. physverify-0.1.0/tests/test_studies.py +86 -0
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -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)]