physverify 0.2.0__tar.gz → 0.3.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.2.0/src/physverify.egg-info → physverify-0.3.0}/PKG-INFO +11 -9
- {physverify-0.2.0 → physverify-0.3.0}/README.md +10 -8
- {physverify-0.2.0 → physverify-0.3.0}/pyproject.toml +1 -1
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/__init__.py +1 -1
- physverify-0.3.0/src/physverify/loaders.py +178 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/method_bugs.py +117 -2
- physverify-0.3.0/src/physverify/references.py +128 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/studies.py +26 -0
- {physverify-0.2.0 → physverify-0.3.0/src/physverify.egg-info}/PKG-INFO +11 -9
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify.egg-info/SOURCES.txt +2 -0
- {physverify-0.2.0 → physverify-0.3.0}/tests/test_corpus_lessons.py +45 -0
- physverify-0.3.0/tests/test_references.py +103 -0
- physverify-0.2.0/src/physverify/loaders.py +0 -94
- {physverify-0.2.0 → physverify-0.3.0}/LICENSE +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/setup.cfg +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/__main__.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/adapters/__init__.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/adapters/double_pendulum.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/adapters/lorenz.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/adapters/projectile_v1.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/adapters/projectile_v2.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/adapters/three_body.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/agent.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/butcher.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/checks.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/cli.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/compare.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/constants.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/matrix.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/mms.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/order.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/problems/__init__.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/problems/_common.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/problems/heat1d.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/problems/kepler.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/problems/pendulum.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/problems/projectile.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/problems/sod.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/pytest_plugin.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/report.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/steppers.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/stochastic/__init__.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/stochastic/calibrate.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/stochastic/checks.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/stochastic/exact.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/stochastic/ising.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/stochastic/matrix.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/stochastic/reference.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/stochastic/sampler.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/stochastic/stats.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify/symbolic.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify.egg-info/dependency_links.txt +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify.egg-info/entry_points.txt +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify.egg-info/requires.txt +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/src/physverify.egg-info/top_level.txt +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/tests/test_agent.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/tests/test_cli.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/tests/test_ising.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/tests/test_method_bugs.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/tests/test_mms.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/tests/test_new_problems.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/tests/test_order.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/tests/test_properties.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/tests/test_real_projects.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/tests/test_reference_problems.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/tests/test_seeded_bugs.py +0 -0
- {physverify-0.2.0 → physverify-0.3.0}/tests/test_studies.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: physverify
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Verification checks for physics simulation code: manufactured solutions, order-of-accuracy studies and a seeded-bug benchmark.
|
|
5
5
|
Author: Ram Chandra Giri
|
|
6
6
|
License-Expression: MIT
|
|
@@ -39,7 +39,7 @@ Dynamic: license-file
|
|
|
39
39
|
|
|
40
40
|
**A tool that checks whether a physics simulation is solving the right equations correctly, not just running without errors.**
|
|
41
41
|
|
|
42
|
-
Status: early version (0.
|
|
42
|
+
Status: early version (0.3.0). It has been tried on 68 public physics repositories (see below), but nobody besides the author has used it yet, so feedback is very welcome.
|
|
43
43
|
|
|
44
44
|
## The problem it addresses
|
|
45
45
|
|
|
@@ -51,7 +51,7 @@ physverify uses a few standard ideas from numerical analysis:
|
|
|
51
51
|
|
|
52
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
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).
|
|
54
|
+
3. **Check conserved quantities and budgets.** Energy, mass or momentum should stay constant (or drift in a predictable way), and heat added through a wall or a source should match what went in, exactly, on every grid.
|
|
55
55
|
4. **Check known special cases.** For example, a projectile with no air resistance has a range you can write down by hand.
|
|
56
56
|
5. **Exact "patch" tests.** Some solutions should be reproduced to computer precision. This catches small bugs that the other checks let through.
|
|
57
57
|
6. **Algebraic checks on the method itself.** For a Runge-Kutta method, whether the coefficients really have the order they claim can be settled exactly, with no test problem (the "order conditions").
|
|
@@ -74,12 +74,14 @@ The bugs and the checks were written by the same person, so these numbers show t
|
|
|
74
74
|
|
|
75
75
|
## Trying it on other people's code
|
|
76
76
|
|
|
77
|
-
To see what it does on code it was not written for,
|
|
77
|
+
To see what it does on code it was not written for, public Python physics repositories from GitHub were run in a locked-down container (October 2026): no network, the repository read-only, nothing from it run on the author's computer. 40 were drawn at random from topic searches (batches 1 to 4); another 60 were the newest small repositories whose README says the work is complete (batch 5), and 20 more were picked because they are quick to test (batch 6); batches 5 and 6 are not random.
|
|
78
78
|
|
|
79
|
-
- **
|
|
80
|
-
- **
|
|
81
|
-
- **The
|
|
82
|
-
- **Our own adapters were wrong about
|
|
79
|
+
- **68 of the 120 could be tested.** Most of the others were not numerical solvers (neural networks, apps and games, analysis tools, finance), one needed a recent NVIDIA GPU, and a few need more set-up.
|
|
80
|
+
- **16 confirmed bugs in 12 of the 68.** For example: a measured energy that counts the magnetic-field term twice; backward time-stepping that is only first-order accurate; Runge-Kutta coefficient tables labelled with a higher order than they have; a spectral derivative that crashes for an odd number of grid points; boundary values applied after the linear solve instead of inside it; an insulated wall that leaks heat; a grid spacing off by one interval; a Kepler solver that fails for negative angles.
|
|
81
|
+
- **The rest had no confirmed bug.** Most passed every check that applied, often to 1e-13 or better.
|
|
82
|
+
- **Our own adapters were wrong about 28 times** before checking (a wrong constant, too-coarse steps, a wrong sign convention, and so on). Every FAIL was checked by hand before being called a bug, and these mistakes are recorded, not hidden.
|
|
83
|
+
|
|
84
|
+
These counts describe the repositories that were drawn; they are not a bug rate for physics code in general.
|
|
83
85
|
|
|
84
86
|
Each lesson became a new check or a clearer message in physverify. The testing and write-ups were done with help from an AI assistant; every number comes from running the code. Authors are told about a finding before their project is named, so the repositories are listed by ID: [results/corpus/SUMMARY.md](https://github.com/rcgiri-physics/physverify/blob/main/results/corpus/SUMMARY.md).
|
|
85
87
|
|
|
@@ -120,7 +122,7 @@ pytest my_spec.py # each check becomes a pytest tes
|
|
|
120
122
|
|
|
121
123
|
## More
|
|
122
124
|
|
|
123
|
-
- [results/corpus/SUMMARY.md](https://github.com/rcgiri-physics/physverify/blob/main/results/corpus/SUMMARY.md): the
|
|
125
|
+
- [results/corpus/SUMMARY.md](https://github.com/rcgiri-physics/physverify/blob/main/results/corpus/SUMMARY.md): the 120 public repositories, what was tested and what was found.
|
|
124
126
|
- [sandbox/README.md](https://github.com/rcgiri-physics/physverify/blob/main/sandbox/README.md): the locked-down container used to run other people's code.
|
|
125
127
|
- [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).
|
|
126
128
|
- [docs/technical.md](https://github.com/rcgiri-physics/physverify/blob/main/docs/technical.md): full details, benchmark tables, limits and related work.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**A tool that checks whether a physics simulation is solving the right equations correctly, not just running without errors.**
|
|
4
4
|
|
|
5
|
-
Status: early version (0.
|
|
5
|
+
Status: early version (0.3.0). It has been tried on 68 public physics repositories (see below), but nobody besides the author has used it yet, so feedback is very welcome.
|
|
6
6
|
|
|
7
7
|
## The problem it addresses
|
|
8
8
|
|
|
@@ -14,7 +14,7 @@ physverify uses a few standard ideas from numerical analysis:
|
|
|
14
14
|
|
|
15
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
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).
|
|
17
|
+
3. **Check conserved quantities and budgets.** Energy, mass or momentum should stay constant (or drift in a predictable way), and heat added through a wall or a source should match what went in, exactly, on every grid.
|
|
18
18
|
4. **Check known special cases.** For example, a projectile with no air resistance has a range you can write down by hand.
|
|
19
19
|
5. **Exact "patch" tests.** Some solutions should be reproduced to computer precision. This catches small bugs that the other checks let through.
|
|
20
20
|
6. **Algebraic checks on the method itself.** For a Runge-Kutta method, whether the coefficients really have the order they claim can be settled exactly, with no test problem (the "order conditions").
|
|
@@ -37,12 +37,14 @@ The bugs and the checks were written by the same person, so these numbers show t
|
|
|
37
37
|
|
|
38
38
|
## Trying it on other people's code
|
|
39
39
|
|
|
40
|
-
To see what it does on code it was not written for,
|
|
40
|
+
To see what it does on code it was not written for, public Python physics repositories from GitHub were run in a locked-down container (October 2026): no network, the repository read-only, nothing from it run on the author's computer. 40 were drawn at random from topic searches (batches 1 to 4); another 60 were the newest small repositories whose README says the work is complete (batch 5), and 20 more were picked because they are quick to test (batch 6); batches 5 and 6 are not random.
|
|
41
41
|
|
|
42
|
-
- **
|
|
43
|
-
- **
|
|
44
|
-
- **The
|
|
45
|
-
- **Our own adapters were wrong about
|
|
42
|
+
- **68 of the 120 could be tested.** Most of the others were not numerical solvers (neural networks, apps and games, analysis tools, finance), one needed a recent NVIDIA GPU, and a few need more set-up.
|
|
43
|
+
- **16 confirmed bugs in 12 of the 68.** For example: a measured energy that counts the magnetic-field term twice; backward time-stepping that is only first-order accurate; Runge-Kutta coefficient tables labelled with a higher order than they have; a spectral derivative that crashes for an odd number of grid points; boundary values applied after the linear solve instead of inside it; an insulated wall that leaks heat; a grid spacing off by one interval; a Kepler solver that fails for negative angles.
|
|
44
|
+
- **The rest had no confirmed bug.** Most passed every check that applied, often to 1e-13 or better.
|
|
45
|
+
- **Our own adapters were wrong about 28 times** before checking (a wrong constant, too-coarse steps, a wrong sign convention, and so on). Every FAIL was checked by hand before being called a bug, and these mistakes are recorded, not hidden.
|
|
46
|
+
|
|
47
|
+
These counts describe the repositories that were drawn; they are not a bug rate for physics code in general.
|
|
46
48
|
|
|
47
49
|
Each lesson became a new check or a clearer message in physverify. The testing and write-ups were done with help from an AI assistant; every number comes from running the code. Authors are told about a finding before their project is named, so the repositories are listed by ID: [results/corpus/SUMMARY.md](https://github.com/rcgiri-physics/physverify/blob/main/results/corpus/SUMMARY.md).
|
|
48
50
|
|
|
@@ -83,7 +85,7 @@ pytest my_spec.py # each check becomes a pytest tes
|
|
|
83
85
|
|
|
84
86
|
## More
|
|
85
87
|
|
|
86
|
-
- [results/corpus/SUMMARY.md](https://github.com/rcgiri-physics/physverify/blob/main/results/corpus/SUMMARY.md): the
|
|
88
|
+
- [results/corpus/SUMMARY.md](https://github.com/rcgiri-physics/physverify/blob/main/results/corpus/SUMMARY.md): the 120 public repositories, what was tested and what was found.
|
|
87
89
|
- [sandbox/README.md](https://github.com/rcgiri-physics/physverify/blob/main/sandbox/README.md): the locked-down container used to run other people's code.
|
|
88
90
|
- [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).
|
|
89
91
|
- [docs/technical.md](https://github.com/rcgiri-physics/physverify/blob/main/docs/technical.md): full details, benchmark tables, limits and related work.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "physverify"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
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"
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Import a module from a path without touching its folder.
|
|
2
|
+
|
|
3
|
+
Used by the adapters that test other people's code. Nothing is written next to
|
|
4
|
+
the module (no __pycache__), and optional plotting imports can be stubbed so a
|
|
5
|
+
simulation file can be imported on a machine without matplotlib.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
import importlib
|
|
12
|
+
import importlib.util
|
|
13
|
+
import sys
|
|
14
|
+
import types
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Placeholder:
|
|
19
|
+
"""Stand-in for a name from a module that is not installed.
|
|
20
|
+
|
|
21
|
+
Calling it returns another placeholder, except that calling it with a single
|
|
22
|
+
function returns that function, so stubbed decorators (``@app.route(...)``,
|
|
23
|
+
``@jit``) leave the decorated code usable. Attribute access also returns a
|
|
24
|
+
placeholder. A tested code path that really uses the stub gets a placeholder
|
|
25
|
+
back instead of a result, which the checks then report.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, name: str = "stub"):
|
|
29
|
+
self._name = name
|
|
30
|
+
|
|
31
|
+
def __call__(self, *args, **kwargs):
|
|
32
|
+
if len(args) == 1 and not kwargs and callable(args[0]) and not isinstance(args[0], Placeholder):
|
|
33
|
+
return args[0]
|
|
34
|
+
return Placeholder(f"{self._name}()")
|
|
35
|
+
|
|
36
|
+
def __getattr__(self, attr):
|
|
37
|
+
if attr.startswith("__"):
|
|
38
|
+
raise AttributeError(attr)
|
|
39
|
+
return Placeholder(f"{self._name}.{attr}")
|
|
40
|
+
|
|
41
|
+
def __repr__(self):
|
|
42
|
+
return f"<stub {self._name}>"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _stub_module(name: str, attrs=()) -> types.ModuleType:
|
|
46
|
+
mod = types.ModuleType(name)
|
|
47
|
+
mod.__dict__["__stub__"] = True
|
|
48
|
+
for a in attrs:
|
|
49
|
+
setattr(mod, a, Placeholder(f"{name}.{a}"))
|
|
50
|
+
return mod
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def stub_modules(spec, *, force: bool = False) -> list[str]:
|
|
54
|
+
"""Install stub modules for imports a tested code path does not use.
|
|
55
|
+
|
|
56
|
+
`spec` is an iterable of module names, or a mapping from a module name to the
|
|
57
|
+
names it must provide (``{"sklearn.metrics": ["accuracy_score"]}``), so that
|
|
58
|
+
``from sklearn.metrics import accuracy_score`` succeeds. Parent packages are
|
|
59
|
+
created as needed. A module that imports for real is left alone unless
|
|
60
|
+
`force` is true. Returns the names installed.
|
|
61
|
+
"""
|
|
62
|
+
items = spec.items() if hasattr(spec, "items") else ((s, ()) for s in spec)
|
|
63
|
+
installed = []
|
|
64
|
+
for name, attrs in items:
|
|
65
|
+
if not force:
|
|
66
|
+
try:
|
|
67
|
+
importlib.import_module(name)
|
|
68
|
+
continue
|
|
69
|
+
except ImportError:
|
|
70
|
+
pass
|
|
71
|
+
parts = name.split(".")
|
|
72
|
+
for i in range(1, len(parts)):
|
|
73
|
+
parent = ".".join(parts[:i])
|
|
74
|
+
if parent not in sys.modules:
|
|
75
|
+
sys.modules[parent] = _stub_module(parent)
|
|
76
|
+
installed.append(parent)
|
|
77
|
+
sys.modules[name] = _stub_module(name, attrs)
|
|
78
|
+
installed.append(name)
|
|
79
|
+
parent, _, child = name.rpartition(".")
|
|
80
|
+
if parent:
|
|
81
|
+
setattr(sys.modules[parent], child, sys.modules[name])
|
|
82
|
+
return installed
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def run_script(path, *, argv=(), stubs=None) -> tuple[dict, str]:
|
|
86
|
+
"""Run a whole script as __main__ and return (its globals, what it printed).
|
|
87
|
+
|
|
88
|
+
For scripts whose results are only module-level variables (coursework,
|
|
89
|
+
notebooks exported to .py, demos). Matplotlib is switched to a non-interactive
|
|
90
|
+
backend and plt.show() does nothing, so the run never waits for a window.
|
|
91
|
+
"""
|
|
92
|
+
import contextlib
|
|
93
|
+
import io
|
|
94
|
+
import runpy
|
|
95
|
+
|
|
96
|
+
if stubs:
|
|
97
|
+
stub_modules(stubs)
|
|
98
|
+
try:
|
|
99
|
+
import matplotlib
|
|
100
|
+
|
|
101
|
+
matplotlib.use("Agg")
|
|
102
|
+
import matplotlib.pyplot as plt
|
|
103
|
+
|
|
104
|
+
plt.show = lambda *a, **k: None
|
|
105
|
+
except ImportError:
|
|
106
|
+
pass
|
|
107
|
+
old_argv = sys.argv
|
|
108
|
+
sys.argv = [str(path), *argv]
|
|
109
|
+
out = io.StringIO()
|
|
110
|
+
try:
|
|
111
|
+
with contextlib.redirect_stdout(out):
|
|
112
|
+
globs = runpy.run_path(str(path), run_name="__main__")
|
|
113
|
+
finally:
|
|
114
|
+
sys.argv = old_argv
|
|
115
|
+
return globs, out.getvalue()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def load_module_from_path(name: str, path, stubs=()):
|
|
119
|
+
path = Path(path)
|
|
120
|
+
if not path.is_file():
|
|
121
|
+
raise FileNotFoundError(path)
|
|
122
|
+
|
|
123
|
+
added = stub_modules(stubs)
|
|
124
|
+
|
|
125
|
+
old_flag = sys.dont_write_bytecode
|
|
126
|
+
sys.dont_write_bytecode = True
|
|
127
|
+
try:
|
|
128
|
+
spec = importlib.util.spec_from_file_location(name, path)
|
|
129
|
+
module = importlib.util.module_from_spec(spec)
|
|
130
|
+
sys.modules[name] = module
|
|
131
|
+
try:
|
|
132
|
+
spec.loader.exec_module(module)
|
|
133
|
+
except BaseException:
|
|
134
|
+
sys.modules.pop(name, None) # do not leave a half-initialised module behind
|
|
135
|
+
raise
|
|
136
|
+
finally:
|
|
137
|
+
sys.dont_write_bytecode = old_flag
|
|
138
|
+
for stub in added:
|
|
139
|
+
sys.modules.pop(stub, None)
|
|
140
|
+
return module
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def load_definitions_from_path(name: str, path, *, keep=(), inner=(), namespace=None, stubs=()):
|
|
144
|
+
"""Load only the definitions of a script whose top level runs a simulation.
|
|
145
|
+
|
|
146
|
+
Many research scripts parse argv, open a window or run a long simulation at
|
|
147
|
+
import. This executes only the top-level imports, functions and classes, and
|
|
148
|
+
the top-level assignments to the names in `keep` (constants the functions
|
|
149
|
+
use). `inner` names functions defined inside other functions (for example
|
|
150
|
+
marimo or Jupyter-exported cells); they are lifted to module level, and
|
|
151
|
+
their imports must then come from `namespace`.
|
|
152
|
+
|
|
153
|
+
Nothing else in the file runs, so a function that relies on a global set by
|
|
154
|
+
the skipped code fails with NameError; set it on the returned module.
|
|
155
|
+
|
|
156
|
+
`stubs` names modules (or maps module names to the names they must provide, see
|
|
157
|
+
`stub_modules`) to replace with stubs when they are not installed
|
|
158
|
+
(pygame, IPython, orbax, ...), for imports the tested functions do not use.
|
|
159
|
+
"""
|
|
160
|
+
path = Path(path)
|
|
161
|
+
tree = ast.parse(path.read_text(encoding="utf-8"), str(path))
|
|
162
|
+
body = []
|
|
163
|
+
for node in tree.body:
|
|
164
|
+
if isinstance(node, (ast.Import, ast.ImportFrom, ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
165
|
+
body.append(node)
|
|
166
|
+
elif isinstance(node, (ast.Assign, ast.AnnAssign)):
|
|
167
|
+
targets = node.targets if isinstance(node, ast.Assign) else [node.target]
|
|
168
|
+
if any(isinstance(t, ast.Name) and t.id in keep for t in targets):
|
|
169
|
+
body.append(node)
|
|
170
|
+
if inner:
|
|
171
|
+
wanted = set(inner)
|
|
172
|
+
body += [n for n in ast.walk(tree) if isinstance(n, ast.FunctionDef) and n.name in wanted and n not in tree.body]
|
|
173
|
+
stub_modules(stubs)
|
|
174
|
+
module = types.ModuleType(name)
|
|
175
|
+
module.__file__ = str(path)
|
|
176
|
+
module.__dict__.update(namespace or {})
|
|
177
|
+
exec(compile(ast.Module(body=body, type_ignores=[]), str(path), "exec"), module.__dict__)
|
|
178
|
+
return module
|
|
@@ -4,7 +4,9 @@ The problem benchmark (physverify.problems) seeds bugs into five solvers. Some b
|
|
|
4
4
|
repositories are not bugs of a solver for one problem but of a method or a helper: a Butcher table with
|
|
5
5
|
the wrong order label, error-estimate weights that do not sum to zero, backward time-stepping with
|
|
6
6
|
stage times on the wrong side, a time loop that stops one step late, a spectral derivative that only
|
|
7
|
-
works for even grids
|
|
7
|
+
works for even grids, Dirichlet values written after the solve instead of inside the linear system, a
|
|
8
|
+
zero-flux wall made by copying the neighbour value, a source term without its factor dt, a grid spacing
|
|
9
|
+
off by one interval. This module seeds each of them, with a bug-free control, and names the check
|
|
8
10
|
meant to catch it. `run_all()` returns one row per case; tests/test_method_bugs.py requires every
|
|
9
11
|
prediction to come true.
|
|
10
12
|
|
|
@@ -21,7 +23,8 @@ import numpy as np
|
|
|
21
23
|
from .butcher import check_tableau
|
|
22
24
|
from .checks import FAIL, PASS, Result, run_check, Check
|
|
23
25
|
from .mms import ManufacturedODE
|
|
24
|
-
from .
|
|
26
|
+
from .order import check_order
|
|
27
|
+
from .studies import backward_mms_study, balance_check, size_sweep_check, time_grid_check
|
|
25
28
|
|
|
26
29
|
# --- Butcher tables ---------------------------------------------------------------------------------
|
|
27
30
|
|
|
@@ -100,6 +103,97 @@ def _spectral_derivative(halves_bug: bool):
|
|
|
100
103
|
return check_at
|
|
101
104
|
|
|
102
105
|
|
|
106
|
+
# --- heat equation boundaries and sources (batch 5) -------------------------------------------------
|
|
107
|
+
|
|
108
|
+
def _cn_dirichlet_error(n: int, boundary_in_system: bool) -> float:
|
|
109
|
+
"""Crank-Nicolson for u_t = u_xx on [0, 1], u = 0 at both ends, mode sin(pi x), t = 0.05, small dt.
|
|
110
|
+
|
|
111
|
+
With the bug the matrix covers all n points, boundary rows included, and the Dirichlet values are
|
|
112
|
+
written into the solution only after each solve, so the boundary nodes evolve freely meanwhile.
|
|
113
|
+
"""
|
|
114
|
+
x = np.linspace(0.0, 1.0, n)
|
|
115
|
+
dx, t_end, steps = x[1] - x[0], 0.05, 2000
|
|
116
|
+
dt = t_end / steps
|
|
117
|
+
lam = dt / dx**2
|
|
118
|
+
u = np.sin(np.pi * x)
|
|
119
|
+
if boundary_in_system:
|
|
120
|
+
m = n - 2
|
|
121
|
+
D = (np.diag(-2.0 * np.ones(m)) + np.diag(np.ones(m - 1), 1) + np.diag(np.ones(m - 1), -1)) * lam
|
|
122
|
+
A, B = np.eye(m) - D / 2, np.eye(m) + D / 2
|
|
123
|
+
v = u[1:-1]
|
|
124
|
+
for _ in range(steps):
|
|
125
|
+
v = np.linalg.solve(A, B @ v)
|
|
126
|
+
u = np.concatenate([[0.0], v, [0.0]])
|
|
127
|
+
else:
|
|
128
|
+
D = (np.diag(-2.0 * np.ones(n)) + np.diag(np.ones(n - 1), 1) + np.diag(np.ones(n - 1), -1)) * lam
|
|
129
|
+
A, B = np.eye(n) - D / 2, np.eye(n) + D / 2
|
|
130
|
+
for _ in range(steps):
|
|
131
|
+
u = np.linalg.solve(A, B @ u)
|
|
132
|
+
u[0] = u[-1] = 0.0
|
|
133
|
+
return float(np.max(np.abs(u - np.exp(-np.pi**2 * t_end) * np.sin(np.pi * x))))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _dirichlet_case(boundary_in_system: bool):
|
|
137
|
+
ns = (11, 21, 41, 81)
|
|
138
|
+
return lambda: check_order([1.0 / (n - 1) for n in ns], [_cn_dirichlet_error(n, boundary_in_system) for n in ns], 2.0)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _neumann_heat_change(n: int, mirror: bool) -> float:
|
|
142
|
+
"""Relative change of the total heat for u_t = u_xx on [0, 1] with insulated ends (explicit, 400 steps).
|
|
143
|
+
|
|
144
|
+
Control: edge nodes updated with a mirrored ghost value, total heat by the trapezoid rule (exactly
|
|
145
|
+
conserved). Bug: interior update, then u[0] = u[1], u[-1] = u[-2], total heat as sum(u) dx.
|
|
146
|
+
"""
|
|
147
|
+
x = np.linspace(0.0, 1.0, n)
|
|
148
|
+
dx = x[1] - x[0]
|
|
149
|
+
dt = 0.25 * dx**2
|
|
150
|
+
u = np.exp(-((x - 0.3) ** 2) / 0.01)
|
|
151
|
+
|
|
152
|
+
def total(v):
|
|
153
|
+
w = np.full(n, dx)
|
|
154
|
+
if mirror:
|
|
155
|
+
w[[0, -1]] = dx / 2
|
|
156
|
+
return float(np.sum(w * v))
|
|
157
|
+
|
|
158
|
+
start = total(u)
|
|
159
|
+
for _ in range(400):
|
|
160
|
+
new = u.copy()
|
|
161
|
+
new[1:-1] = u[1:-1] + dt / dx**2 * (u[2:] - 2 * u[1:-1] + u[:-2])
|
|
162
|
+
if mirror:
|
|
163
|
+
new[0] = u[0] + dt / dx**2 * 2 * (u[1] - u[0])
|
|
164
|
+
new[-1] = u[-1] + dt / dx**2 * 2 * (u[-2] - u[-1])
|
|
165
|
+
else:
|
|
166
|
+
new[0], new[-1] = new[1], new[-2]
|
|
167
|
+
u = new
|
|
168
|
+
return (total(u) - start) / start
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _source_gain(n: int, times_dt: bool) -> float:
|
|
172
|
+
"""Gain of u under a constant source S = 2 over T = 1 with no diffusion, in n steps; exactly S T = 2."""
|
|
173
|
+
dt, u = 1.0 / n, 0.0
|
|
174
|
+
for _ in range(n):
|
|
175
|
+
u += 2.0 * dt if times_dt else 2.0
|
|
176
|
+
return u
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _well_energy_error(n: int, spacing_from_intervals: bool) -> float:
|
|
180
|
+
"""Lowest level of the infinite square well [0, 2] (hbar = m = 1) with n - 1 interior points.
|
|
181
|
+
|
|
182
|
+
n - 1 interior points split the well into n intervals, so dx = W/n; the bug uses W/(n - 1).
|
|
183
|
+
"""
|
|
184
|
+
W = 2.0
|
|
185
|
+
dx = W / n if spacing_from_intervals else W / (n - 1)
|
|
186
|
+
m = n - 1
|
|
187
|
+
H = (np.diag(np.full(m, 1.0 / dx**2)) + np.diag(np.full(m - 1, -0.5 / dx**2), 1)
|
|
188
|
+
+ np.diag(np.full(m - 1, -0.5 / dx**2), -1))
|
|
189
|
+
return float(abs(np.linalg.eigvalsh(H)[0] - np.pi**2 / (2 * W**2)))
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _spacing_case(spacing_from_intervals: bool):
|
|
193
|
+
ns = (50, 100, 200, 400)
|
|
194
|
+
return lambda: check_order([2.0 / n for n in ns], [_well_energy_error(n, spacing_from_intervals) for n in ns], 2.0)
|
|
195
|
+
|
|
196
|
+
|
|
103
197
|
@dataclass(frozen=True)
|
|
104
198
|
class MethodCase:
|
|
105
199
|
name: str
|
|
@@ -137,6 +231,27 @@ CASES = [
|
|
|
137
231
|
MethodCase("spectral_even_only", True, "wavenumbers built from two halves of the array (fails for odd n)", "G10",
|
|
138
232
|
"studies.size_sweep_check", lambda: size_sweep_check(_spectral_derivative(True)),
|
|
139
233
|
missed_by="any study on power-of-two grids"),
|
|
234
|
+
MethodCase("dirichlet_ok", False, "Crank-Nicolson solving only for interior nodes, Dirichlet values in the system",
|
|
235
|
+
"-", "order.check_order", _dirichlet_case(True)),
|
|
236
|
+
MethodCase("dirichlet_after_solve", True,
|
|
237
|
+
"Crank-Nicolson matrix over all nodes, Dirichlet values written only after each solve", "H01",
|
|
238
|
+
"order.check_order", _dirichlet_case(False),
|
|
239
|
+
missed_by="a single run compared with a loose tolerance (the error is small but does not shrink)"),
|
|
240
|
+
MethodCase("neumann_ok", False, "insulated ends with a mirrored ghost value, trapezoid total", "-",
|
|
241
|
+
"studies.balance_check",
|
|
242
|
+
lambda: balance_check(lambda n: _neumann_heat_change(n, True), 0.0, (21, 41, 81), tol=1e-12)),
|
|
243
|
+
MethodCase("neumann_by_copy", True, "insulated ends made by copying the neighbour value after the update", "H54",
|
|
244
|
+
"studies.balance_check",
|
|
245
|
+
lambda: balance_check(lambda n: _neumann_heat_change(n, False), 0.0, (21, 41, 81), tol=1e-12),
|
|
246
|
+
missed_by="a one-step test with the heat away from the walls"),
|
|
247
|
+
MethodCase("source_ok", False, "source term multiplied by dt", "-", "studies.balance_check",
|
|
248
|
+
lambda: balance_check(lambda n: _source_gain(n, True), 2.0, (10, 100), tol=1e-12)),
|
|
249
|
+
MethodCase("source_without_dt", True, "source term added once per step without the factor dt", "H01",
|
|
250
|
+
"studies.balance_check", lambda: balance_check(lambda n: _source_gain(n, False), 2.0, (10, 100), tol=1e-12),
|
|
251
|
+
missed_by="any test with zero source"),
|
|
252
|
+
MethodCase("spacing_ok", False, "n - 1 interior points with dx = W/n", "-", "order.check_order", _spacing_case(True)),
|
|
253
|
+
MethodCase("spacing_off_by_one", True, "n - 1 interior points with dx = W/(n - 1)", "H19", "order.check_order",
|
|
254
|
+
_spacing_case(False), missed_by="a single comparison at n = 100 with a 5% tolerance"),
|
|
140
255
|
]
|
|
141
256
|
|
|
142
257
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Exact reference solutions that testing public codes needed again and again.
|
|
2
|
+
|
|
3
|
+
Each one was written by hand in several adapters before it moved here (repository IDs as in
|
|
4
|
+
results/corpus/SUMMARY.md). All are numpy-only and vectorised where that is natural.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
# --- statistical mechanics ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def ising2d_exact(L: int, T: float, J: float = 1.0, h: float = 0.0) -> dict:
|
|
17
|
+
"""Exact thermal averages of the periodic L x L Ising model by enumerating all 2^(L*L) states.
|
|
18
|
+
|
|
19
|
+
H = -J sum_<ij> s_i s_j - h sum_i s_i, k_B = 1. Returns per-site <e>, <|m|>, <m^2>, <m^4>, the
|
|
20
|
+
specific heat per site and Z. Practical up to L = 4 (65536 states); used for D04, H06, H55.
|
|
21
|
+
"""
|
|
22
|
+
n = L * L
|
|
23
|
+
if n > 20:
|
|
24
|
+
raise ValueError("enumeration is limited to L*L <= 20 sites (L <= 4)")
|
|
25
|
+
states = ((np.arange(2**n)[:, None] >> np.arange(n)) & 1) * 2 - 1
|
|
26
|
+
s = states.reshape(-1, L, L)
|
|
27
|
+
bonds = np.sum(s * np.roll(s, 1, axis=1), axis=(1, 2)) + np.sum(s * np.roll(s, 1, axis=2), axis=(1, 2))
|
|
28
|
+
M = s.sum(axis=(1, 2))
|
|
29
|
+
E = -J * bonds - h * M
|
|
30
|
+
w = np.exp(-(E - E.min()) / T)
|
|
31
|
+
Z = w.sum()
|
|
32
|
+
avg = lambda q: float(np.sum(w * q) / Z) # noqa: E731
|
|
33
|
+
e, e2 = avg(E) / n, avg(E**2) / n**2
|
|
34
|
+
return {"e": e, "m_abs": avg(np.abs(M)) / n, "m2": avg(M**2) / n**2, "m4": avg(M**4) / n**4,
|
|
35
|
+
"c": n * (e2 - e * e) / T**2, "Z": float(Z * np.exp(-E.min() / T))}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def langevin(x):
|
|
39
|
+
"""L(x) = coth x - 1/x, with the series near 0."""
|
|
40
|
+
x = np.asarray(x, dtype=float)
|
|
41
|
+
small = np.abs(x) < 1e-3
|
|
42
|
+
safe = np.where(small, 1.0, x)
|
|
43
|
+
return np.where(small, x / 3 - x**3 / 45, 1 / np.tanh(safe) - 1 / safe)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def heisenberg_open_chain(N: int, J: float, T: float) -> dict:
|
|
47
|
+
"""Classical unit-vector Heisenberg chain with open ends, H = -J sum S_i.S_{i+1}: exact per-site
|
|
48
|
+
energy and specific heat (each bond is independent). Used for H64."""
|
|
49
|
+
b = J / T
|
|
50
|
+
e = -(N - 1) * J * float(langevin(b)) / N
|
|
51
|
+
c = (N - 1) / N * (1 - b * b / math.sinh(b) ** 2)
|
|
52
|
+
return {"e": e, "c": c}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# --- diffusion and transport -------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def heat_sine_mode(x, t, L: float = 1.0, kappa: float = 1.0, mode: int = 1):
|
|
59
|
+
"""u_t = kappa u_xx on [0, L], u = 0 at both ends, u(x, 0) = sin(mode pi x / L)."""
|
|
60
|
+
k = mode * np.pi / L
|
|
61
|
+
return np.exp(-kappa * k * k * t) * np.sin(k * np.asarray(x, dtype=float))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def advection_diffusion_gaussian(x, t, x0: float, sigma0: float, v: float = 0.0, D: float = 0.0):
|
|
65
|
+
"""Gaussian of peak 1 and width sigma0 at x0, carried at speed v and spread by D on the whole line:
|
|
66
|
+
sigma^2 = sigma0^2 + 2 D t, peak sigma0/sigma. Used for H25."""
|
|
67
|
+
s = math.sqrt(sigma0**2 + 2 * D * t)
|
|
68
|
+
return sigma0 / s * np.exp(-((np.asarray(x, dtype=float) - x0 - v * t) ** 2) / (2 * s * s))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def semi_infinite_step(x, t, alpha: float, T0: float, Ts: float):
|
|
72
|
+
"""Semi-infinite solid at T0, surface held at Ts from t = 0: T = Ts + (T0 - Ts) erf(x / 2 sqrt(alpha t))."""
|
|
73
|
+
x = np.asarray(x, dtype=float)
|
|
74
|
+
return Ts + (T0 - Ts) * np.vectorize(math.erf)(x / (2 * math.sqrt(alpha * t)))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# --- mechanics ---------------------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def double_pendulum_acc(th1, w1, th2, w2, m1, m2, l1, l2, g=9.81):
|
|
81
|
+
"""Angular accelerations of a double pendulum (point masses, massless rods, angles from the downward
|
|
82
|
+
vertical), from the Lagrangian mass matrix. Used for D01, E01, H03, H27."""
|
|
83
|
+
d = th1 - th2
|
|
84
|
+
M = np.array([[(m1 + m2) * l1**2, m2 * l1 * l2 * np.cos(d)], [m2 * l1 * l2 * np.cos(d), m2 * l2**2]])
|
|
85
|
+
r = np.array([-m2 * l1 * l2 * w2**2 * np.sin(d) - (m1 + m2) * g * l1 * np.sin(th1),
|
|
86
|
+
m2 * l1 * l2 * w1**2 * np.sin(d) - m2 * g * l2 * np.sin(th2)])
|
|
87
|
+
return np.linalg.solve(M, r)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def double_pendulum_energy(th1, w1, th2, w2, m1, m2, l1, l2, g=9.81):
|
|
91
|
+
v1sq = (l1 * w1) ** 2
|
|
92
|
+
v2sq = v1sq + (l2 * w2) ** 2 + 2 * l1 * l2 * w1 * w2 * np.cos(th1 - th2)
|
|
93
|
+
return 0.5 * m1 * v1sq + 0.5 * m2 * v2sq - (m1 + m2) * g * l1 * np.cos(th1) - m2 * g * l2 * np.cos(th2)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def cantilever_tip(P: float, L: float, E: float, I: float) -> tuple[float, float]:
|
|
97
|
+
"""Euler-Bernoulli cantilever with an end load: tip deflection P L^3/(3EI) and rotation P L^2/(2EI)."""
|
|
98
|
+
return P * L**3 / (3 * E * I), P * L**2 / (2 * E * I)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# --- quantum mechanics -------------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def infinite_well_energy(n, W: float, hbar: float = 1.0, m: float = 1.0):
|
|
105
|
+
"""E_n = n^2 pi^2 hbar^2 / (2 m W^2), n = 1, 2, ... Used for H19."""
|
|
106
|
+
n = np.asarray(n, dtype=float)
|
|
107
|
+
return n * n * np.pi**2 * hbar**2 / (2 * m * W * W)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def barrier_transmission(E, V: float, a: float, hbar: float = 1.0, m: float = 1.0):
|
|
111
|
+
"""Transmission probability through a square barrier of height V and width a (E != V). Used for H40."""
|
|
112
|
+
E = np.asarray(E, dtype=float)
|
|
113
|
+
k = np.sqrt(2 * m * np.abs(V - E)) / hbar * a
|
|
114
|
+
s2 = np.where(E < V, np.sinh(k) ** 2, np.sin(k) ** 2)
|
|
115
|
+
return 1 / (1 + V * V * s2 / (4 * E * np.abs(V - E)))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def tfim_open_ground_energy(N: int, J: float, h: float) -> float:
|
|
119
|
+
"""Ground energy of the open transverse-field Ising chain H = J sum Z_i Z_{i+1} + h sum X_i, from the
|
|
120
|
+
free-fermion (Jordan-Wigner, Bogoliubov-de Gennes) solution; the sign of J does not matter. Used for H22."""
|
|
121
|
+
A = np.zeros((N, N))
|
|
122
|
+
B = np.zeros((N, N))
|
|
123
|
+
np.fill_diagonal(A, 2 * h)
|
|
124
|
+
for i in range(N - 1):
|
|
125
|
+
A[i, i + 1] = A[i + 1, i] = J
|
|
126
|
+
B[i, i + 1], B[i + 1, i] = J, -J
|
|
127
|
+
eps = np.linalg.eigvalsh(np.block([[A, B], [-B, -A]]))
|
|
128
|
+
return float(-0.5 * np.sum(eps[eps > 0]))
|
|
@@ -186,6 +186,32 @@ def backward_mms_study(integrate_back: Callable, mms: ManufacturedODE, t_end: fl
|
|
|
186
186
|
return check_order(hs, errs, expected_order, **kw)
|
|
187
187
|
|
|
188
188
|
|
|
189
|
+
def balance_check(measure: Callable, expected: float, resolutions, *, tol: float = 1e-9, label: str = "") -> Result:
|
|
190
|
+
"""Check an exact discrete balance at every resolution: `measure(n)` must equal `expected` to `tol`.
|
|
191
|
+
|
|
192
|
+
A conservative scheme keeps a budget exactly on every grid: the total heat with
|
|
193
|
+
insulated walls, the heat gained from a known boundary flux or source over a run.
|
|
194
|
+
Three real codes failed such a budget while still converging: a zero-flux wall
|
|
195
|
+
made by copying the neighbour value lost 8.5% of the heat, a source added
|
|
196
|
+
without the factor dt depended on the number of steps, and boundary nodes
|
|
197
|
+
given full cells took in 7% too much heat. A convergence study alone can pass
|
|
198
|
+
these, so the check reports whether the mismatch shrinks with resolution.
|
|
199
|
+
"""
|
|
200
|
+
rows = []
|
|
201
|
+
for n in resolutions:
|
|
202
|
+
rows.append((n, float(measure(n))))
|
|
203
|
+
bad = [(n, v) for n, v in rows if not abs(v - expected) <= tol]
|
|
204
|
+
prefix = f"{label}: " if label else ""
|
|
205
|
+
listing = ", ".join(f"n={n}: {v:.6g}" for n, v in rows)
|
|
206
|
+
if not bad:
|
|
207
|
+
return Result(PASS, f"{prefix}balance {expected:.6g} kept to {tol:g} at every resolution ({listing})")
|
|
208
|
+
gaps = [abs(v - expected) for _, v in rows]
|
|
209
|
+
shrinking = len(gaps) > 1 and all(b < a for a, b in zip(gaps, gaps[1:]))
|
|
210
|
+
hint = ("; the mismatch shrinks with resolution, so the scheme converges but does not conserve"
|
|
211
|
+
if shrinking else "")
|
|
212
|
+
return Result(FAIL, f"{prefix}expected {expected:.6g}, got {listing}{hint}")
|
|
213
|
+
|
|
214
|
+
|
|
189
215
|
def size_sweep_check(run_at_size: Callable, sizes=(15, 16, 31, 32, 63, 64), *, label: str = "") -> Result:
|
|
190
216
|
"""Call `run_at_size(n)` for odd and even sizes; FAIL if it raises for some of them.
|
|
191
217
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: physverify
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Verification checks for physics simulation code: manufactured solutions, order-of-accuracy studies and a seeded-bug benchmark.
|
|
5
5
|
Author: Ram Chandra Giri
|
|
6
6
|
License-Expression: MIT
|
|
@@ -39,7 +39,7 @@ Dynamic: license-file
|
|
|
39
39
|
|
|
40
40
|
**A tool that checks whether a physics simulation is solving the right equations correctly, not just running without errors.**
|
|
41
41
|
|
|
42
|
-
Status: early version (0.
|
|
42
|
+
Status: early version (0.3.0). It has been tried on 68 public physics repositories (see below), but nobody besides the author has used it yet, so feedback is very welcome.
|
|
43
43
|
|
|
44
44
|
## The problem it addresses
|
|
45
45
|
|
|
@@ -51,7 +51,7 @@ physverify uses a few standard ideas from numerical analysis:
|
|
|
51
51
|
|
|
52
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
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).
|
|
54
|
+
3. **Check conserved quantities and budgets.** Energy, mass or momentum should stay constant (or drift in a predictable way), and heat added through a wall or a source should match what went in, exactly, on every grid.
|
|
55
55
|
4. **Check known special cases.** For example, a projectile with no air resistance has a range you can write down by hand.
|
|
56
56
|
5. **Exact "patch" tests.** Some solutions should be reproduced to computer precision. This catches small bugs that the other checks let through.
|
|
57
57
|
6. **Algebraic checks on the method itself.** For a Runge-Kutta method, whether the coefficients really have the order they claim can be settled exactly, with no test problem (the "order conditions").
|
|
@@ -74,12 +74,14 @@ The bugs and the checks were written by the same person, so these numbers show t
|
|
|
74
74
|
|
|
75
75
|
## Trying it on other people's code
|
|
76
76
|
|
|
77
|
-
To see what it does on code it was not written for,
|
|
77
|
+
To see what it does on code it was not written for, public Python physics repositories from GitHub were run in a locked-down container (October 2026): no network, the repository read-only, nothing from it run on the author's computer. 40 were drawn at random from topic searches (batches 1 to 4); another 60 were the newest small repositories whose README says the work is complete (batch 5), and 20 more were picked because they are quick to test (batch 6); batches 5 and 6 are not random.
|
|
78
78
|
|
|
79
|
-
- **
|
|
80
|
-
- **
|
|
81
|
-
- **The
|
|
82
|
-
- **Our own adapters were wrong about
|
|
79
|
+
- **68 of the 120 could be tested.** Most of the others were not numerical solvers (neural networks, apps and games, analysis tools, finance), one needed a recent NVIDIA GPU, and a few need more set-up.
|
|
80
|
+
- **16 confirmed bugs in 12 of the 68.** For example: a measured energy that counts the magnetic-field term twice; backward time-stepping that is only first-order accurate; Runge-Kutta coefficient tables labelled with a higher order than they have; a spectral derivative that crashes for an odd number of grid points; boundary values applied after the linear solve instead of inside it; an insulated wall that leaks heat; a grid spacing off by one interval; a Kepler solver that fails for negative angles.
|
|
81
|
+
- **The rest had no confirmed bug.** Most passed every check that applied, often to 1e-13 or better.
|
|
82
|
+
- **Our own adapters were wrong about 28 times** before checking (a wrong constant, too-coarse steps, a wrong sign convention, and so on). Every FAIL was checked by hand before being called a bug, and these mistakes are recorded, not hidden.
|
|
83
|
+
|
|
84
|
+
These counts describe the repositories that were drawn; they are not a bug rate for physics code in general.
|
|
83
85
|
|
|
84
86
|
Each lesson became a new check or a clearer message in physverify. The testing and write-ups were done with help from an AI assistant; every number comes from running the code. Authors are told about a finding before their project is named, so the repositories are listed by ID: [results/corpus/SUMMARY.md](https://github.com/rcgiri-physics/physverify/blob/main/results/corpus/SUMMARY.md).
|
|
85
87
|
|
|
@@ -120,7 +122,7 @@ pytest my_spec.py # each check becomes a pytest tes
|
|
|
120
122
|
|
|
121
123
|
## More
|
|
122
124
|
|
|
123
|
-
- [results/corpus/SUMMARY.md](https://github.com/rcgiri-physics/physverify/blob/main/results/corpus/SUMMARY.md): the
|
|
125
|
+
- [results/corpus/SUMMARY.md](https://github.com/rcgiri-physics/physverify/blob/main/results/corpus/SUMMARY.md): the 120 public repositories, what was tested and what was found.
|
|
124
126
|
- [sandbox/README.md](https://github.com/rcgiri-physics/physverify/blob/main/sandbox/README.md): the locked-down container used to run other people's code.
|
|
125
127
|
- [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).
|
|
126
128
|
- [docs/technical.md](https://github.com/rcgiri-physics/physverify/blob/main/docs/technical.md): full details, benchmark tables, limits and related work.
|
|
@@ -15,6 +15,7 @@ src/physverify/method_bugs.py
|
|
|
15
15
|
src/physverify/mms.py
|
|
16
16
|
src/physverify/order.py
|
|
17
17
|
src/physverify/pytest_plugin.py
|
|
18
|
+
src/physverify/references.py
|
|
18
19
|
src/physverify/report.py
|
|
19
20
|
src/physverify/steppers.py
|
|
20
21
|
src/physverify/studies.py
|
|
@@ -58,5 +59,6 @@ tests/test_order.py
|
|
|
58
59
|
tests/test_properties.py
|
|
59
60
|
tests/test_real_projects.py
|
|
60
61
|
tests/test_reference_problems.py
|
|
62
|
+
tests/test_references.py
|
|
61
63
|
tests/test_seeded_bugs.py
|
|
62
64
|
tests/test_studies.py
|
|
@@ -143,3 +143,48 @@ def test_load_definitions_stubs_missing_imports(tmp_path):
|
|
|
143
143
|
src.write_text("import not_installed_plotting_pkg\nimport math\ndef f(x):\n return math.sqrt(x)\n")
|
|
144
144
|
mod = load_definitions_from_path("sim_stub", src, stubs=("not_installed_plotting_pkg",))
|
|
145
145
|
assert mod.f(4.0) == 2.0
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_balance_check_catches_a_converging_but_non_conservative_scheme():
|
|
149
|
+
from physverify.studies import balance_check
|
|
150
|
+
|
|
151
|
+
# a budget that is wrong by 1/n: converges, never conserves (the heat-loss pattern of a real code)
|
|
152
|
+
r = balance_check(lambda n: 1.0 / n, 0.0, (10, 20, 40), tol=1e-12)
|
|
153
|
+
assert r.status == FAIL and "converges but does not conserve" in r.message
|
|
154
|
+
# exact at every resolution
|
|
155
|
+
assert balance_check(lambda n: 2.0, 2.0, (10, 100)).status == PASS
|
|
156
|
+
# wrong and growing with n (a source added without dt): FAIL without the converging hint
|
|
157
|
+
r = balance_check(lambda n: 2.0 * n, 2.0, (10, 100))
|
|
158
|
+
assert r.status == FAIL and "converges" not in r.message
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_stub_modules_provide_names_and_keep_decorated_functions():
|
|
162
|
+
import sys
|
|
163
|
+
|
|
164
|
+
from physverify.loaders import Placeholder, stub_modules
|
|
165
|
+
|
|
166
|
+
names = stub_modules({"no_such_pkg_xyz.metrics": ["score"], "no_such_web_xyz": ["Flask"]})
|
|
167
|
+
try:
|
|
168
|
+
from no_such_pkg_xyz.metrics import score # noqa: F401 (a parent package is created too)
|
|
169
|
+
import no_such_web_xyz
|
|
170
|
+
|
|
171
|
+
app = no_such_web_xyz.Flask(__name__)
|
|
172
|
+
|
|
173
|
+
@app.route("/x", methods=["POST"])
|
|
174
|
+
def view():
|
|
175
|
+
return 42
|
|
176
|
+
|
|
177
|
+
assert view() == 42 and isinstance(score, Placeholder)
|
|
178
|
+
assert stub_modules(["math"]) == [] # a module that imports for real is left alone
|
|
179
|
+
finally:
|
|
180
|
+
for n in names:
|
|
181
|
+
sys.modules.pop(n, None)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_run_script_returns_globals_and_output(tmp_path):
|
|
185
|
+
from physverify.loaders import run_script
|
|
186
|
+
|
|
187
|
+
src = tmp_path / "demo.py"
|
|
188
|
+
src.write_text("import sys\nx = 6 * 7\nprint('hello', sys.argv[1:])\n")
|
|
189
|
+
g, out = run_script(src, argv=["a"])
|
|
190
|
+
assert g["x"] == 42 and "hello ['a']" in out
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Each reference in physverify.references against an independent computation."""
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
import math
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from physverify import references as R
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_ising_enumeration_matches_a_plain_loop():
|
|
12
|
+
L, T, J, h = 3, 2.2, 0.8, 0.3
|
|
13
|
+
Z = Es = Ms = 0.0
|
|
14
|
+
for spins in itertools.product((-1, 1), repeat=L * L):
|
|
15
|
+
s = np.array(spins).reshape(L, L)
|
|
16
|
+
E = 0.0
|
|
17
|
+
for i in range(L):
|
|
18
|
+
for j in range(L):
|
|
19
|
+
E -= J * s[i, j] * (s[(i + 1) % L, j] + s[i, (j + 1) % L]) + h * s[i, j]
|
|
20
|
+
w = math.exp(-E / T)
|
|
21
|
+
Z, Es, Ms = Z + w, Es + w * E, Ms + w * abs(s.sum())
|
|
22
|
+
r = R.ising2d_exact(L, T, J, h)
|
|
23
|
+
assert abs(r["e"] - Es / Z / L**2) < 1e-12
|
|
24
|
+
assert abs(r["m_abs"] - Ms / Z / L**2) < 1e-12
|
|
25
|
+
assert abs(r["Z"] - Z) / Z < 1e-12
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_ising_specific_heat_is_the_temperature_derivative_of_energy():
|
|
29
|
+
dT = 1e-4
|
|
30
|
+
e = lambda T: R.ising2d_exact(4, T)["e"] # noqa: E731
|
|
31
|
+
c_fd = (e(2.3 + dT) - e(2.3 - dT)) / (2 * dT)
|
|
32
|
+
assert abs(R.ising2d_exact(4, 2.3)["c"] - c_fd) < 1e-6
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_heisenberg_chain_bond_energy_by_quadrature():
|
|
36
|
+
# one bond: <cos theta> under exp(b cos theta) on the sphere
|
|
37
|
+
b = 1.7
|
|
38
|
+
z = np.linspace(-1, 1, 200001)
|
|
39
|
+
w = np.exp(b * z)
|
|
40
|
+
mean = np.trapezoid(z * w, z) / np.trapezoid(w, z)
|
|
41
|
+
r = R.heisenberg_open_chain(5, 1.7, 1.0)
|
|
42
|
+
assert abs(r["e"] - (-(4 / 5) * 1.7 * mean)) < 1e-9 # -(N-1)/N J <cos>
|
|
43
|
+
assert abs(float(R.langevin(1e-5)) - 1e-5 / 3) < 1e-15
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_heat_mode_and_gaussian_satisfy_their_equations():
|
|
47
|
+
x, t, h = 0.37, 0.05, 1e-4
|
|
48
|
+
u = lambda x, t: R.heat_sine_mode(x, t, L=2.0, kappa=0.7, mode=3) # noqa: E731
|
|
49
|
+
ut = (u(x, t + h) - u(x, t - h)) / (2 * h)
|
|
50
|
+
uxx = (u(x + h, t) - 2 * u(x, t) + u(x - h, t)) / h**2
|
|
51
|
+
assert abs(ut - 0.7 * uxx) < 1e-5
|
|
52
|
+
g = lambda x, t: R.advection_diffusion_gaussian(x, t, 1.0, 0.5, v=0.3, D=0.2) # noqa: E731
|
|
53
|
+
x, t = 1.4, 2.0
|
|
54
|
+
gt = (g(x, t + h) - g(x, t - h)) / (2 * h)
|
|
55
|
+
gx = (g(x + h, t) - g(x - h, t)) / (2 * h)
|
|
56
|
+
gxx = (g(x + h, t) - 2 * g(x, t) + g(x - h, t)) / h**2
|
|
57
|
+
assert abs(gt + 0.3 * gx - 0.2 * gxx) < 1e-5
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_double_pendulum_conserves_its_energy():
|
|
61
|
+
p = (0.7, 1.9, 1.3, 0.6, 9.81)
|
|
62
|
+
y = np.array([2.0, 0.0, -0.5, 0.3])
|
|
63
|
+
|
|
64
|
+
def f(y):
|
|
65
|
+
a = R.double_pendulum_acc(y[0], y[1], y[2], y[3], *p)
|
|
66
|
+
return np.array([y[1], a[0], y[3], a[1]])
|
|
67
|
+
|
|
68
|
+
E0 = R.double_pendulum_energy(y[0], y[1], y[2], y[3], *p)
|
|
69
|
+
dt = 1e-3
|
|
70
|
+
for _ in range(2000):
|
|
71
|
+
k1 = f(y); k2 = f(y + dt / 2 * k1); k3 = f(y + dt / 2 * k2); k4 = f(y + dt * k3)
|
|
72
|
+
y = y + dt / 6 * (k1 + 2 * k2 + 2 * k3 + k4)
|
|
73
|
+
assert abs(R.double_pendulum_energy(y[0], y[1], y[2], y[3], *p) - E0) < 1e-6 * abs(E0) # RK4, dt = 1e-3, 2 s: drift about 5e-8
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_quantum_references():
|
|
77
|
+
# infinite well against a fine finite-difference matrix
|
|
78
|
+
n, W = 400, 2.0
|
|
79
|
+
dx = W / n
|
|
80
|
+
H = np.diag(np.full(n - 1, 1 / dx**2)) + np.diag(np.full(n - 2, -0.5 / dx**2), 1) + np.diag(np.full(n - 2, -0.5 / dx**2), -1)
|
|
81
|
+
assert abs(np.linalg.eigvalsh(H)[0] / R.infinite_well_energy(1, W) - 1) < 1e-4
|
|
82
|
+
# barrier: unitarity-free textbook limit, T -> 1 for V -> 0 and the E < V tunnelling formula
|
|
83
|
+
assert abs(R.barrier_transmission(0.7, 1e-9, 2.0) - 1) < 1e-8
|
|
84
|
+
k = math.sqrt(2 * 0.3) * 2.0
|
|
85
|
+
assert abs(R.barrier_transmission(0.7, 1.0, 2.0) - 1 / (1 + math.sinh(k) ** 2 / (4 * 0.7 * 0.3))) < 1e-14
|
|
86
|
+
# TFIM against dense diagonalisation
|
|
87
|
+
N, J, h = 6, 1.1, 0.7
|
|
88
|
+
X, Z, I = np.array([[0, 1], [1, 0]]), np.diag([1, -1]), np.eye(2)
|
|
89
|
+
|
|
90
|
+
def op(d):
|
|
91
|
+
out = np.ones((1, 1))
|
|
92
|
+
for q in range(N):
|
|
93
|
+
out = np.kron(out, d.get(q, I))
|
|
94
|
+
return out
|
|
95
|
+
|
|
96
|
+
Hd = sum(J * op({i: Z, i + 1: Z}) for i in range(N - 1)) + sum(h * op({i: X}) for i in range(N))
|
|
97
|
+
assert abs(np.linalg.eigvalsh(Hd)[0] - R.tfim_open_ground_energy(N, J, h)) < 1e-10
|
|
98
|
+
assert R.cantilever_tip(10.0, 5.0, 2e8, 1e-4)[0] == 10.0 * 125.0 / (3 * 2e8 * 1e-4)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_semi_infinite_step_limits():
|
|
102
|
+
assert abs(R.semi_infinite_step(0.0, 1.0, 1e-5, 300.0, 1000.0) - 1000.0) < 1e-12
|
|
103
|
+
assert abs(R.semi_infinite_step(1.0, 1.0, 1e-5, 300.0, 1000.0) - 300.0) < 1e-9
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
"""Import a module from a path without touching its folder.
|
|
2
|
-
|
|
3
|
-
Used by the adapters that test other people's code. Nothing is written next to
|
|
4
|
-
the module (no __pycache__), and optional plotting imports can be stubbed so a
|
|
5
|
-
simulation file can be imported on a machine without matplotlib.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import ast
|
|
11
|
-
import importlib
|
|
12
|
-
import importlib.util
|
|
13
|
-
import sys
|
|
14
|
-
import types
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _stub_module(name: str) -> types.ModuleType:
|
|
19
|
-
return types.ModuleType(name)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def load_module_from_path(name: str, path, stubs=()):
|
|
23
|
-
path = Path(path)
|
|
24
|
-
if not path.is_file():
|
|
25
|
-
raise FileNotFoundError(path)
|
|
26
|
-
|
|
27
|
-
added = []
|
|
28
|
-
for stub in stubs:
|
|
29
|
-
try:
|
|
30
|
-
importlib.import_module(stub)
|
|
31
|
-
except ImportError:
|
|
32
|
-
sys.modules[stub] = _stub_module(stub)
|
|
33
|
-
added.append(stub)
|
|
34
|
-
|
|
35
|
-
old_flag = sys.dont_write_bytecode
|
|
36
|
-
sys.dont_write_bytecode = True
|
|
37
|
-
try:
|
|
38
|
-
spec = importlib.util.spec_from_file_location(name, path)
|
|
39
|
-
module = importlib.util.module_from_spec(spec)
|
|
40
|
-
sys.modules[name] = module
|
|
41
|
-
try:
|
|
42
|
-
spec.loader.exec_module(module)
|
|
43
|
-
except BaseException:
|
|
44
|
-
sys.modules.pop(name, None) # do not leave a half-initialised module behind
|
|
45
|
-
raise
|
|
46
|
-
finally:
|
|
47
|
-
sys.dont_write_bytecode = old_flag
|
|
48
|
-
for stub in added:
|
|
49
|
-
sys.modules.pop(stub, None)
|
|
50
|
-
return module
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def load_definitions_from_path(name: str, path, *, keep=(), inner=(), namespace=None, stubs=()):
|
|
54
|
-
"""Load only the definitions of a script whose top level runs a simulation.
|
|
55
|
-
|
|
56
|
-
Many research scripts parse argv, open a window or run a long simulation at
|
|
57
|
-
import. This executes only the top-level imports, functions and classes, and
|
|
58
|
-
the top-level assignments to the names in `keep` (constants the functions
|
|
59
|
-
use). `inner` names functions defined inside other functions (for example
|
|
60
|
-
marimo or Jupyter-exported cells); they are lifted to module level, and
|
|
61
|
-
their imports must then come from `namespace`.
|
|
62
|
-
|
|
63
|
-
Nothing else in the file runs, so a function that relies on a global set by
|
|
64
|
-
the skipped code fails with NameError; set it on the returned module.
|
|
65
|
-
|
|
66
|
-
`stubs` names modules to replace with empty ones when they are not installed
|
|
67
|
-
(pygame, IPython, orbax, ...), for imports the tested functions do not use.
|
|
68
|
-
"""
|
|
69
|
-
path = Path(path)
|
|
70
|
-
tree = ast.parse(path.read_text(encoding="utf-8"), str(path))
|
|
71
|
-
body = []
|
|
72
|
-
for node in tree.body:
|
|
73
|
-
if isinstance(node, (ast.Import, ast.ImportFrom, ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
74
|
-
body.append(node)
|
|
75
|
-
elif isinstance(node, (ast.Assign, ast.AnnAssign)):
|
|
76
|
-
targets = node.targets if isinstance(node, ast.Assign) else [node.target]
|
|
77
|
-
if any(isinstance(t, ast.Name) and t.id in keep for t in targets):
|
|
78
|
-
body.append(node)
|
|
79
|
-
if inner:
|
|
80
|
-
wanted = set(inner)
|
|
81
|
-
body += [n for n in ast.walk(tree) if isinstance(n, ast.FunctionDef) and n.name in wanted and n not in tree.body]
|
|
82
|
-
for stub in stubs:
|
|
83
|
-
try:
|
|
84
|
-
importlib.import_module(stub)
|
|
85
|
-
except ImportError:
|
|
86
|
-
parent, _, child = stub.rpartition(".")
|
|
87
|
-
sys.modules[stub] = _stub_module(stub)
|
|
88
|
-
if parent in sys.modules:
|
|
89
|
-
setattr(sys.modules[parent], child, sys.modules[stub])
|
|
90
|
-
module = types.ModuleType(name)
|
|
91
|
-
module.__file__ = str(path)
|
|
92
|
-
module.__dict__.update(namespace or {})
|
|
93
|
-
exec(compile(ast.Module(body=body, type_ignores=[]), str(path), "exec"), module.__dict__)
|
|
94
|
-
return module
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|