pynamicalsys 1.0.1__py3-none-any.whl → 1.2.1__py3-none-any.whl
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.
- pynamicalsys/__init__.py +8 -1
- pynamicalsys/__version__.py +2 -2
- pynamicalsys/continuous_time/chaotic_indicators.py +347 -0
- pynamicalsys/continuous_time/models.py +240 -0
- pynamicalsys/continuous_time/numerical_integrators.py +337 -0
- pynamicalsys/continuous_time/trajectory_analysis.py +163 -0
- pynamicalsys/continuous_time/validators.py +114 -0
- pynamicalsys/core/continuous_dynamical_systems.py +777 -1
- pynamicalsys/core/discrete_dynamical_systems.py +44 -45
- pynamicalsys/discrete_time/trajectory_analysis.py +3 -2
- {pynamicalsys-1.0.1.dist-info → pynamicalsys-1.2.1.dist-info}/METADATA +15 -3
- {pynamicalsys-1.0.1.dist-info → pynamicalsys-1.2.1.dist-info}/RECORD +14 -9
- {pynamicalsys-1.0.1.dist-info → pynamicalsys-1.2.1.dist-info}/WHEEL +0 -0
- {pynamicalsys-1.0.1.dist-info → pynamicalsys-1.2.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,337 @@
|
|
1
|
+
# numerical_integrators.py
|
2
|
+
|
3
|
+
# Copyright (C) 2025 Matheus Rolim Sales
|
4
|
+
#
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
8
|
+
# (at your option) any later version.
|
9
|
+
#
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
+
# GNU General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU General Public License
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
17
|
+
|
18
|
+
from typing import Optional, Callable # , Union, Tuple, Dict, List, Any, Sequence
|
19
|
+
from numpy.typing import NDArray
|
20
|
+
import numpy as np
|
21
|
+
from numba import njit, prange
|
22
|
+
|
23
|
+
from pynamicalsys.continuous_time.models import variational_equations
|
24
|
+
|
25
|
+
|
26
|
+
@njit(cache=True)
|
27
|
+
def rk4_step(
|
28
|
+
t: float,
|
29
|
+
u: NDArray[np.float64],
|
30
|
+
parameters: NDArray[np.float64],
|
31
|
+
equations_of_motion: Callable[
|
32
|
+
[float, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
|
33
|
+
],
|
34
|
+
time_step: float = 0.01,
|
35
|
+
) -> NDArray[np.float64]:
|
36
|
+
|
37
|
+
k1 = equations_of_motion(t, u, parameters)
|
38
|
+
k2 = equations_of_motion(t + 0.5 * time_step, u + 0.5 * time_step * k1, parameters)
|
39
|
+
k3 = equations_of_motion(t + 0.5 * time_step, u + 0.5 * time_step * k2, parameters)
|
40
|
+
k4 = equations_of_motion(t + time_step, u + time_step * k3, parameters)
|
41
|
+
|
42
|
+
u_next = u + (time_step / 6.0) * (k1 + 2 * k2 + 2 * k3 + k4)
|
43
|
+
return u_next
|
44
|
+
|
45
|
+
|
46
|
+
@njit(cache=True)
|
47
|
+
def variational_rk4_step(
|
48
|
+
t: float,
|
49
|
+
u: NDArray[np.float64],
|
50
|
+
parameters: NDArray[np.float64],
|
51
|
+
equations_of_motion: Callable[
|
52
|
+
[float, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
|
53
|
+
],
|
54
|
+
jacobian: Callable[
|
55
|
+
[float, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
|
56
|
+
],
|
57
|
+
time_step: float = 0.01,
|
58
|
+
number_of_deviation_vectors: Optional[int] = None,
|
59
|
+
) -> NDArray[np.float64]:
|
60
|
+
|
61
|
+
k1 = variational_equations(
|
62
|
+
t,
|
63
|
+
u,
|
64
|
+
parameters,
|
65
|
+
equations_of_motion,
|
66
|
+
jacobian,
|
67
|
+
number_of_deviation_vectors=number_of_deviation_vectors,
|
68
|
+
)
|
69
|
+
|
70
|
+
k2 = variational_equations(
|
71
|
+
t + 0.5 * time_step,
|
72
|
+
u + 0.5 * time_step * k1,
|
73
|
+
parameters,
|
74
|
+
equations_of_motion,
|
75
|
+
jacobian,
|
76
|
+
number_of_deviation_vectors=number_of_deviation_vectors,
|
77
|
+
)
|
78
|
+
k3 = variational_equations(
|
79
|
+
t + 0.5 * time_step,
|
80
|
+
u + 0.5 * time_step * k2,
|
81
|
+
parameters,
|
82
|
+
equations_of_motion,
|
83
|
+
jacobian,
|
84
|
+
number_of_deviation_vectors=number_of_deviation_vectors,
|
85
|
+
)
|
86
|
+
k4 = variational_equations(
|
87
|
+
t + time_step,
|
88
|
+
u + time_step * k3,
|
89
|
+
parameters,
|
90
|
+
equations_of_motion,
|
91
|
+
jacobian,
|
92
|
+
number_of_deviation_vectors=number_of_deviation_vectors,
|
93
|
+
)
|
94
|
+
|
95
|
+
u_next = u + (time_step / 6.0) * (k1 + 2 * k2 + 2 * k3 + k4)
|
96
|
+
return u_next
|
97
|
+
|
98
|
+
|
99
|
+
# RK45 Dormand–Prince method coefficients
|
100
|
+
RK45_C = np.array([0.0, 1 / 5, 3 / 10, 4 / 5, 8 / 9, 1.0, 1.0])
|
101
|
+
RK45_A = np.array(
|
102
|
+
[
|
103
|
+
[0, 0, 0, 0, 0, 0],
|
104
|
+
[1 / 5, 0, 0, 0, 0, 0],
|
105
|
+
[3 / 40, 9 / 40, 0, 0, 0, 0],
|
106
|
+
[44 / 45, -56 / 15, 32 / 9, 0, 0, 0],
|
107
|
+
[19372 / 6561, -25360 / 2187, 64448 / 6561, -212 / 729, 0, 0],
|
108
|
+
[9017 / 3168, -355 / 33, 46732 / 5247, 49 / 176, -5103 / 18656, 0],
|
109
|
+
[35 / 384, 0, 500 / 1113, 125 / 192, -2187 / 6784, 11 / 84],
|
110
|
+
]
|
111
|
+
)
|
112
|
+
RK45_B5 = np.array([35 / 384, 0, 500 / 1113, 125 / 192, -2187 / 6784, 11 / 84, 0])
|
113
|
+
RK45_B4 = np.array(
|
114
|
+
[5179 / 57600, 0, 7571 / 16695, 393 / 640, -92097 / 339200, 187 / 2100, 1 / 40]
|
115
|
+
)
|
116
|
+
|
117
|
+
|
118
|
+
@njit(cache=True)
|
119
|
+
def rk45_step(t, u, parameters, equations_of_motion, time_step, atol=1e-6, rtol=1e-3):
|
120
|
+
"""Single adaptive step of RK45 (Dormand-Prince).
|
121
|
+
|
122
|
+
Returns:
|
123
|
+
u_new: next state
|
124
|
+
t_new: next time
|
125
|
+
h_new: adjusted step size
|
126
|
+
accept: whether step was accepted
|
127
|
+
"""
|
128
|
+
k = np.empty((7, u.size), dtype=np.float64)
|
129
|
+
for i in range(7):
|
130
|
+
ti = t + RK45_C[i] * time_step
|
131
|
+
ui = u.copy()
|
132
|
+
for j in range(i):
|
133
|
+
ui += time_step * RK45_A[i, j] * k[j]
|
134
|
+
k[i] = equations_of_motion(ti, ui, parameters)
|
135
|
+
|
136
|
+
# Compute 5th and 4th order estimates
|
137
|
+
u5 = u.copy()
|
138
|
+
u4 = u.copy()
|
139
|
+
for i in range(7):
|
140
|
+
u5 += time_step * RK45_B5[i] * k[i]
|
141
|
+
u4 += time_step * RK45_B4[i] * k[i]
|
142
|
+
|
143
|
+
# Compute element-wise error estimate
|
144
|
+
error = np.abs(u5 - u4)
|
145
|
+
scale = atol + rtol * np.maximum(np.abs(u), np.abs(u5))
|
146
|
+
error_ratio = error / scale
|
147
|
+
err = np.max(error_ratio)
|
148
|
+
|
149
|
+
# Adapt step size
|
150
|
+
if err == 0:
|
151
|
+
factor = 2.0
|
152
|
+
else:
|
153
|
+
factor = 0.9 * err**-0.25
|
154
|
+
|
155
|
+
if factor < 0.1:
|
156
|
+
factor = 0.1
|
157
|
+
elif factor > 2.0:
|
158
|
+
factor = 2.0
|
159
|
+
|
160
|
+
time_step_new = time_step * factor
|
161
|
+
|
162
|
+
accept = err < 1.0
|
163
|
+
return u5, t + time_step, time_step_new, accept
|
164
|
+
|
165
|
+
|
166
|
+
@njit(cache=True)
|
167
|
+
def variational_rk45_step(
|
168
|
+
t,
|
169
|
+
u,
|
170
|
+
parameters,
|
171
|
+
equations_of_motion,
|
172
|
+
jacobian,
|
173
|
+
time_step,
|
174
|
+
number_of_deviation_vectors=None,
|
175
|
+
atol=1e-6,
|
176
|
+
rtol=1e-3,
|
177
|
+
):
|
178
|
+
"""Single adaptive step of RK45 (Dormand-Prince).
|
179
|
+
|
180
|
+
Returns:
|
181
|
+
u_new: next state
|
182
|
+
t_new: next time
|
183
|
+
h_new: adjusted step size
|
184
|
+
accept: whether step was accepted
|
185
|
+
"""
|
186
|
+
k = np.empty((7, u.size), dtype=np.float64)
|
187
|
+
for i in range(7):
|
188
|
+
ti = t + RK45_C[i] * time_step
|
189
|
+
ui = u.copy()
|
190
|
+
for j in range(i):
|
191
|
+
ui += time_step * RK45_A[i][j] * k[j]
|
192
|
+
k[i] = variational_equations(
|
193
|
+
ti,
|
194
|
+
ui,
|
195
|
+
parameters,
|
196
|
+
equations_of_motion,
|
197
|
+
jacobian,
|
198
|
+
number_of_deviation_vectors=number_of_deviation_vectors,
|
199
|
+
)
|
200
|
+
|
201
|
+
# Compute 5th and 4th order estimates
|
202
|
+
u5 = u.copy()
|
203
|
+
u4 = u.copy()
|
204
|
+
for i in range(7):
|
205
|
+
u5 += time_step * RK45_B5[i] * k[i]
|
206
|
+
u4 += time_step * RK45_B4[i] * k[i]
|
207
|
+
|
208
|
+
# Compute element-wise error estimate
|
209
|
+
error = np.abs(u5 - u4)
|
210
|
+
scale = atol + rtol * np.maximum(np.abs(u), np.abs(u5))
|
211
|
+
error_ratio = error / scale
|
212
|
+
err = np.max(error_ratio)
|
213
|
+
|
214
|
+
# Adapt step size
|
215
|
+
if err == 0:
|
216
|
+
factor = 2.0
|
217
|
+
else:
|
218
|
+
factor = 0.9 * err**-0.25
|
219
|
+
if factor < 0.1:
|
220
|
+
factor = 0.1
|
221
|
+
elif factor > 2.0:
|
222
|
+
factor = 2.0
|
223
|
+
|
224
|
+
time_step_new = time_step * factor
|
225
|
+
|
226
|
+
accept = err < 1.0
|
227
|
+
return u5, t + time_step, time_step_new, accept
|
228
|
+
|
229
|
+
|
230
|
+
@njit(cache=True)
|
231
|
+
def rk4_step_wrapped(
|
232
|
+
t: float,
|
233
|
+
u: NDArray[np.float64],
|
234
|
+
parameters: NDArray[np.float64],
|
235
|
+
equations_of_motion: Callable[
|
236
|
+
[float, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
|
237
|
+
],
|
238
|
+
jacobian=None,
|
239
|
+
time_step: float = 0.01,
|
240
|
+
number_of_deviation_vectors=None,
|
241
|
+
atol: float = 1e-6, # unused, just to match signature
|
242
|
+
rtol: float = 1e-3, # unused, just to match signature
|
243
|
+
) -> tuple[NDArray[np.float64], float, float, bool]:
|
244
|
+
"""
|
245
|
+
Wrapper around rk4_step to match rk45_step return format.
|
246
|
+
Returns (u_next, t_next, h_next, accept) with accept always True.
|
247
|
+
"""
|
248
|
+
|
249
|
+
if jacobian is None:
|
250
|
+
u_next = rk4_step(t, u, parameters, equations_of_motion, time_step)
|
251
|
+
else:
|
252
|
+
u_next = variational_rk4_step(
|
253
|
+
t,
|
254
|
+
u,
|
255
|
+
parameters,
|
256
|
+
equations_of_motion,
|
257
|
+
jacobian,
|
258
|
+
time_step=time_step,
|
259
|
+
number_of_deviation_vectors=number_of_deviation_vectors,
|
260
|
+
)
|
261
|
+
|
262
|
+
t_next = t + time_step
|
263
|
+
h_next = time_step
|
264
|
+
accept = True
|
265
|
+
return u_next, t_next, h_next, accept
|
266
|
+
|
267
|
+
|
268
|
+
@njit(cache=True)
|
269
|
+
def rk45_step_wrapped(
|
270
|
+
t: float,
|
271
|
+
u: NDArray[np.float64],
|
272
|
+
parameters: NDArray[np.float64],
|
273
|
+
equations_of_motion: Callable[
|
274
|
+
[float, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
|
275
|
+
],
|
276
|
+
jacobian=None,
|
277
|
+
time_step: float = 0.01,
|
278
|
+
number_of_deviation_vectors=None,
|
279
|
+
atol: float = 1e-6,
|
280
|
+
rtol: float = 1e-3,
|
281
|
+
) -> tuple[NDArray[np.float64], float, float, bool]:
|
282
|
+
"""
|
283
|
+
Wrapper around rk4_step to match rk45_step return format.
|
284
|
+
Returns (u_next, t_next, h_next, accept) with accept always True.
|
285
|
+
"""
|
286
|
+
|
287
|
+
if jacobian is None:
|
288
|
+
return rk45_step(
|
289
|
+
t, u, parameters, equations_of_motion, time_step, atol=atol, rtol=rtol
|
290
|
+
)
|
291
|
+
else:
|
292
|
+
return variational_rk45_step(
|
293
|
+
t,
|
294
|
+
u,
|
295
|
+
parameters,
|
296
|
+
equations_of_motion,
|
297
|
+
jacobian,
|
298
|
+
time_step,
|
299
|
+
number_of_deviation_vectors=number_of_deviation_vectors,
|
300
|
+
atol=atol,
|
301
|
+
rtol=rtol,
|
302
|
+
)
|
303
|
+
|
304
|
+
|
305
|
+
@njit(cache=True)
|
306
|
+
def estimate_initial_step(
|
307
|
+
t0: float,
|
308
|
+
u0: np.ndarray,
|
309
|
+
parameters: np.ndarray,
|
310
|
+
equations_of_motion: Callable[[float, np.ndarray, np.ndarray], np.ndarray],
|
311
|
+
order: int = 5, # Dormand-Prince method is 5th order
|
312
|
+
atol: float = 1e-6,
|
313
|
+
rtol: float = 1e-3,
|
314
|
+
) -> float:
|
315
|
+
"""Estimate a good initial time step for adaptive integration."""
|
316
|
+
f0 = equations_of_motion(t0, u0, parameters)
|
317
|
+
|
318
|
+
scale = atol + rtol * np.abs(u0)
|
319
|
+
d0 = np.linalg.norm(u0 / scale)
|
320
|
+
d1 = np.linalg.norm(f0 / scale)
|
321
|
+
|
322
|
+
if d0 < 1e-5 or d1 < 1e-5:
|
323
|
+
h0 = 1e-6
|
324
|
+
else:
|
325
|
+
h0 = 0.01 * d0 / d1
|
326
|
+
|
327
|
+
# Take one Euler step to estimate second derivative
|
328
|
+
u1 = u0 + h0 * f0
|
329
|
+
f1 = equations_of_motion(t0 + h0, u1, parameters)
|
330
|
+
d2 = np.linalg.norm((f1 - f0) / scale) / h0
|
331
|
+
|
332
|
+
if d1 <= 1e-15 and d2 <= 1e-15:
|
333
|
+
h1 = max(1e-6, h0 * 1e-3)
|
334
|
+
else:
|
335
|
+
h1 = (0.01 / max(d1, d2)) ** (1.0 / (order + 1))
|
336
|
+
|
337
|
+
return min(100 * h0, h1)
|
@@ -0,0 +1,163 @@
|
|
1
|
+
# trajectory_analysis.py
|
2
|
+
|
3
|
+
# Copyright (C) 2025 Matheus Rolim Sales
|
4
|
+
#
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
8
|
+
# (at your option) any later version.
|
9
|
+
#
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
+
# GNU General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU General Public License
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
17
|
+
|
18
|
+
from typing import Optional, Callable, Union, Tuple, Dict, List, Any, Sequence
|
19
|
+
from numpy.typing import NDArray
|
20
|
+
import numpy as np
|
21
|
+
from numba import njit, prange
|
22
|
+
|
23
|
+
from pynamicalsys.continuous_time.numerical_integrators import (
|
24
|
+
rk4_step_wrapped,
|
25
|
+
)
|
26
|
+
|
27
|
+
|
28
|
+
@njit(cache=True)
|
29
|
+
def evolve_system(
|
30
|
+
u: NDArray[np.float64],
|
31
|
+
parameters: NDArray[np.float64],
|
32
|
+
total_time: float,
|
33
|
+
equations_of_motion: Callable[
|
34
|
+
[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
|
35
|
+
],
|
36
|
+
time_step: float = 0.01,
|
37
|
+
atol: float = 1e-6,
|
38
|
+
rtol: float = 1e-3,
|
39
|
+
integrator=rk4_step_wrapped,
|
40
|
+
) -> NDArray[np.float64]:
|
41
|
+
|
42
|
+
u = u.copy()
|
43
|
+
|
44
|
+
# number_of_steps = round(total_time / time_step)
|
45
|
+
|
46
|
+
time = 0
|
47
|
+
while time < total_time:
|
48
|
+
if time + time_step > total_time:
|
49
|
+
time_step = total_time - time
|
50
|
+
u_new, time_new, time_step_new, accept = integrator(
|
51
|
+
time,
|
52
|
+
u,
|
53
|
+
parameters,
|
54
|
+
equations_of_motion,
|
55
|
+
time_step=time_step,
|
56
|
+
atol=atol,
|
57
|
+
rtol=rtol,
|
58
|
+
)
|
59
|
+
if accept:
|
60
|
+
time = time_new
|
61
|
+
u = u_new.copy()
|
62
|
+
|
63
|
+
time_step = time_step_new
|
64
|
+
|
65
|
+
return u
|
66
|
+
|
67
|
+
|
68
|
+
@njit(cache=True)
|
69
|
+
def generate_trajectory(
|
70
|
+
u: NDArray[np.float64],
|
71
|
+
parameters: NDArray[np.float64],
|
72
|
+
total_time: float,
|
73
|
+
equations_of_motion: Callable[
|
74
|
+
[np.float64, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
|
75
|
+
],
|
76
|
+
transient_time: Optional[float] = None,
|
77
|
+
time_step: float = 0.01,
|
78
|
+
atol: float = 1e-6,
|
79
|
+
rtol: float = 1e-3,
|
80
|
+
integrator=rk4_step_wrapped,
|
81
|
+
) -> NDArray[np.float64]:
|
82
|
+
|
83
|
+
u = u.copy()
|
84
|
+
if transient_time is not None:
|
85
|
+
u = evolve_system(
|
86
|
+
u,
|
87
|
+
parameters,
|
88
|
+
transient_time,
|
89
|
+
equations_of_motion,
|
90
|
+
time_step=time_step,
|
91
|
+
atol=atol,
|
92
|
+
rtol=rtol,
|
93
|
+
integrator=integrator,
|
94
|
+
)
|
95
|
+
time = transient_time
|
96
|
+
else:
|
97
|
+
time = 0
|
98
|
+
|
99
|
+
neq = len(u)
|
100
|
+
result = np.zeros(neq + 1)
|
101
|
+
trajectory = []
|
102
|
+
|
103
|
+
while time < total_time:
|
104
|
+
if time + time_step > total_time:
|
105
|
+
time_step = total_time - time
|
106
|
+
u_new, time_new, time_step_new, accept = integrator(
|
107
|
+
time,
|
108
|
+
u,
|
109
|
+
parameters,
|
110
|
+
equations_of_motion,
|
111
|
+
time_step=time_step,
|
112
|
+
atol=atol,
|
113
|
+
rtol=rtol,
|
114
|
+
)
|
115
|
+
if accept:
|
116
|
+
time = time_new
|
117
|
+
u = u_new.copy()
|
118
|
+
result = [time]
|
119
|
+
for i in range(neq):
|
120
|
+
result.append(u[i])
|
121
|
+
trajectory.append(result)
|
122
|
+
time_step = time_step_new
|
123
|
+
|
124
|
+
return trajectory
|
125
|
+
|
126
|
+
|
127
|
+
@njit(cache=True, parallel=True)
|
128
|
+
def ensemble_trajectories(
|
129
|
+
u: NDArray[np.float64],
|
130
|
+
parameters: NDArray[np.float64],
|
131
|
+
total_time: float,
|
132
|
+
equations_of_motion: Callable[
|
133
|
+
[np.float64, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
|
134
|
+
],
|
135
|
+
transient_time: Optional[float] = None,
|
136
|
+
time_step: float = 0.01,
|
137
|
+
atol: float = 1e-6,
|
138
|
+
rtol: float = 1e-3,
|
139
|
+
integrator=rk4_step_wrapped,
|
140
|
+
) -> NDArray[np.float64]:
|
141
|
+
|
142
|
+
if u.ndim != 2:
|
143
|
+
raise ValueError("Initial conditions must be 2D array (num_ic, neq)")
|
144
|
+
|
145
|
+
num_ic, neq = u.shape
|
146
|
+
|
147
|
+
trajectories = []
|
148
|
+
|
149
|
+
for i in prange(num_ic):
|
150
|
+
trajectory = generate_trajectory(
|
151
|
+
u[i],
|
152
|
+
parameters,
|
153
|
+
total_time,
|
154
|
+
equations_of_motion,
|
155
|
+
transient_time=transient_time,
|
156
|
+
time_step=time_step,
|
157
|
+
atol=atol,
|
158
|
+
rtol=rtol,
|
159
|
+
integrator=integrator,
|
160
|
+
)
|
161
|
+
trajectories.append(np.array(trajectory))
|
162
|
+
|
163
|
+
return trajectories
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# validators.py
|
2
|
+
|
3
|
+
# Copyright (C) 2025 Matheus Rolim Sales
|
4
|
+
#
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
8
|
+
# (at your option) any later version.
|
9
|
+
#
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
+
# GNU General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU General Public License
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
17
|
+
|
18
|
+
from numbers import Integral, Real
|
19
|
+
from typing import Type, Union
|
20
|
+
|
21
|
+
import numpy as np
|
22
|
+
from numpy.typing import NDArray
|
23
|
+
|
24
|
+
|
25
|
+
def validate_non_negative(
|
26
|
+
value: Union[Integral, int, Real, float],
|
27
|
+
name: str,
|
28
|
+
type_: Type[Union[Integral, int, Real, float]] = Integral,
|
29
|
+
) -> None:
|
30
|
+
if not isinstance(value, type_):
|
31
|
+
raise TypeError(f"{name} must be of type {type_.__name__}")
|
32
|
+
if value < 0:
|
33
|
+
raise ValueError(f"{name} must be non-negative")
|
34
|
+
|
35
|
+
|
36
|
+
def validate_times(transient_time, total_time, type_=Real) -> tuple[float, float]:
|
37
|
+
|
38
|
+
if isinstance(total_time, (Integral, Real)):
|
39
|
+
total_time = float(total_time)
|
40
|
+
else:
|
41
|
+
raise ValueError("total_time must be a valid number")
|
42
|
+
if total_time < 0:
|
43
|
+
raise ValueError("total_time must be non-negative")
|
44
|
+
|
45
|
+
if transient_time is not None:
|
46
|
+
|
47
|
+
if isinstance(transient_time, (Integral, Real)):
|
48
|
+
transient_time = float(transient_time)
|
49
|
+
else:
|
50
|
+
raise ValueError("transient_time must be a valid number")
|
51
|
+
if transient_time < 0:
|
52
|
+
raise ValueError("transient_time must be non-negative")
|
53
|
+
|
54
|
+
if transient_time >= total_time:
|
55
|
+
raise ValueError("transient_time must be less than total_time")
|
56
|
+
|
57
|
+
return transient_time, total_time
|
58
|
+
|
59
|
+
|
60
|
+
def validate_initial_conditions(
|
61
|
+
u, system_dimension, allow_ensemble=True
|
62
|
+
) -> NDArray[np.float64]:
|
63
|
+
if np.isscalar(u):
|
64
|
+
u = np.array([u], dtype=np.float64)
|
65
|
+
else:
|
66
|
+
u = np.asarray(u, dtype=np.float64)
|
67
|
+
if u.ndim not in (1, 2):
|
68
|
+
raise ValueError("Initial condition must be 1D or 2D array")
|
69
|
+
|
70
|
+
u = np.ascontiguousarray(u).copy()
|
71
|
+
|
72
|
+
if u.ndim == 1:
|
73
|
+
if len(u) != system_dimension:
|
74
|
+
raise ValueError(
|
75
|
+
f"1D initial condition must have length {system_dimension}"
|
76
|
+
)
|
77
|
+
elif u.ndim == 2:
|
78
|
+
if not allow_ensemble:
|
79
|
+
raise ValueError(
|
80
|
+
"Ensemble of initial conditions not allowed in this context"
|
81
|
+
)
|
82
|
+
if u.shape[1] != system_dimension:
|
83
|
+
raise ValueError(
|
84
|
+
f"Each initial condition must have length {system_dimension}"
|
85
|
+
)
|
86
|
+
return u
|
87
|
+
|
88
|
+
|
89
|
+
def validate_parameters(parameters, number_of_parameters) -> NDArray[np.float64]:
|
90
|
+
if number_of_parameters == 0:
|
91
|
+
if parameters is not None:
|
92
|
+
raise ValueError("This system does not expect any parameters.")
|
93
|
+
return np.array([0], dtype=np.float64)
|
94
|
+
|
95
|
+
if parameters is None:
|
96
|
+
raise ValueError(
|
97
|
+
f"This system expects {number_of_parameters} parameter(s), but got None."
|
98
|
+
)
|
99
|
+
|
100
|
+
if np.isscalar(parameters):
|
101
|
+
parameters = np.array([parameters], dtype=np.float64)
|
102
|
+
else:
|
103
|
+
parameters = np.asarray(parameters, dtype=np.float64)
|
104
|
+
if parameters.ndim != 1:
|
105
|
+
raise ValueError(
|
106
|
+
f"`parameters` must be a 1D array or scalar. Got shape {parameters.shape}."
|
107
|
+
)
|
108
|
+
|
109
|
+
if parameters.size != number_of_parameters:
|
110
|
+
raise ValueError(
|
111
|
+
f"Expected {number_of_parameters} parameter(s), but got {parameters.size}."
|
112
|
+
)
|
113
|
+
|
114
|
+
return parameters
|