yennefer 0.2.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.
yennefer-0.2.0/LICENSE ADDED
@@ -0,0 +1,98 @@
1
+ The YEN library
2
+ Copyright (c) 2026 Gregory Shipunov (aka. GrindelfP)
3
+
4
+ This project is licensed under the MIT License - see below for the full text.
5
+ The project also includes third-party software components licensed under different terms.
6
+
7
+ ==============================================================================
8
+ PART 1: MIT License (Everything except DOP853 Implementation)
9
+ ==============================================================================
10
+ Copyright (c) 2026 Gregory Shipunov (aka. GrindelfP)
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+
30
+ ==============================================================================
31
+ PART 2: Third-Party Licenses (DOP853 Implementation)
32
+ ==============================================================================
33
+ The DOP853 implementation in this library is a derivative work based on
34
+ the following sources. These components are subject to the terms and
35
+ conditions of their respective licenses:
36
+
37
+ ------------------------------------------------------------------------------
38
+ Source: Modern Fortran Edition of the DOP853
39
+ Author: Jacob Williams (https://github.com/jacobwilliams/dop853)
40
+ License: BSD 3-Clause
41
+ ------------------------------------------------------------------------------
42
+ Copyright (c) 2015-2022, Jacob Williams
43
+ All rights reserved.
44
+
45
+ Redistribution and use in source and binary forms, with or without modification,
46
+ are permitted provided that the following conditions are met:
47
+
48
+ * Redistributions of source code must retain the above copyright notice, this
49
+ list of conditions and the following disclaimer.
50
+
51
+ * Redistributions in binary form must reproduce the above copyright notice, this
52
+ list of conditions and the following disclaimer in the documentation and/or
53
+ other materials provided with the distribution.
54
+
55
+ * The names of its contributors may not be used to endorse or promote products
56
+ derived from this software without specific prior written permission.
57
+
58
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
59
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
60
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
61
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
62
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
63
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
64
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
65
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
66
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
67
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
68
+
69
+ ------------------------------------------------------------------------------
70
+ Source: Original DOP853
71
+ Author: Ernst Hairer (http://www.unige.ch/~hairer/prog/nonstiff/dop853.f)
72
+ License: Hairer License (BSD-style)
73
+ ------------------------------------------------------------------------------
74
+
75
+ Copyright (c) 2004, Ernst Hairer
76
+
77
+ Redistribution and use in source and binary forms, with or without
78
+ modification, are permitted provided that the following conditions are
79
+ met:
80
+
81
+ - Redistributions of source code must retain the above copyright
82
+ notice, this list of conditions and the following disclaimer.
83
+
84
+ - Redistributions in binary form must reproduce the above copyright
85
+ notice, this list of conditions and the following disclaimer in the
86
+ documentation and/or other materials provided with the distribution.
87
+
88
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS �AS
89
+ IS� AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
90
+ TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
91
+ PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
92
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
93
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
94
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
95
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
96
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
97
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
98
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: yennefer
3
+ Version: 0.2.0
4
+ Summary: Fast numerical ODE solvers (RK4, RK45, DOP853) optimized with Numba.
5
+ Author-email: "Gregory Shipunov (aka GrindelfP.)" <grindelf.perlomutrovij@gmail.com>
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: numpy
10
+ Requires-Dist: numba
11
+ Dynamic: license-file
12
+
13
+ # YENNEFER ODE SOLVER
14
+
15
+ ## Description
16
+
17
+ This is an ordinary differential equations systems solver library named Yen (after Yennefer of Vengerberg). Fast numerical ODE solvers (RK4, RK45, DOP853) optimized with Numba.
@@ -0,0 +1,5 @@
1
+ # YENNEFER ODE SOLVER
2
+
3
+ ## Description
4
+
5
+ This is an ordinary differential equations systems solver library named Yen (after Yennefer of Vengerberg). Fast numerical ODE solvers (RK4, RK45, DOP853) optimized with Numba.
@@ -0,0 +1,20 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "yennefer"
7
+ version = "0.2.0"
8
+ description = "Fast numerical ODE solvers (RK4, RK45, DOP853) optimized with Numba."
9
+ readme = "README.md"
10
+ authors = [
11
+ { name = "Gregory Shipunov (aka GrindelfP.)", email = "grindelf.perlomutrovij@gmail.com" }
12
+ ]
13
+ requires-python = ">=3.10"
14
+ dependencies = [
15
+ "numpy",
16
+ "numba"
17
+ ]
18
+
19
+ [tool.setuptools.packages.find]
20
+ include = ["yen*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,13 @@
1
+ """
2
+ yennefer - Numerical integration library for Ordinary Differential Equations.
3
+ """
4
+
5
+ from .rk4 import RK4Solver
6
+ from .rk45 import RK45Solver
7
+ from .dop853 import DOP853Solver
8
+
9
+ __all__ = [
10
+ "RK4Solver",
11
+ "RK45Solver",
12
+ "DOP853Solver",
13
+ ]
@@ -0,0 +1,243 @@
1
+ """
2
+ DOP853 — Dormand-Prince 8(5,3) explicit Runge-Kutta method.
3
+
4
+ Source: Hairer E., Nørsett S.P., Wanner G.
5
+ "Solving Ordinary Differential Equations I", 2nd ed., Springer, 1993.
6
+
7
+ Based on the DOP853 implementation by Ernst Hairer and Jacob Williams.
8
+ Original Fortran source: https://github.com/jacobwilliams/dop853
9
+ See LICENSE file for full license text.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Callable
15
+
16
+ import numpy as np
17
+ from numpy.typing import NDArray
18
+ from numba import njit
19
+
20
+ from .dop853_constants import *
21
+
22
+ F = Callable[[float, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]]
23
+
24
+ @njit
25
+ def _dop853_integrate(
26
+ f: F,
27
+ y0: NDArray[np.float64],
28
+ dt_init: float,
29
+ t_max: float,
30
+ params: NDArray[np.float64],
31
+ rtol: float,
32
+ atol: float,
33
+ n_max_steps: int,
34
+ fac1: float = 0.333,
35
+ fac2: float = 6.0,
36
+ safe: float = 0.9,
37
+ beta: float = 0.0,
38
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
39
+ n_vars = y0.shape[0]
40
+
41
+ # Pre-allocate arrays
42
+ t_arr = np.empty(n_max_steps + 1, dtype=np.float64)
43
+ y_arr = np.empty((n_max_steps + 1, n_vars), dtype=np.float64)
44
+
45
+ t = 0.0
46
+ y = np.copy(y0)
47
+
48
+ t_arr[0] = t
49
+ y_arr[0] = y
50
+
51
+ h = dt_init
52
+ idx = 1
53
+ nstep = 0
54
+
55
+ k1 = f(t, y, params)
56
+
57
+ facold = 1.0e-4
58
+ expo1 = 1.0 / 8.0 - beta * 0.2
59
+ facc1 = 1.0 / fac1
60
+ facc2 = 1.0 / fac2
61
+
62
+ posneg = 1.0 if t_max > 0.0 else -1.0
63
+ reject = False
64
+ last = False
65
+
66
+ while t < t_max and nstep < n_max_steps:
67
+
68
+ if np.abs(h) < np.abs(t) * np.finfo(np.float64).eps:
69
+ # Step size too small
70
+ break
71
+
72
+ if (t + 1.01 * h - t_max) * posneg > 0.0:
73
+ h = t_max - t
74
+ last = True
75
+
76
+ nstep += 1
77
+
78
+ # --- 12 Stages computation ---
79
+ y_tmp = y + h * A21 * k1
80
+ k2 = f(t + C2 * h, y_tmp, params)
81
+
82
+ y_tmp = y + h * (A31 * k1 + A32 * k2)
83
+ k3 = f(t + C3 * h, y_tmp, params)
84
+
85
+ y_tmp = y + h * (A41 * k1 + A43 * k3)
86
+ k4 = f(t + C4 * h, y_tmp, params)
87
+
88
+ y_tmp = y + h * (A51 * k1 + A53 * k3 + A54 * k4)
89
+ k5 = f(t + C5 * h, y_tmp, params)
90
+
91
+ y_tmp = y + h * (A61 * k1 + A64 * k4 + A65 * k5)
92
+ k6 = f(t + C6 * h, y_tmp, params)
93
+
94
+ y_tmp = y + h * (A71 * k1 + A74 * k4 + A75 * k5 + A76 * k6)
95
+ k7 = f(t + C7 * h, y_tmp, params)
96
+
97
+ y_tmp = y + h * (A81 * k1 + A84 * k4 + A85 * k5 + A86 * k6 + A87 * k7)
98
+ k8 = f(t + C8 * h, y_tmp, params)
99
+
100
+ y_tmp = y + h * (A91 * k1 + A94 * k4 + A95 * k5 + A96 * k6 + A97 * k7 + A98 * k8)
101
+ k9 = f(t + C9 * h, y_tmp, params)
102
+
103
+ y_tmp = y + h * (A101 * k1 + A104 * k4 + A105 * k5 + A106 * k6 + A107 * k7 + A108 * k8 + A109 * k9)
104
+ k10 = f(t + C10 * h, y_tmp, params)
105
+
106
+ y_tmp = y + h * (
107
+ A111 * k1 + A114 * k4 + A115 * k5 + A116 * k6 + A117 * k7 + A118 * k8 + A119 * k9 + A1110 * k10)
108
+ k11 = f(t + C11 * h, y_tmp, params)
109
+
110
+ t_next = t + h
111
+ y_tmp = y + h * (
112
+ A121 * k1 + A124 * k4 + A125 * k5 + A126 * k6 + A127 * k7 + A128 * k8 + A129 * k9 + A1210 * k10 + A1211 * k11)
113
+ k12 = f(t_next, y_tmp, params)
114
+
115
+ k_final = B1 * k1 + B6 * k6 + B7 * k7 + B8 * k8 + B9 * k9 + B10 * k10 + B11 * k11 + B12 * k12
116
+ y_next = y + h * k_final
117
+
118
+ # --- Error estimation ---
119
+ err = 0.0
120
+ err2 = 0.0
121
+
122
+ for i in range(n_vars):
123
+ sk = atol + rtol * max(abs(y[i]), abs(y_next[i]))
124
+ erri = k_final[i] - BHH1 * k1[i] - BHH2 * k9[i] - BHH3 * k12[i]
125
+ err2 += (erri / sk) ** 2
126
+
127
+ erri_2 = (ER1 * k1[i] + ER6 * k6[i] + ER7 * k7[i] + ER8 * k8[i] +
128
+ ER9 * k9[i] + ER10 * k10[i] + ER11 * k11[i] + ER12 * k12[i])
129
+ err += (erri_2 / sk) ** 2
130
+
131
+ deno = err + 0.01 * err2
132
+ if deno <= 0.0:
133
+ deno = 1.0
134
+
135
+ err = abs(h) * err * np.sqrt(1.0 / (n_vars * deno))
136
+
137
+ # --- Computation of hnew ---
138
+ fac11 = err ** expo1
139
+ fac = fac11 / (facold ** beta)
140
+ fac = max(facc2, min(facc1, fac / safe))
141
+ h_new = h / fac
142
+
143
+ if err <= 1.0:
144
+ # Step is accepted
145
+ facold = max(err, 1.0e-4)
146
+ k_new_eval = f(t_next, y_next, params) # New k1 mapping
147
+
148
+ k1 = k_new_eval
149
+ y = y_next
150
+ t = t_next
151
+
152
+ t_arr[idx] = t
153
+ y_arr[idx] = y
154
+ idx += 1
155
+
156
+ if last:
157
+ h = h_new
158
+ break
159
+
160
+ if reject:
161
+ h_new = posneg * min(abs(h_new), abs(h))
162
+
163
+ reject = False
164
+ else:
165
+ # Step is rejected
166
+ h_new = h / min(facc1, fac11 / safe)
167
+ reject = True
168
+ last = False
169
+
170
+ h = h_new
171
+
172
+ return t_arr[:idx], y_arr[:idx]
173
+
174
+
175
+ class DOP853Solver:
176
+ """Adaptive-step Runge-Kutta 8th-order ODE solver (Dormand & Prince 8(5,3)).
177
+
178
+ The right-hand side ``f`` must have the signature::
179
+
180
+ f(t: float, y: NDArray[float64], params: NDArray[float64]) -> NDArray[float64]
181
+
182
+ The core method ``_dop853_integrate`` is decorated with ``@njit``
183
+ and is compiled on the first call. For maximum performance, decorate
184
+ ``f`` with ``@njit`` as well before passing it to the solver.
185
+ """
186
+
187
+ def __init__(
188
+ self,
189
+ function: F,
190
+ y0: NDArray[np.float64],
191
+ params: NDArray[np.float64],
192
+ rtol: float = 1e-9,
193
+ atol: float = 1e-9,
194
+ n_max_steps: int = np.inf,
195
+ ) -> None:
196
+ self._f = function
197
+ self._y0 = np.asarray(y0, dtype=np.float64)
198
+ self._params = np.asarray(params, dtype=np.float64)
199
+
200
+ self.rtol = rtol
201
+ self.atol = atol
202
+ self.n_max_steps = n_max_steps
203
+
204
+ self._t: NDArray[np.float64] | None = None
205
+ self._y: NDArray[np.float64] | None = None
206
+
207
+ def solve(self, t_max: float, dt_init: float) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
208
+ """Run the integration from ``t = 0`` to ``t_max``.
209
+
210
+ ``dt`` serves as the initial step size guess (`h_init`) for the adaptive algorithm.
211
+
212
+ Returns
213
+ -------
214
+ t : NDArray[float64], shape (N_accepted_steps,)
215
+ Time grid at accepted adaptive steps.
216
+ y : NDArray[float64], shape (N_accepted_steps, n_vars)
217
+ Solution at each accepted time point.
218
+ """
219
+ self._t, self._y = _dop853_integrate(
220
+ self._f,
221
+ self._y0,
222
+ dt_init,
223
+ t_max,
224
+ self._params,
225
+ self.rtol,
226
+ self.atol,
227
+ self.n_max_steps
228
+ )
229
+ return self._t, self._y
230
+
231
+ @property
232
+ def t(self) -> NDArray[np.float64]:
233
+ """Time grid from the last ``solve()`` call."""
234
+ if self._t is None:
235
+ raise RuntimeError("Call solve() first.")
236
+ return self._t
237
+
238
+ @property
239
+ def y(self) -> NDArray[np.float64]:
240
+ """Solution array from the last ``solve()`` call."""
241
+ if self._y is None:
242
+ raise RuntimeError("Call solve() first.")
243
+ return self._y
@@ -0,0 +1,107 @@
1
+ """
2
+ DOP853 — Dormand-Prince 8(5,3) explicit Runge-Kutta method.
3
+
4
+ Source: Hairer E., Nørsett S.P., Wanner G.
5
+ "Solving Ordinary Differential Equations I", 2nd ed., Springer, 1993.
6
+
7
+ Based on the DOP853 implementation by Ernst Hairer and Jacob Williams.
8
+ Original Fortran source: https://github.com/jacobwilliams/dop853
9
+ See LICENSE file for full license text.
10
+ """
11
+
12
+ # ==============================================================================
13
+ # DOP853 Coefficients
14
+ # ==============================================================================
15
+ C2 = 0.0526001519587677318785587544488
16
+ C3 = 0.0789002279381515978178381316732
17
+ C4 = 0.118350341907227396726757197510
18
+ C5 = 0.281649658092772603273242802490
19
+ C6 = 0.333333333333333333333333333333
20
+ C7 = 0.25
21
+ C8 = 0.307692307692307692307692307692
22
+ C9 = 0.651282051282051282051282051282
23
+ C10 = 0.6
24
+ C11 = 0.857142857142857142857142857142
25
+
26
+ B1 = 0.0542937341165687622380535766363
27
+ B6 = 4.45031289275240888144113950566
28
+ B7 = 1.89151789931450038304281599044
29
+ B8 = -5.8012039600105847814672114227
30
+ B9 = 0.31116436695781989440891606237
31
+ B10 = -0.152160949662516078556178806805
32
+ B11 = 0.201365400804030348374776537501
33
+ B12 = 0.0447106157277725905176885569043
34
+
35
+ BHH1 = 0.244094488188976377952755905512
36
+ BHH2 = 0.733846688281611857341361741547
37
+ BHH3 = 0.0220588235294117647058823529412
38
+
39
+ ER1 = 0.01312004499419488073250102996
40
+ ER6 = -1.225156446376204440720569753
41
+ ER7 = -0.4957589496572501915214079952
42
+ ER8 = 1.664377182454986536961530415
43
+ ER9 = -0.3503288487499736816886487290
44
+ ER10 = 0.3341791187130174790297318841
45
+ ER11 = 0.08192320648511571246570742613
46
+ ER12 = -0.02235530786388629525884427845
47
+
48
+ A21 = 0.0526001519587677318785587544488
49
+
50
+ A31 = 0.0197250569845378994544595329183
51
+ A32 = 0.0591751709536136983633785987549
52
+
53
+ A41 = 0.0295875854768068491816892993775
54
+ A43 = 0.0887627564304205475450678981324
55
+
56
+ A51 = 0.241365134159266685502369798665
57
+ A53 = -0.884549479328286085344864962717
58
+ A54 = 0.924834003261792003115737966543
59
+
60
+ A61 = 0.037037037037037037037037037037
61
+ A64 = 0.170828608729473871279604482173
62
+ A65 = 0.125467687566822425016691814123
63
+
64
+ A71 = 0.037109375
65
+ A74 = 0.170252211019544039314978060272
66
+ A75 = 0.0602165389804559606850219397283
67
+ A76 = -0.017578125
68
+
69
+ A81 = 0.0370920001185047927108779319836
70
+ A84 = 0.170383925712239993810214054705
71
+ A85 = 0.107262030446373284651809199168
72
+ A86 = -0.0153194377486244017527936158236
73
+ A87 = 0.00827378916381402288758473766002
74
+
75
+ A91 = 0.624110958716075717114429577812
76
+ A94 = -3.36089262944694129406857109825
77
+ A95 = -0.868219346841726006818189891453
78
+ A96 = 27.5920996994467083049415600797
79
+ A97 = 20.1540675504778934086186788979
80
+ A98 = -43.4898841810699588477366255144
81
+
82
+ A101 = 0.477662536438264365890433908527
83
+ A104 = -2.48811461997166764192642586468
84
+ A105 = -0.590290826836842996371446475743
85
+ A106 = 21.2300514481811942347288949897
86
+ A107 = 15.2792336328824235832596922938
87
+ A108 = -33.2882109689848629194453265587
88
+ A109 = -0.0203312017085086261358222928593
89
+
90
+ A111 = -0.93714243008598732571704021658
91
+ A114 = 5.18637242884406370830023853209
92
+ A115 = 1.09143734899672957818500254654
93
+ A116 = -8.14978701074692612513997267357
94
+ A117 = -18.5200656599969598641566180701
95
+ A118 = 22.7394870993505042818970056734
96
+ A119 = 2.49360555267965238987089396762
97
+ A1110 = -3.0467644718982195003823669022
98
+
99
+ A121 = 2.27331014751653820792359768449
100
+ A124 = -10.5344954667372501984066689879
101
+ A125 = -2.00087205822486249909675718444
102
+ A126 = -17.9589318631187989172765950534
103
+ A127 = 27.9488845294199600508499808837
104
+ A128 = -2.85899827713502369474065508674
105
+ A129 = -8.87285693353062954433549289258
106
+ A1210 = 12.3605671757943030647266201528
107
+ A1211 = 0.643392746015763530355970484046
@@ -0,0 +1,104 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable
4
+
5
+ import numpy as np
6
+ from numpy.typing import NDArray
7
+ from numba import njit
8
+
9
+
10
+ F = Callable[[float, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]]
11
+
12
+
13
+ @njit
14
+ def _rk4_step(
15
+ f: F,
16
+ t: float,
17
+ y: NDArray[np.float64],
18
+ dt: float,
19
+ params: NDArray[np.float64],
20
+ ) -> NDArray[np.float64]:
21
+ k1 = f(t, y, params)
22
+ k2 = f(t + 0.5 * dt, y + 0.5*dt*k1, params)
23
+ k3 = f(t + 0.5 * dt, y + 0.5*dt*k2, params)
24
+ k4 = f(t + dt, y + dt*k3, params)
25
+ return y + (dt / 6.0) * (k1 + 2.0*k2 + 2.0*k3 + k4)
26
+
27
+
28
+ @njit
29
+ def _rk4_integrate(
30
+ f: F,
31
+ y0: NDArray[np.float64],
32
+ dt: float,
33
+ t_max: float,
34
+ params: NDArray[np.float64],
35
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
36
+ n_steps = int(t_max / dt) + 1
37
+ n_vars = y0.shape[0]
38
+
39
+ t_arr = np.empty(n_steps, dtype=np.float64)
40
+ y_arr = np.empty((n_steps, n_vars), dtype=np.float64)
41
+
42
+ t_arr[0] = 0.0
43
+ y_arr[0] = y0
44
+
45
+ for i in range(1, n_steps):
46
+ t_arr[i] = t_arr[i - 1] + dt
47
+ y_arr[i] = _rk4_step(f, t_arr[i - 1], y_arr[i - 1], dt, params)
48
+
49
+ return t_arr, y_arr
50
+
51
+
52
+ class RK4Solver:
53
+ """Fixed-step Runge-Kutta 4th-order ODE solver.
54
+
55
+ The right-hand side ``f`` must have the signature::
56
+
57
+ f(t: float, y: NDArray[float64], params: NDArray[float64]) -> NDArray[float64]
58
+
59
+ Both ``_rk4_step`` and ``_rk4_integrate`` are decorated with ``@njit``
60
+ and are compiled on the first call. For maximum performance, decorate
61
+ ``f`` with ``@njit`` as well before passing it to the solver.
62
+ """
63
+
64
+ def __init__(
65
+ self,
66
+ function: F,
67
+ y0: NDArray[np.float64],
68
+ params: NDArray[np.float64],
69
+ ) -> None:
70
+ self._f = function
71
+ self._y0 = np.asarray(y0, dtype=np.float64)
72
+ self._params = np.asarray(params, dtype=np.float64)
73
+
74
+ self._t: NDArray[np.float64] | None = None
75
+ self._y: NDArray[np.float64] | None = None
76
+
77
+ def solve(self, t_max: float, dt: float) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
78
+ """Run the integration from ``t = 0`` to ``t_max``.
79
+
80
+ Returns
81
+ -------
82
+ t : NDArray[float64], shape (N,)
83
+ Time grid.
84
+ y : NDArray[float64], shape (N, n_vars)
85
+ Solution at each time point.
86
+ """
87
+ self._t, self._y = _rk4_integrate(
88
+ self._f, self._y0, dt, t_max, self._params
89
+ )
90
+ return self._t, self._y
91
+
92
+ @property
93
+ def t(self) -> NDArray[np.float64]:
94
+ """Time grid from the last ``solve()`` call."""
95
+ if self._t is None:
96
+ raise RuntimeError("Call solve() first.")
97
+ return self._t
98
+
99
+ @property
100
+ def y(self) -> NDArray[np.float64]:
101
+ """Solution array from the last ``solve()`` call, shape (N, n_vars)."""
102
+ if self._y is None:
103
+ raise RuntimeError("Call solve() first.")
104
+ return self._y
@@ -0,0 +1,182 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable
4
+
5
+ import numpy as np
6
+ from numpy.typing import NDArray
7
+ from numba import njit
8
+
9
+ F = Callable[[float, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]]
10
+
11
+
12
+ @njit
13
+ def _rkf45_step(
14
+ f: F,
15
+ t: float,
16
+ y: NDArray[np.float64],
17
+ dt: float,
18
+ params: NDArray[np.float64],
19
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
20
+ k1 = f(t, y, params)
21
+ k2 = f(t + 0.25 * dt, y + dt * (0.25 * k1), params)
22
+ k3 = f(t + 0.375 * dt, y + dt * (3.0 / 32.0 * k1 + 9.0 / 32.0 * k2), params)
23
+ k4 = f(t + (12.0 / 13.0) * dt, y + dt * (1932.0 / 2197.0 * k1 - 7200.0 / 2197.0 * k2 + 7296.0 / 2197.0 * k3),
24
+ params)
25
+ k5 = f(t + dt, y + dt * (439.0 / 216.0 * k1 - 8.0 * k2 + 3680.0 / 513.0 * k3 - 845.0 / 4104.0 * k4), params)
26
+ k6 = f(t + 0.5 * dt,
27
+ y + dt * (-8.0 / 27.0 * k1 + 2.0 * k2 - 3544.0 / 2565.0 * k3 + 1859.0 / 4104.0 * k4 - 11.0 / 40.0 * k5),
28
+ params)
29
+
30
+ y5 = y + dt * (
31
+ 16.0 / 135.0 * k1 + 6656.0 / 12825.0 * k3 + 28561.0 / 56430.0 * k4 - 9.0 / 50.0 * k5 + 2.0 / 55.0 * k6)
32
+
33
+ y4 = y + dt * (25.0 / 216.0 * k1 + 1408.0 / 2565.0 * k3 + 2197.0 / 4104.0 * k4 - 1.0 / 5.0 * k5)
34
+
35
+ y_err = y5 - y4
36
+
37
+ return y5, y_err
38
+
39
+
40
+ @njit
41
+ def _rkf45_integrate(
42
+ f: F,
43
+ y0: NDArray[np.float64],
44
+ dt_initial: float,
45
+ t_max: float,
46
+ params: NDArray[np.float64],
47
+ atol: float,
48
+ rtol: float,
49
+ max_step: float,
50
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
51
+ n_vars = y0.shape[0]
52
+
53
+ capacity = 1000
54
+ t_arr = np.empty(capacity, dtype=np.float64)
55
+ y_arr = np.empty((capacity, n_vars), dtype=np.float64)
56
+
57
+ t_arr[0] = 0.0
58
+ y_arr[0] = y0
59
+
60
+ t = 0.0
61
+ y = y0
62
+ dt = dt_initial
63
+ step_idx = 1
64
+
65
+ while t < t_max:
66
+ if t + dt > t_max:
67
+ dt = t_max - t
68
+
69
+ y_new, y_err = _rkf45_step(f, t, y, dt, params)
70
+
71
+ scale = atol + np.maximum(np.abs(y), np.abs(y_new)) * rtol
72
+ error_norm = np.max(np.abs(y_err) / scale)
73
+
74
+ if error_norm <= 1.0:
75
+ t = t + dt
76
+ y = y_new
77
+
78
+ if step_idx >= capacity:
79
+ new_capacity = capacity * 2
80
+ new_t_arr = np.empty(new_capacity, dtype=np.float64)
81
+ new_y_arr = np.empty((new_capacity, n_vars), dtype=np.float64)
82
+ new_t_arr[:capacity] = t_arr
83
+ new_y_arr[:capacity] = y_arr
84
+ t_arr = new_t_arr
85
+ y_arr = new_y_arr
86
+ capacity = new_capacity
87
+
88
+ t_arr[step_idx] = t
89
+ y_arr[step_idx] = y
90
+ step_idx += 1
91
+
92
+ if error_norm == 0.0:
93
+ factor = 5.0
94
+ else:
95
+ factor = 0.9 * (1.0 / error_norm) ** 0.25
96
+
97
+ factor = min(5.0, max(0.1, factor))
98
+ dt = dt * factor
99
+
100
+ if dt > max_step:
101
+ dt = max_step
102
+
103
+ return t_arr[:step_idx], y_arr[:step_idx]
104
+
105
+
106
+ class RK45Solver:
107
+ """Adaptive-step Runge-Kutta-Fehlberg 4(5) ODE solver.
108
+
109
+ The right-hand side ``f`` must have the signature::
110
+
111
+ f(t: float, y: NDArray[float64], params: NDArray[float64]) -> NDArray[float64]
112
+
113
+ Both ``_rkf45_step`` and ``_rkf45_integrate`` are decorated with ``@njit``
114
+ and are compiled on the first call. For maximum performance, decorate
115
+ ``f`` with ``@njit`` as well before passing it to the solver.
116
+ """
117
+
118
+ def __init__(
119
+ self,
120
+ function: F,
121
+ y0: NDArray[np.float64],
122
+ params: NDArray[np.float64],
123
+ atol: float = 1e-6,
124
+ rtol: float = 1e-3,
125
+ max_step: float = np.inf,
126
+ ) -> None:
127
+ self._f = function
128
+ self._y0 = np.asarray(y0, dtype=np.float64)
129
+ self._params = np.asarray(params, dtype=np.float64)
130
+
131
+ self.rtol = rtol
132
+ self.atol = atol
133
+ self.max_step = max_step
134
+
135
+ self._t: NDArray[np.float64] | None = None
136
+ self._y: NDArray[np.float64] | None = None
137
+
138
+ def solve(
139
+ self,
140
+ t_max: float,
141
+ dt_initial: float = 1e-3,
142
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
143
+ """Run the adaptive integration from ``t = 0`` to ``t_max``.
144
+
145
+ Parameters
146
+ ----------
147
+ t_max : float
148
+ Maximum integration time.
149
+ dt_initial : float, optional
150
+ Initial step size guess, by default 1e-3.
151
+ atol : float, optional
152
+ Absolute tolerance for error control, by default 1e-6.
153
+ rtol : float, optional
154
+ Relative tolerance for error control, by default 1e-3.
155
+ max_step : float, optional
156
+ Maximum allowed step size, by default infinity.
157
+
158
+ Returns
159
+ -------
160
+ t : NDArray[float64], shape (N,)
161
+ Time grid (adaptively determined).
162
+ y : NDArray[float64], shape (N, n_vars)
163
+ Solution at each time point.
164
+ """
165
+ self._t, self._y = _rkf45_integrate(
166
+ self._f, self._y0, dt_initial, t_max, self._params, self.atol, self.rtol, self.max_step
167
+ )
168
+ return self._t, self._y
169
+
170
+ @property
171
+ def t(self) -> NDArray[np.float64]:
172
+ """Time grid from the last ``solve()`` call."""
173
+ if self._t is None:
174
+ raise RuntimeError("Call solve() first.")
175
+ return self._t
176
+
177
+ @property
178
+ def y(self) -> NDArray[np.float64]:
179
+ """Solution array from the last ``solve()`` call, shape (N, n_vars)."""
180
+ if self._y is None:
181
+ raise RuntimeError("Call solve() first.")
182
+ return self._y
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: yennefer
3
+ Version: 0.2.0
4
+ Summary: Fast numerical ODE solvers (RK4, RK45, DOP853) optimized with Numba.
5
+ Author-email: "Gregory Shipunov (aka GrindelfP.)" <grindelf.perlomutrovij@gmail.com>
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: numpy
10
+ Requires-Dist: numba
11
+ Dynamic: license-file
12
+
13
+ # YENNEFER ODE SOLVER
14
+
15
+ ## Description
16
+
17
+ This is an ordinary differential equations systems solver library named Yen (after Yennefer of Vengerberg). Fast numerical ODE solvers (RK4, RK45, DOP853) optimized with Numba.
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ yennefer/__init__.py
5
+ yennefer/dop853.py
6
+ yennefer/dop853_constants.py
7
+ yennefer/rk4.py
8
+ yennefer/rk45.py
9
+ yennefer.egg-info/PKG-INFO
10
+ yennefer.egg-info/SOURCES.txt
11
+ yennefer.egg-info/dependency_links.txt
12
+ yennefer.egg-info/requires.txt
13
+ yennefer.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ numpy
2
+ numba
@@ -0,0 +1 @@
1
+ yennefer