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.
Files changed (36) hide show
  1. solve_nivp-0.1.2/LICENSE +21 -0
  2. solve_nivp-0.1.2/PKG-INFO +139 -0
  3. solve_nivp-0.1.2/README.md +106 -0
  4. solve_nivp-0.1.2/pyproject.toml +59 -0
  5. solve_nivp-0.1.2/setup.cfg +4 -0
  6. solve_nivp-0.1.2/setup.py +57 -0
  7. solve_nivp-0.1.2/solve_nivp/ODESolver.py +116 -0
  8. solve_nivp-0.1.2/solve_nivp/ODESystem.py +190 -0
  9. solve_nivp-0.1.2/solve_nivp/__init__.py +329 -0
  10. solve_nivp-0.1.2/solve_nivp/_numba_accel.py +91 -0
  11. solve_nivp-0.1.2/solve_nivp/_selftest.py +50 -0
  12. solve_nivp-0.1.2/solve_nivp/adaptive_integrator.py +673 -0
  13. solve_nivp-0.1.2/solve_nivp/integrations.py +801 -0
  14. solve_nivp-0.1.2/solve_nivp/nonlinear_solvers.py +1243 -0
  15. solve_nivp-0.1.2/solve_nivp/projections.py +1024 -0
  16. solve_nivp-0.1.2/solve_nivp/rl/__init__.py +3 -0
  17. solve_nivp-0.1.2/solve_nivp/rl/callbacks.py +125 -0
  18. solve_nivp-0.1.2/solve_nivp/rl/dependency.py +37 -0
  19. solve_nivp-0.1.2/solve_nivp/rl/env.py +323 -0
  20. solve_nivp-0.1.2/solve_nivp.egg-info/PKG-INFO +139 -0
  21. solve_nivp-0.1.2/solve_nivp.egg-info/SOURCES.txt +34 -0
  22. solve_nivp-0.1.2/solve_nivp.egg-info/dependency_links.txt +1 -0
  23. solve_nivp-0.1.2/solve_nivp.egg-info/entry_points.txt +2 -0
  24. solve_nivp-0.1.2/solve_nivp.egg-info/requires.txt +15 -0
  25. solve_nivp-0.1.2/solve_nivp.egg-info/top_level.txt +2 -0
  26. solve_nivp-0.1.2/tests/__init__.py +0 -0
  27. solve_nivp-0.1.2/tests/test_coulomb_projection.py +53 -0
  28. solve_nivp-0.1.2/tests/test_coulomb_projection_jacobian.py +19 -0
  29. solve_nivp-0.1.2/tests/test_globalization.py +23 -0
  30. solve_nivp-0.1.2/tests/test_import_and_api.py +37 -0
  31. solve_nivp-0.1.2/tests/test_integrators_added.py +58 -0
  32. solve_nivp-0.1.2/tests/test_nonlinear_solvers_added.py +71 -0
  33. solve_nivp-0.1.2/tests/test_projection_batch_equivalence.py +76 -0
  34. solve_nivp-0.1.2/tests/test_projections_added.py +136 -0
  35. solve_nivp-0.1.2/tests/test_sparse_semismooth_newton.py +38 -0
  36. solve_nivp-0.1.2/tests/test_threading_time_and_fk.py +75 -0
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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)