solve-nivp 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- solve_nivp-0.1.2/LICENSE +21 -0
- solve_nivp-0.1.2/PKG-INFO +139 -0
- solve_nivp-0.1.2/README.md +106 -0
- solve_nivp-0.1.2/pyproject.toml +59 -0
- solve_nivp-0.1.2/setup.cfg +4 -0
- solve_nivp-0.1.2/setup.py +57 -0
- solve_nivp-0.1.2/solve_nivp/ODESolver.py +116 -0
- solve_nivp-0.1.2/solve_nivp/ODESystem.py +190 -0
- solve_nivp-0.1.2/solve_nivp/__init__.py +329 -0
- solve_nivp-0.1.2/solve_nivp/_numba_accel.py +91 -0
- solve_nivp-0.1.2/solve_nivp/_selftest.py +50 -0
- solve_nivp-0.1.2/solve_nivp/adaptive_integrator.py +673 -0
- solve_nivp-0.1.2/solve_nivp/integrations.py +801 -0
- solve_nivp-0.1.2/solve_nivp/nonlinear_solvers.py +1243 -0
- solve_nivp-0.1.2/solve_nivp/projections.py +1024 -0
- solve_nivp-0.1.2/solve_nivp/rl/__init__.py +3 -0
- solve_nivp-0.1.2/solve_nivp/rl/callbacks.py +125 -0
- solve_nivp-0.1.2/solve_nivp/rl/dependency.py +37 -0
- solve_nivp-0.1.2/solve_nivp/rl/env.py +323 -0
- solve_nivp-0.1.2/solve_nivp.egg-info/PKG-INFO +139 -0
- solve_nivp-0.1.2/solve_nivp.egg-info/SOURCES.txt +34 -0
- solve_nivp-0.1.2/solve_nivp.egg-info/dependency_links.txt +1 -0
- solve_nivp-0.1.2/solve_nivp.egg-info/entry_points.txt +2 -0
- solve_nivp-0.1.2/solve_nivp.egg-info/requires.txt +15 -0
- solve_nivp-0.1.2/solve_nivp.egg-info/top_level.txt +2 -0
- solve_nivp-0.1.2/tests/__init__.py +0 -0
- solve_nivp-0.1.2/tests/test_coulomb_projection.py +53 -0
- solve_nivp-0.1.2/tests/test_coulomb_projection_jacobian.py +19 -0
- solve_nivp-0.1.2/tests/test_globalization.py +23 -0
- solve_nivp-0.1.2/tests/test_import_and_api.py +37 -0
- solve_nivp-0.1.2/tests/test_integrators_added.py +58 -0
- solve_nivp-0.1.2/tests/test_nonlinear_solvers_added.py +71 -0
- solve_nivp-0.1.2/tests/test_projection_batch_equivalence.py +76 -0
- solve_nivp-0.1.2/tests/test_projections_added.py +136 -0
- solve_nivp-0.1.2/tests/test_sparse_semismooth_newton.py +38 -0
- solve_nivp-0.1.2/tests/test_threading_time_and_fk.py +75 -0
solve_nivp-0.1.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 David Riley
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: solve_nivp
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: A Python toolkit for integrating nonsmooth dynamical systems
|
|
5
|
+
Author: David Riley, Ioannis Stefanou
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ERC-INJECT/solve_nivp
|
|
8
|
+
Project-URL: Documentation, https://github.com/ERC-INJECT/solve_nivp/tree/main/docs
|
|
9
|
+
Project-URL: Issues, https://github.com/ERC-INJECT/solve_nivp/issues
|
|
10
|
+
Keywords: nonsmooth dynamics,ODE,DAE,variational inequalities,semismooth Newton,projection methods
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: numpy>=1.20
|
|
21
|
+
Requires-Dist: scipy>=1.8
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
24
|
+
Provides-Extra: rl
|
|
25
|
+
Requires-Dist: gymnasium>=0.29; extra == "rl"
|
|
26
|
+
Requires-Dist: stable-baselines3>=2.2; extra == "rl"
|
|
27
|
+
Requires-Dist: sb3-contrib>=2.2; extra == "rl"
|
|
28
|
+
Requires-Dist: matplotlib>=3.5; extra == "rl"
|
|
29
|
+
Provides-Extra: docs
|
|
30
|
+
Requires-Dist: sphinx>=6; extra == "docs"
|
|
31
|
+
Requires-Dist: sphinx-rtd-theme>=1.2; extra == "docs"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# solve_nivp
|
|
35
|
+
|
|
36
|
+
A Python library for time integration of **nonsmooth** ODE/DAE systems—models
|
|
37
|
+
with abrupt changes such as impacts, switching, or inequality constraints.
|
|
38
|
+
Such models arise in frictional contact mechanics, piecewise and switching
|
|
39
|
+
behaviour in circuits, sliding-mode control, and discontinuous rules in
|
|
40
|
+
finance and energy markets. Classical solvers, which assume smoothness, often
|
|
41
|
+
require regularisation or very small steps due to the inherent stiffness
|
|
42
|
+
of these models. **solve_nivp** builds nonsmooth rules directly into the
|
|
43
|
+
implicit time-stepping scheme, enabling users to encode constraints and advance
|
|
44
|
+
the state robustly.
|
|
45
|
+
|
|
46
|
+
## Key features
|
|
47
|
+
|
|
48
|
+
- **Projection-based constraint encoding.** Users express set-valued or
|
|
49
|
+
nonsmooth relations as projections onto convex sets (Coulomb friction cone,
|
|
50
|
+
sign / normal cone, second-order cone, algebraic constraints). Custom
|
|
51
|
+
projections need only implement `project()` and an optional `tangent_cone()`.
|
|
52
|
+
|
|
53
|
+
- **Nonlinear solvers for nonsmooth problems.** A semismooth Newton method with
|
|
54
|
+
Armijo line search and a variational-inequality (VI) fixed-point iteration,
|
|
55
|
+
both with standard tolerances, safeguards, and iteration diagnostics.
|
|
56
|
+
|
|
57
|
+
- **Implicit integrators.** Backward Euler, Trapezoidal, θ-method, a composite
|
|
58
|
+
TR–BE scheme (Bathe-type, second-order), and an embedded BE–TR error estimator.
|
|
59
|
+
|
|
60
|
+
- **Adaptive step-size control** with Richardson extrapolation.
|
|
61
|
+
|
|
62
|
+
- **Optional RL add-on.** Exposes the time integrator as a Gym-style
|
|
63
|
+
environment for learning adaptive step-size policies (TD3 / TQC via Stable
|
|
64
|
+
Baselines 3).
|
|
65
|
+
|
|
66
|
+
The library is organised around three interchangeable components—projection,
|
|
67
|
+
nonlinear solver, and integrator—so that swapping algorithms during
|
|
68
|
+
experimentation is straightforward. Linear-algebra routines operate on dense or
|
|
69
|
+
sparse arrays in the SciPy ecosystem.
|
|
70
|
+
|
|
71
|
+
## Installation
|
|
72
|
+
|
|
73
|
+
Recommended developer install:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
77
|
+
pip install -U pip
|
|
78
|
+
pip install -e .[test]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Optional extras:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# RL experiments
|
|
85
|
+
pip install -e .[rl]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Quickstart
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
import numpy as np
|
|
92
|
+
from solve_nivp import solve_ivp_ns
|
|
93
|
+
|
|
94
|
+
# simple smooth rhs: y' = -y
|
|
95
|
+
rhs = lambda t, y: -y
|
|
96
|
+
|
|
97
|
+
t_span = (0.0, 1.0)
|
|
98
|
+
y0 = np.array([1.0])
|
|
99
|
+
|
|
100
|
+
# identity projection, VI solver via composite integrator
|
|
101
|
+
sol = solve_ivp_ns(
|
|
102
|
+
fun=rhs,
|
|
103
|
+
t_span=t_span,
|
|
104
|
+
y0=y0,
|
|
105
|
+
method='composite',
|
|
106
|
+
projection='identity',
|
|
107
|
+
solver='VI',
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
print(sol[0][:5], sol[1][:5]) # t, y samples
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
See `examples/` for notebooks on friction stick–slip, bouncing ball (contact/impact), SOC constraints, and sliding-mode control.
|
|
114
|
+
|
|
115
|
+
## Running tests
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
pytest -q
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Building the documentation
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
cd docs
|
|
125
|
+
make clean html
|
|
126
|
+
```
|
|
127
|
+
Open `docs/_build/html/index.html`.
|
|
128
|
+
|
|
129
|
+
## RL experiments (optional)
|
|
130
|
+
|
|
131
|
+
The `RL_Adaption/` folder contains optional experiments (TD3/TQC) for learned adaptivity on challenging nonsmooth problems. Large artifacts are ignored by Git and not required for core installation or testing.
|
|
132
|
+
|
|
133
|
+
## Citation
|
|
134
|
+
|
|
135
|
+
See `CITATION.cff`. If you use this software, please cite the JOSS paper once available.
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT License (see `LICENSE`).
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# solve_nivp
|
|
2
|
+
|
|
3
|
+
A Python library for time integration of **nonsmooth** ODE/DAE systems—models
|
|
4
|
+
with abrupt changes such as impacts, switching, or inequality constraints.
|
|
5
|
+
Such models arise in frictional contact mechanics, piecewise and switching
|
|
6
|
+
behaviour in circuits, sliding-mode control, and discontinuous rules in
|
|
7
|
+
finance and energy markets. Classical solvers, which assume smoothness, often
|
|
8
|
+
require regularisation or very small steps due to the inherent stiffness
|
|
9
|
+
of these models. **solve_nivp** builds nonsmooth rules directly into the
|
|
10
|
+
implicit time-stepping scheme, enabling users to encode constraints and advance
|
|
11
|
+
the state robustly.
|
|
12
|
+
|
|
13
|
+
## Key features
|
|
14
|
+
|
|
15
|
+
- **Projection-based constraint encoding.** Users express set-valued or
|
|
16
|
+
nonsmooth relations as projections onto convex sets (Coulomb friction cone,
|
|
17
|
+
sign / normal cone, second-order cone, algebraic constraints). Custom
|
|
18
|
+
projections need only implement `project()` and an optional `tangent_cone()`.
|
|
19
|
+
|
|
20
|
+
- **Nonlinear solvers for nonsmooth problems.** A semismooth Newton method with
|
|
21
|
+
Armijo line search and a variational-inequality (VI) fixed-point iteration,
|
|
22
|
+
both with standard tolerances, safeguards, and iteration diagnostics.
|
|
23
|
+
|
|
24
|
+
- **Implicit integrators.** Backward Euler, Trapezoidal, θ-method, a composite
|
|
25
|
+
TR–BE scheme (Bathe-type, second-order), and an embedded BE–TR error estimator.
|
|
26
|
+
|
|
27
|
+
- **Adaptive step-size control** with Richardson extrapolation.
|
|
28
|
+
|
|
29
|
+
- **Optional RL add-on.** Exposes the time integrator as a Gym-style
|
|
30
|
+
environment for learning adaptive step-size policies (TD3 / TQC via Stable
|
|
31
|
+
Baselines 3).
|
|
32
|
+
|
|
33
|
+
The library is organised around three interchangeable components—projection,
|
|
34
|
+
nonlinear solver, and integrator—so that swapping algorithms during
|
|
35
|
+
experimentation is straightforward. Linear-algebra routines operate on dense or
|
|
36
|
+
sparse arrays in the SciPy ecosystem.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
Recommended developer install:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
44
|
+
pip install -U pip
|
|
45
|
+
pip install -e .[test]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Optional extras:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# RL experiments
|
|
52
|
+
pip install -e .[rl]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Quickstart
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
import numpy as np
|
|
59
|
+
from solve_nivp import solve_ivp_ns
|
|
60
|
+
|
|
61
|
+
# simple smooth rhs: y' = -y
|
|
62
|
+
rhs = lambda t, y: -y
|
|
63
|
+
|
|
64
|
+
t_span = (0.0, 1.0)
|
|
65
|
+
y0 = np.array([1.0])
|
|
66
|
+
|
|
67
|
+
# identity projection, VI solver via composite integrator
|
|
68
|
+
sol = solve_ivp_ns(
|
|
69
|
+
fun=rhs,
|
|
70
|
+
t_span=t_span,
|
|
71
|
+
y0=y0,
|
|
72
|
+
method='composite',
|
|
73
|
+
projection='identity',
|
|
74
|
+
solver='VI',
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
print(sol[0][:5], sol[1][:5]) # t, y samples
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
See `examples/` for notebooks on friction stick–slip, bouncing ball (contact/impact), SOC constraints, and sliding-mode control.
|
|
81
|
+
|
|
82
|
+
## Running tests
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pytest -q
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Building the documentation
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
cd docs
|
|
92
|
+
make clean html
|
|
93
|
+
```
|
|
94
|
+
Open `docs/_build/html/index.html`.
|
|
95
|
+
|
|
96
|
+
## RL experiments (optional)
|
|
97
|
+
|
|
98
|
+
The `RL_Adaption/` folder contains optional experiments (TD3/TQC) for learned adaptivity on challenging nonsmooth problems. Large artifacts are ignored by Git and not required for core installation or testing.
|
|
99
|
+
|
|
100
|
+
## Citation
|
|
101
|
+
|
|
102
|
+
See `CITATION.cff`. If you use this software, please cite the JOSS paper once available.
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT License (see `LICENSE`).
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "solve_nivp"
|
|
7
|
+
version = "0.1.2"
|
|
8
|
+
description = "A Python toolkit for integrating nonsmooth dynamical systems"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "David Riley" },
|
|
14
|
+
{ name = "Ioannis Stefanou" },
|
|
15
|
+
]
|
|
16
|
+
keywords = [
|
|
17
|
+
"nonsmooth dynamics",
|
|
18
|
+
"ODE",
|
|
19
|
+
"DAE",
|
|
20
|
+
"variational inequalities",
|
|
21
|
+
"semismooth Newton",
|
|
22
|
+
"projection methods",
|
|
23
|
+
]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Development Status :: 4 - Beta",
|
|
26
|
+
"Intended Audience :: Science/Research",
|
|
27
|
+
"License :: OSI Approved :: MIT License",
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
30
|
+
"Topic :: Scientific/Engineering :: Physics",
|
|
31
|
+
]
|
|
32
|
+
dependencies = [
|
|
33
|
+
"numpy>=1.20",
|
|
34
|
+
"scipy>=1.8",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
test = [
|
|
39
|
+
"pytest>=7",
|
|
40
|
+
]
|
|
41
|
+
rl = [
|
|
42
|
+
"gymnasium>=0.29",
|
|
43
|
+
"stable-baselines3>=2.2",
|
|
44
|
+
"sb3-contrib>=2.2",
|
|
45
|
+
"matplotlib>=3.5",
|
|
46
|
+
]
|
|
47
|
+
docs = [
|
|
48
|
+
"sphinx>=6",
|
|
49
|
+
"sphinx-rtd-theme>=1.2",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[project.scripts]
|
|
53
|
+
solve_nivp-selftest = "solve_nivp._selftest:main"
|
|
54
|
+
|
|
55
|
+
[project.urls]
|
|
56
|
+
Homepage = "https://github.com/ERC-INJECT/solve_nivp"
|
|
57
|
+
Documentation = "https://github.com/ERC-INJECT/solve_nivp/tree/main/docs"
|
|
58
|
+
Issues = "https://github.com/ERC-INJECT/solve_nivp/issues"
|
|
59
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from setuptools import setup, find_packages, Command
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
class BuildSphinx(Command):
|
|
6
|
+
description = "Build Sphinx documentation."
|
|
7
|
+
user_options = [
|
|
8
|
+
('builder=', 'b', 'Sphinx builder to use (html, latex)')
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
def initialize_options(self):
|
|
12
|
+
self.builder = 'html'
|
|
13
|
+
self.build_dir = None
|
|
14
|
+
|
|
15
|
+
def finalize_options(self):
|
|
16
|
+
# Build directory for Sphinx output.
|
|
17
|
+
self.build_dir = os.path.join(os.path.dirname(__file__), 'docs/_build')
|
|
18
|
+
|
|
19
|
+
def run(self):
|
|
20
|
+
# First, automatically run sphinx-apidoc to generate .rst files.
|
|
21
|
+
# This documents only the solve_nivp subpackage.
|
|
22
|
+
from sphinx.ext.apidoc import main as sphinx_apidoc_main
|
|
23
|
+
apidoc_args = [
|
|
24
|
+
'--force', # Overwrite existing .rst files.
|
|
25
|
+
'--module-first', # Put module documentation before submodule docs.
|
|
26
|
+
'-o', os.path.join('docs', 'source'), # Output directory for the .rst files.
|
|
27
|
+
'solve_nivp' # Path to the package to document.
|
|
28
|
+
]
|
|
29
|
+
sphinx_apidoc_main(apidoc_args)
|
|
30
|
+
|
|
31
|
+
# Now, build the documentation using Sphinx.
|
|
32
|
+
from sphinx.cmd.build import main as sphinx_main
|
|
33
|
+
args = [
|
|
34
|
+
'-b', self.builder,
|
|
35
|
+
os.path.join('docs', 'source'),
|
|
36
|
+
os.path.join(self.build_dir, self.builder)
|
|
37
|
+
]
|
|
38
|
+
errno = sphinx_main(args)
|
|
39
|
+
if errno:
|
|
40
|
+
raise SystemExit(errno)
|
|
41
|
+
|
|
42
|
+
# For LaTeX, optionally compile to PDF.
|
|
43
|
+
if self.builder == 'latex':
|
|
44
|
+
latex_dir = os.path.join(self.build_dir, 'latex')
|
|
45
|
+
# This assumes you have a Makefile in your LaTeX output directory.
|
|
46
|
+
errno = subprocess.call(['make', 'all-pdf'], cwd=latex_dir)
|
|
47
|
+
if errno:
|
|
48
|
+
raise SystemExit(errno)
|
|
49
|
+
print("PDF generated in:", os.path.join(latex_dir, 'Documentation.pdf'))
|
|
50
|
+
|
|
51
|
+
setup(
|
|
52
|
+
name="solve_nivp",
|
|
53
|
+
version="0.1.2",
|
|
54
|
+
packages=find_packages(), # automatically discovers packages
|
|
55
|
+
description="A solver package for implicit ODEs and projection-based solvers",
|
|
56
|
+
cmdclass={'build_sphinx': BuildSphinx},
|
|
57
|
+
)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import Any, Tuple, List
|
|
3
|
+
|
|
4
|
+
class ODESolver:
|
|
5
|
+
"""Time integration driver (fixed or adaptive) for an ``ODESystem``.
|
|
6
|
+
|
|
7
|
+
Stores growing histories of time grid, states, step sizes and solver
|
|
8
|
+
diagnostics. For adaptive runs, rejected steps do not append entries.
|
|
9
|
+
|
|
10
|
+
Residual semantics
|
|
11
|
+
------------------
|
|
12
|
+
``fk`` list stores the raw implicit residual / function value ``F(y_k)``
|
|
13
|
+
returned by the integrator's nonlinear solve at each *accepted* step. This
|
|
14
|
+
can be useful for post-process diagnostics (e.g. monitoring equilibrium of
|
|
15
|
+
projected components). Entries may be ``None`` if a method does not return
|
|
16
|
+
a residual (should not occur with current integrators).
|
|
17
|
+
"""
|
|
18
|
+
def __init__(self, system: Any, t_span: Tuple[float, float], h: float = 1e-2):
|
|
19
|
+
"""
|
|
20
|
+
Initialize the ODESolver.
|
|
21
|
+
|
|
22
|
+
Parameters:
|
|
23
|
+
system: The ODE system to be integrated.
|
|
24
|
+
t_span: A tuple (t0, tf) specifying the start and end times.
|
|
25
|
+
h: The initial time step size.
|
|
26
|
+
"""
|
|
27
|
+
self.system = system
|
|
28
|
+
self.t0, self.tf = t_span
|
|
29
|
+
self.h_initial = h
|
|
30
|
+
self.t_values: List[float] = [self.t0]
|
|
31
|
+
self.y_values: List[np.ndarray] = [self.system.current_y.copy()]
|
|
32
|
+
self.h_values: List[float] = [h]
|
|
33
|
+
self.error_estimates: List[Tuple[Any, bool, int]] = []
|
|
34
|
+
self.fk: List[Any] = []
|
|
35
|
+
|
|
36
|
+
def solve(self, return_attempts: bool = False) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, List[Tuple[Any, bool, int]]]:
|
|
37
|
+
"""Integrate from ``t0`` to ``tf``.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
return_attempts : bool, default False
|
|
42
|
+
When ``True`` and adaptive stepping is enabled with attempt logging,
|
|
43
|
+
include the raw attempt log as a sixth return value.
|
|
44
|
+
|
|
45
|
+
Returns
|
|
46
|
+
-------
|
|
47
|
+
t_values : ndarray (m,)
|
|
48
|
+
Time points (monotone, includes final time).
|
|
49
|
+
y_values : ndarray (m, n)
|
|
50
|
+
State history.
|
|
51
|
+
h_values : ndarray (m,)
|
|
52
|
+
Step sizes used; first entry equals initial ``h`` guess.
|
|
53
|
+
fk : object ndarray (m,)
|
|
54
|
+
Residual / implicit function evaluations per accepted step.
|
|
55
|
+
error_estimates : list[tuple]
|
|
56
|
+
Per-step tuples ``(solver_error, success, iterations)`` coming from
|
|
57
|
+
the nonlinear solver (solver_error is typically final residual norm).
|
|
58
|
+
attempts : dict or None, optional
|
|
59
|
+
Only returned when ``return_attempts`` is ``True``. Contains arrays of
|
|
60
|
+
attempted times, step sizes, acceptance flags, etc., if recorded.
|
|
61
|
+
"""
|
|
62
|
+
t = self.t0
|
|
63
|
+
h = self.h_initial
|
|
64
|
+
stepper = getattr(self.system, 'adaptive_stepper', None)
|
|
65
|
+
if stepper is not None and hasattr(stepper, 'reset_attempt_log'):
|
|
66
|
+
stepper.reset_attempt_log()
|
|
67
|
+
|
|
68
|
+
# Initialize progress bar for integration.
|
|
69
|
+
# pbar = tqdm(total=self.tf - self.t0, desc='Integration Progress', unit='time unit')
|
|
70
|
+
while t < self.tf:
|
|
71
|
+
# Ensure we do not overshoot the final time.
|
|
72
|
+
h_step = min(h, self.tf - t)
|
|
73
|
+
if self.system.adaptive:
|
|
74
|
+
# Adaptive stepping returns:
|
|
75
|
+
# (y_new, fk_new, h_new, E, success, solver_error, iterations)
|
|
76
|
+
y_new, fk_new, h_new, E, success, solver_error, iterations = self.system.step(t, h_step)
|
|
77
|
+
if success:
|
|
78
|
+
t += h_step
|
|
79
|
+
self.t_values.append(t)
|
|
80
|
+
self.y_values.append(y_new.copy())
|
|
81
|
+
self.fk.append(fk_new.copy() if fk_new is not None else None)
|
|
82
|
+
self.h_values.append(h_new)
|
|
83
|
+
self.error_estimates.append((solver_error, success, iterations))
|
|
84
|
+
self.system.current_y = y_new
|
|
85
|
+
h = h_new # Update step size for next iteration.
|
|
86
|
+
else:
|
|
87
|
+
h = h_new
|
|
88
|
+
# If the adaptive step fails, reduce the step size and try again.
|
|
89
|
+
if h<= self.system.adaptive_stepper.h_min:
|
|
90
|
+
if self.system.verbose:
|
|
91
|
+
print(f"Failed integration: reached minimum step size at t={t:.5f} and step did not converge.")
|
|
92
|
+
break
|
|
93
|
+
else:
|
|
94
|
+
# Fixed stepping mode.
|
|
95
|
+
y_new, fk_new, solver_error, success, iterations = self.system.step(t, h_step)
|
|
96
|
+
t += h_step
|
|
97
|
+
self.t_values.append(t)
|
|
98
|
+
self.y_values.append(y_new.copy())
|
|
99
|
+
self.fk.append(fk_new.copy() if fk_new is not None else None)
|
|
100
|
+
self.h_values.append(h_step)
|
|
101
|
+
self.error_estimates.append((solver_error, success, iterations))
|
|
102
|
+
self.system.current_y = y_new
|
|
103
|
+
# pbar.update(h_step)
|
|
104
|
+
# pbar.close()
|
|
105
|
+
t_arr = np.array(self.t_values)
|
|
106
|
+
y_arr = np.array(self.y_values)
|
|
107
|
+
h_arr = np.array(self.h_values)
|
|
108
|
+
fk_arr = np.array(self.fk, dtype=object)
|
|
109
|
+
|
|
110
|
+
if return_attempts:
|
|
111
|
+
attempt_log = None
|
|
112
|
+
if stepper is not None and hasattr(stepper, 'get_attempt_log'):
|
|
113
|
+
attempt_log = stepper.get_attempt_log()
|
|
114
|
+
return t_arr, y_arr, h_arr, fk_arr, self.error_estimates, attempt_log
|
|
115
|
+
|
|
116
|
+
return t_arr, y_arr, h_arr, fk_arr, self.error_estimates
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import Callable, Optional, Union
|
|
3
|
+
|
|
4
|
+
# Import integration methods from integrations.py.
|
|
5
|
+
# It is assumed that integrations.py is in the same package or accessible via the PYTHONPATH.
|
|
6
|
+
from .integrations import (
|
|
7
|
+
IntegrationMethod,
|
|
8
|
+
BackwardEuler,
|
|
9
|
+
Trapezoidal,
|
|
10
|
+
ThetaMethod,
|
|
11
|
+
CompositeMethod,
|
|
12
|
+
EmbeddedBETR,
|
|
13
|
+
# BDFMethod,
|
|
14
|
+
# AdaptiveSteppingBDF
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# from .adaptive_integrator import AdaptiveStepping
|
|
18
|
+
|
|
19
|
+
class ODESystem:
|
|
20
|
+
"""Encapsulate RHS, initial state and integration method configuration.
|
|
21
|
+
|
|
22
|
+
The system binds a user RHS ``fun`` with an implicit integration method
|
|
23
|
+
(possibly adaptive) and stores current state for the driver. The RHS may
|
|
24
|
+
support signature variants ``fun(t, y)`` or ``fun(t, y, Fk)`` (third
|
|
25
|
+
argument ignored if unused).
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
fun : callable
|
|
30
|
+
ODE right-hand side ``fun(t, y) -> ndarray``.
|
|
31
|
+
y0 : array_like, shape (n,)
|
|
32
|
+
Initial state vector.
|
|
33
|
+
method : str | IntegrationMethod, default 'backward_euler'
|
|
34
|
+
Integration scheme name or pre-instantiated method object.
|
|
35
|
+
a : float, default 1.0
|
|
36
|
+
Auxiliary parameter (currently only placeholder for composite schemes).
|
|
37
|
+
adaptive : bool, default False
|
|
38
|
+
Enable adaptive step controller (two half-step Richardson + PI).
|
|
39
|
+
atol, rtol : float
|
|
40
|
+
Absolute / relative tolerances (adaptive only).
|
|
41
|
+
component_slices : list[slice], optional
|
|
42
|
+
Partition for per-block error norm and projection logic.
|
|
43
|
+
verbose : bool, default False
|
|
44
|
+
Emit basic rejection diagnostics.
|
|
45
|
+
A : ndarray, optional
|
|
46
|
+
Constant mass / descriptor matrix; identity if omitted.
|
|
47
|
+
|
|
48
|
+
Notes
|
|
49
|
+
-----
|
|
50
|
+
Integrator ``step`` methods must return a 5‑tuple ``(y_new, Fk_new, err, success, iterations)``.
|
|
51
|
+
``Fk_new`` is propagated upward and recorded by the driver for diagnostics.
|
|
52
|
+
"""
|
|
53
|
+
def __init__(self,
|
|
54
|
+
fun: Callable[[float, np.ndarray], np.ndarray],
|
|
55
|
+
y0: Union[np.ndarray, list],
|
|
56
|
+
method: Union[str, IntegrationMethod] = 'backward_euler',
|
|
57
|
+
a: float = 1.0,
|
|
58
|
+
adaptive: bool = False,
|
|
59
|
+
atol: float = 1e-6,
|
|
60
|
+
rtol: float = 1e-3,
|
|
61
|
+
component_slices: Optional[list] = None,
|
|
62
|
+
verbose: bool = False,
|
|
63
|
+
A: Optional[np.ndarray] = None,
|
|
64
|
+
record_attempts: bool = False):
|
|
65
|
+
self.fun = fun
|
|
66
|
+
self.y0 = np.array(y0, dtype=float)
|
|
67
|
+
self.current_y = self.y0.copy()
|
|
68
|
+
self.adaptive = adaptive
|
|
69
|
+
self.atol = atol
|
|
70
|
+
self.rtol = rtol
|
|
71
|
+
self.verbose = verbose
|
|
72
|
+
self.component_slices = component_slices
|
|
73
|
+
self.A = A
|
|
74
|
+
self.record_attempts = bool(record_attempts)
|
|
75
|
+
|
|
76
|
+
# If method is an instance of IntegrationMethod, use it directly.
|
|
77
|
+
if isinstance(method, IntegrationMethod):
|
|
78
|
+
self.method = method
|
|
79
|
+
if A is not None:
|
|
80
|
+
self.method.A = A
|
|
81
|
+
# Otherwise, select the integration method based on the provided string.
|
|
82
|
+
elif isinstance(method, str):
|
|
83
|
+
self.method = self._select_method(method.lower(), a, A)
|
|
84
|
+
else:
|
|
85
|
+
raise ValueError("Invalid integration method specification.")
|
|
86
|
+
|
|
87
|
+
# Set up adaptive stepping if requested.
|
|
88
|
+
if self.adaptive:
|
|
89
|
+
# # For BDF methods, use a specialized adaptive stepper.
|
|
90
|
+
# if self.method.__class__.__name__.lower() == 'bdfmethod':
|
|
91
|
+
# self.adaptive_stepper = AdaptiveSteppingBDF(
|
|
92
|
+
# self.method,
|
|
93
|
+
# atol=self.atol,
|
|
94
|
+
# rtol=self.rtol
|
|
95
|
+
# )
|
|
96
|
+
# else:
|
|
97
|
+
# Otherwise, use the generic adaptive stepping mechanism.
|
|
98
|
+
# Always use the generic adaptive stepper
|
|
99
|
+
from .adaptive_integrator import AdaptiveStepping
|
|
100
|
+
self.adaptive_stepper = AdaptiveStepping(
|
|
101
|
+
integrator=self.method,
|
|
102
|
+
component_slices=self.component_slices,
|
|
103
|
+
atol=self.atol,
|
|
104
|
+
rtol=self.rtol,
|
|
105
|
+
verbose=self.verbose,
|
|
106
|
+
record_attempts=self.record_attempts,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def _select_method(self, method_name: str, a: float, A: Optional[np.ndarray]):
|
|
110
|
+
"""
|
|
111
|
+
Select and return an integration method based on the provided method name.
|
|
112
|
+
|
|
113
|
+
Parameters:
|
|
114
|
+
method_name : str
|
|
115
|
+
Name of the integration method.
|
|
116
|
+
a : float
|
|
117
|
+
Parameter used by some integrators.
|
|
118
|
+
A : optional
|
|
119
|
+
Matrix parameter to pass to the integration method.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
An instance of the selected integration method.
|
|
123
|
+
"""
|
|
124
|
+
if method_name == 'backward_euler':
|
|
125
|
+
return BackwardEuler(A=A)
|
|
126
|
+
elif method_name == 'trapezoidal':
|
|
127
|
+
return Trapezoidal(A=A)
|
|
128
|
+
elif method_name == 'theta':
|
|
129
|
+
return ThetaMethod(theta=0.5, A=A)
|
|
130
|
+
elif method_name == 'composite':
|
|
131
|
+
return CompositeMethod(a=a, A=A)
|
|
132
|
+
elif method_name == 'embedded_betr':
|
|
133
|
+
return EmbeddedBETR(A=A)
|
|
134
|
+
else:
|
|
135
|
+
raise ValueError(f"Unknown integration method: {method_name}")
|
|
136
|
+
|
|
137
|
+
def step_fixed(self, t: float, h: float):
|
|
138
|
+
"""
|
|
139
|
+
Perform one fixed-step integration.
|
|
140
|
+
|
|
141
|
+
Parameters:
|
|
142
|
+
t : float
|
|
143
|
+
Current time.
|
|
144
|
+
h : float
|
|
145
|
+
Fixed time step size.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
A tuple containing:
|
|
149
|
+
y_new : array_like, updated state vector.
|
|
150
|
+
f_new : array_like, derivative evaluated at the new state.
|
|
151
|
+
solver_error : float, error reported by the integrator.
|
|
152
|
+
success : bool, solver success flag.
|
|
153
|
+
iterations : int, number of iterations taken by the solver.
|
|
154
|
+
"""
|
|
155
|
+
y_new, f_new, solver_error, success, iterations = self.method.step(self.fun, t, self.current_y, h)
|
|
156
|
+
self.current_y = y_new # Update the system state.
|
|
157
|
+
return y_new, f_new, solver_error, success, iterations
|
|
158
|
+
|
|
159
|
+
def step_adaptive(self, t: float, h: float):
|
|
160
|
+
"""
|
|
161
|
+
Perform one adaptive-step integration using the adaptive stepper.
|
|
162
|
+
|
|
163
|
+
Parameters:
|
|
164
|
+
t : float
|
|
165
|
+
Current time.
|
|
166
|
+
h : float
|
|
167
|
+
Proposed time step size.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
The result of the adaptive stepping procedure.
|
|
171
|
+
"""
|
|
172
|
+
return self.adaptive_stepper.step(self.fun, t, self.current_y, h)
|
|
173
|
+
|
|
174
|
+
def step(self, t: float, h: float):
|
|
175
|
+
"""
|
|
176
|
+
Take one integration step using either adaptive or fixed stepping.
|
|
177
|
+
|
|
178
|
+
Parameters:
|
|
179
|
+
t : float
|
|
180
|
+
Current time.
|
|
181
|
+
h : float
|
|
182
|
+
Time step size.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
The integration step results, which vary depending on the stepping mode.
|
|
186
|
+
"""
|
|
187
|
+
if self.adaptive:
|
|
188
|
+
return self.step_adaptive(t, h)
|
|
189
|
+
else:
|
|
190
|
+
return self.step_fixed(t, h)
|