pynamicalsys 1.0.1__py3-none-any.whl → 1.2.2__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.2.dist-info}/METADATA +35 -15
- {pynamicalsys-1.0.1.dist-info → pynamicalsys-1.2.2.dist-info}/RECORD +14 -9
- {pynamicalsys-1.0.1.dist-info → pynamicalsys-1.2.2.dist-info}/WHEEL +0 -0
- {pynamicalsys-1.0.1.dist-info → pynamicalsys-1.2.2.dist-info}/top_level.txt +0 -0
pynamicalsys/__init__.py
CHANGED
@@ -16,9 +16,16 @@
|
|
16
16
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
17
17
|
|
18
18
|
from pynamicalsys.core.discrete_dynamical_systems import DiscreteDynamicalSystem
|
19
|
+
from pynamicalsys.core.continuous_dynamical_systems import ContinuousDynamicalSystem
|
19
20
|
from pynamicalsys.core.basin_metrics import BasinMetrics
|
20
21
|
from pynamicalsys.core.plot_styler import PlotStyler
|
21
22
|
from pynamicalsys.core.time_series_metrics import TimeSeriesMetrics
|
22
23
|
from .__version__ import __version__
|
23
24
|
|
24
|
-
__all__ = [
|
25
|
+
__all__ = [
|
26
|
+
"DiscreteDynamicalSystem",
|
27
|
+
"ContinuousDynamicalSystem",
|
28
|
+
"PlotStyler",
|
29
|
+
"TimeSeriesMetrics",
|
30
|
+
"BasinMetrics",
|
31
|
+
]
|
pynamicalsys/__version__.py
CHANGED
@@ -0,0 +1,347 @@
|
|
1
|
+
# chaotic_indicators.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.common.utils import qr
|
24
|
+
from pynamicalsys.continuous_time.trajectory_analysis import evolve_system
|
25
|
+
from pynamicalsys.continuous_time.numerical_integrators import rk4_step_wrapped
|
26
|
+
|
27
|
+
|
28
|
+
@njit(cache=True)
|
29
|
+
def lyapunov_exponents(
|
30
|
+
u: NDArray[np.float64],
|
31
|
+
parameters: NDArray[np.float64],
|
32
|
+
total_time: float,
|
33
|
+
equations_of_motion: Callable[
|
34
|
+
[np.float64, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
|
35
|
+
],
|
36
|
+
jacobian: Callable[
|
37
|
+
[np.float64, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
|
38
|
+
],
|
39
|
+
transient_time: Optional[float] = None,
|
40
|
+
time_step: float = 0.01,
|
41
|
+
atol: float = 1e-6,
|
42
|
+
rtol: float = 1e-3,
|
43
|
+
integrator=rk4_step_wrapped,
|
44
|
+
return_history: bool = False,
|
45
|
+
seed: int = 13,
|
46
|
+
log_base: float = np.e,
|
47
|
+
QR: Callable[
|
48
|
+
[NDArray[np.float64]], Tuple[NDArray[np.float64], NDArray[np.float64]]
|
49
|
+
] = qr,
|
50
|
+
) -> NDArray[np.float64]:
|
51
|
+
|
52
|
+
neq = len(u) # Number of equations of the system
|
53
|
+
nt = neq + neq**2 # Total number of equations including variational equations
|
54
|
+
|
55
|
+
u = u.copy()
|
56
|
+
|
57
|
+
# Handle transient time
|
58
|
+
if transient_time is not None:
|
59
|
+
u = evolve_system(
|
60
|
+
u,
|
61
|
+
parameters,
|
62
|
+
transient_time,
|
63
|
+
equations_of_motion,
|
64
|
+
time_step=time_step,
|
65
|
+
atol=atol,
|
66
|
+
rtol=rtol,
|
67
|
+
integrator=integrator,
|
68
|
+
)
|
69
|
+
sample_time = total_time - transient_time
|
70
|
+
time = transient_time
|
71
|
+
else:
|
72
|
+
sample_time = total_time
|
73
|
+
time = 0
|
74
|
+
|
75
|
+
# State + deviation vectors
|
76
|
+
uv = np.zeros(nt)
|
77
|
+
uv[:neq] = u.copy()
|
78
|
+
|
79
|
+
# Randomly define the deviation vectors and orthonormalize them
|
80
|
+
np.random.seed(seed)
|
81
|
+
uv[neq:] = -1 + 2 * np.random.rand(nt - neq)
|
82
|
+
v = uv[neq:].reshape(neq, neq)
|
83
|
+
v, _ = QR(v)
|
84
|
+
uv[neq:] = v.reshape(neq**2)
|
85
|
+
|
86
|
+
exponents = np.zeros(neq, dtype=np.float64)
|
87
|
+
history = []
|
88
|
+
|
89
|
+
while time < total_time:
|
90
|
+
if time + time_step > total_time:
|
91
|
+
time_step = total_time - time
|
92
|
+
|
93
|
+
uv_new, time_new, time_step_new, accept = integrator(
|
94
|
+
time,
|
95
|
+
uv,
|
96
|
+
parameters,
|
97
|
+
equations_of_motion,
|
98
|
+
jacobian=jacobian,
|
99
|
+
time_step=time_step,
|
100
|
+
)
|
101
|
+
|
102
|
+
if accept:
|
103
|
+
time = time_new
|
104
|
+
uv = uv_new.copy()
|
105
|
+
# Reshape the deviation vectors into a neq x neq matrix
|
106
|
+
v = uv[neq:].reshape(neq, neq).copy()
|
107
|
+
|
108
|
+
# Perform the QR decomposition
|
109
|
+
v, R = QR(v)
|
110
|
+
|
111
|
+
# Accumulate the log
|
112
|
+
exponents += np.log(np.abs(np.diag(R))) / np.log(log_base)
|
113
|
+
|
114
|
+
if return_history:
|
115
|
+
result = [time]
|
116
|
+
for i in range(neq):
|
117
|
+
result.append(
|
118
|
+
exponents[i]
|
119
|
+
/ (time - (transient_time if transient_time is not None else 0))
|
120
|
+
)
|
121
|
+
history.append(result)
|
122
|
+
|
123
|
+
# Reshape v back to uv
|
124
|
+
uv[neq:] = v.reshape(neq**2)
|
125
|
+
|
126
|
+
time_step = time_step_new
|
127
|
+
|
128
|
+
if return_history:
|
129
|
+
return history
|
130
|
+
else:
|
131
|
+
result = []
|
132
|
+
for i in range(neq):
|
133
|
+
result.append(
|
134
|
+
exponents[i]
|
135
|
+
/ (time - (transient_time if transient_time is not None else 0))
|
136
|
+
)
|
137
|
+
return [result]
|
138
|
+
|
139
|
+
|
140
|
+
@njit(cache=True)
|
141
|
+
def SALI(
|
142
|
+
u: NDArray[np.float64],
|
143
|
+
parameters: NDArray[np.float64],
|
144
|
+
total_time: float,
|
145
|
+
equations_of_motion: Callable[
|
146
|
+
[np.float64, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
|
147
|
+
],
|
148
|
+
jacobian: Callable[
|
149
|
+
[np.float64, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
|
150
|
+
],
|
151
|
+
transient_time: Optional[float] = None,
|
152
|
+
time_step: float = 0.01,
|
153
|
+
atol: float = 1e-6,
|
154
|
+
rtol: float = 1e-3,
|
155
|
+
integrator=rk4_step_wrapped,
|
156
|
+
return_history: bool = False,
|
157
|
+
seed: int = 13,
|
158
|
+
threshold: float = 1e-16,
|
159
|
+
) -> NDArray[np.float64]:
|
160
|
+
|
161
|
+
neq = len(u) # Number of equations of the system
|
162
|
+
ndv = 2 # Number of deviation vectors
|
163
|
+
nt = neq + neq * ndv # Total number of equations including variational equations
|
164
|
+
|
165
|
+
u = u.copy()
|
166
|
+
|
167
|
+
# Handle transient time
|
168
|
+
if transient_time is not None:
|
169
|
+
u = evolve_system(
|
170
|
+
u,
|
171
|
+
parameters,
|
172
|
+
transient_time,
|
173
|
+
equations_of_motion,
|
174
|
+
time_step=time_step,
|
175
|
+
atol=atol,
|
176
|
+
rtol=rtol,
|
177
|
+
integrator=integrator,
|
178
|
+
)
|
179
|
+
time = transient_time
|
180
|
+
else:
|
181
|
+
time = 0
|
182
|
+
|
183
|
+
# State + deviation vectors
|
184
|
+
uv = np.zeros(nt)
|
185
|
+
uv[:neq] = u.copy()
|
186
|
+
|
187
|
+
# Randomly define the deviation vectors and orthonormalize them
|
188
|
+
np.random.seed(seed)
|
189
|
+
uv[neq:] = -1 + 2 * np.random.rand(nt - neq)
|
190
|
+
v = uv[neq:].reshape(neq, ndv)
|
191
|
+
v, _ = qr(v)
|
192
|
+
uv[neq:] = v.reshape(neq * ndv)
|
193
|
+
|
194
|
+
history = []
|
195
|
+
|
196
|
+
while time < total_time:
|
197
|
+
if time + time_step > total_time:
|
198
|
+
time_step = total_time - time
|
199
|
+
|
200
|
+
uv_new, time_new, time_step_new, accept = integrator(
|
201
|
+
time,
|
202
|
+
uv,
|
203
|
+
parameters,
|
204
|
+
equations_of_motion,
|
205
|
+
jacobian=jacobian,
|
206
|
+
time_step=time_step,
|
207
|
+
number_of_deviation_vectors=ndv,
|
208
|
+
)
|
209
|
+
|
210
|
+
if accept:
|
211
|
+
time = time_new
|
212
|
+
uv = uv_new.copy()
|
213
|
+
|
214
|
+
# Reshape the deviation vectors into a neq x ndv matrix
|
215
|
+
v = uv[neq:].reshape(neq, ndv)
|
216
|
+
|
217
|
+
# Normalize the deviation vectors
|
218
|
+
v[:, 0] /= np.linalg.norm(v[:, 0])
|
219
|
+
v[:, 1] /= np.linalg.norm(v[:, 1])
|
220
|
+
|
221
|
+
# Calculate the aligment indexes and SALI
|
222
|
+
PAI = np.linalg.norm(v[:, 0] + v[:, 1])
|
223
|
+
AAI = np.linalg.norm(v[:, 0] - v[:, 1])
|
224
|
+
sali = min(PAI, AAI)
|
225
|
+
|
226
|
+
if return_history:
|
227
|
+
result = [time, sali]
|
228
|
+
history.append(result)
|
229
|
+
|
230
|
+
# Early termination
|
231
|
+
if sali <= threshold:
|
232
|
+
break
|
233
|
+
|
234
|
+
# Reshape v back to uv
|
235
|
+
uv[neq:] = v.reshape(neq * ndv)
|
236
|
+
|
237
|
+
time_step = time_step_new
|
238
|
+
|
239
|
+
if return_history:
|
240
|
+
return history
|
241
|
+
else:
|
242
|
+
return [[time, sali]]
|
243
|
+
|
244
|
+
|
245
|
+
# @njit(cache=True)
|
246
|
+
def LDI(
|
247
|
+
u: NDArray[np.float64],
|
248
|
+
parameters: NDArray[np.float64],
|
249
|
+
total_time: float,
|
250
|
+
equations_of_motion: Callable[
|
251
|
+
[np.float64, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
|
252
|
+
],
|
253
|
+
jacobian: Callable[
|
254
|
+
[np.float64, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
|
255
|
+
],
|
256
|
+
number_deviation_vectors: int,
|
257
|
+
transient_time: Optional[float] = None,
|
258
|
+
time_step: float = 0.01,
|
259
|
+
atol: float = 1e-6,
|
260
|
+
rtol: float = 1e-3,
|
261
|
+
integrator=rk4_step_wrapped,
|
262
|
+
return_history: bool = False,
|
263
|
+
seed: int = 13,
|
264
|
+
threshold: float = 1e-16,
|
265
|
+
) -> NDArray[np.float64]:
|
266
|
+
|
267
|
+
neq = len(u) # Number of equations of the system
|
268
|
+
ndv = number_deviation_vectors # Number of deviation vectors
|
269
|
+
nt = neq + neq * ndv # Total number of equations including variational equations
|
270
|
+
|
271
|
+
u = u.copy()
|
272
|
+
|
273
|
+
# Handle transient time
|
274
|
+
if transient_time is not None:
|
275
|
+
u = evolve_system(
|
276
|
+
u,
|
277
|
+
parameters,
|
278
|
+
transient_time,
|
279
|
+
equations_of_motion,
|
280
|
+
time_step=time_step,
|
281
|
+
atol=atol,
|
282
|
+
rtol=rtol,
|
283
|
+
integrator=integrator,
|
284
|
+
)
|
285
|
+
time = transient_time
|
286
|
+
else:
|
287
|
+
time = 0
|
288
|
+
|
289
|
+
# State + deviation vectors
|
290
|
+
uv = np.zeros(nt)
|
291
|
+
uv[:neq] = u.copy()
|
292
|
+
|
293
|
+
# Randomly define the deviation vectors and orthonormalize them
|
294
|
+
np.random.seed(seed)
|
295
|
+
uv[neq:] = -1 + 2 * np.random.rand(nt - neq)
|
296
|
+
v = uv[neq:].reshape(neq, ndv)
|
297
|
+
v, _ = qr(v)
|
298
|
+
uv[neq:] = v.reshape(neq * ndv)
|
299
|
+
|
300
|
+
history = []
|
301
|
+
|
302
|
+
while time < total_time:
|
303
|
+
if time + time_step > total_time:
|
304
|
+
time_step = total_time - time
|
305
|
+
|
306
|
+
uv_new, time_new, time_step_new, accept = integrator(
|
307
|
+
time,
|
308
|
+
uv,
|
309
|
+
parameters,
|
310
|
+
equations_of_motion,
|
311
|
+
jacobian=jacobian,
|
312
|
+
time_step=time_step,
|
313
|
+
number_of_deviation_vectors=ndv,
|
314
|
+
)
|
315
|
+
|
316
|
+
if accept:
|
317
|
+
time = time_new
|
318
|
+
uv = uv_new.copy()
|
319
|
+
|
320
|
+
# Reshape the deviation vectors into a neq x ndv matrix
|
321
|
+
v = uv[neq:].reshape(neq, ndv)
|
322
|
+
|
323
|
+
# Normalize the deviation vectors
|
324
|
+
for i in range(ndv):
|
325
|
+
v[:, i] /= np.linalg.norm(v[:, i])
|
326
|
+
|
327
|
+
# Calculate the singular values
|
328
|
+
S = np.linalg.svd(v, full_matrices=False, compute_uv=False)
|
329
|
+
ldi = np.prod(S)
|
330
|
+
|
331
|
+
if return_history:
|
332
|
+
result = [time, ldi]
|
333
|
+
history.append(result)
|
334
|
+
|
335
|
+
# Early termination
|
336
|
+
if ldi <= threshold:
|
337
|
+
break
|
338
|
+
|
339
|
+
# Reshape v back to uv
|
340
|
+
uv[neq:] = v.reshape(neq * ndv)
|
341
|
+
|
342
|
+
time_step = time_step_new
|
343
|
+
|
344
|
+
if return_history:
|
345
|
+
return history
|
346
|
+
else:
|
347
|
+
return [[time, ldi]]
|
@@ -0,0 +1,240 @@
|
|
1
|
+
# models.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 Callable, Optional
|
19
|
+
|
20
|
+
import numpy as np
|
21
|
+
from numba import njit
|
22
|
+
from numpy.typing import NDArray
|
23
|
+
|
24
|
+
|
25
|
+
@njit
|
26
|
+
def lorenz_system(
|
27
|
+
time: float, u: NDArray[np.float64], parameters: NDArray[np.float64]
|
28
|
+
) -> NDArray[np.float64]:
|
29
|
+
sigma, rho, beta = parameters
|
30
|
+
dudt = np.zeros_like(u)
|
31
|
+
dudt[0] = sigma * (u[1] - u[0])
|
32
|
+
dudt[1] = u[0] * (rho - u[2]) - u[1]
|
33
|
+
dudt[2] = u[0] * u[1] - beta * u[2]
|
34
|
+
|
35
|
+
return dudt
|
36
|
+
|
37
|
+
|
38
|
+
@njit
|
39
|
+
def lorenz_jacobian(
|
40
|
+
time: float, u: NDArray[np.float64], parameters: NDArray[np.float64]
|
41
|
+
) -> NDArray[np.float64]:
|
42
|
+
|
43
|
+
sigma, rho, beta = parameters
|
44
|
+
|
45
|
+
J = np.empty((3, 3), dtype=np.float64)
|
46
|
+
J[0, 0] = -sigma
|
47
|
+
J[0, 1] = sigma
|
48
|
+
J[0, 2] = 0.0
|
49
|
+
|
50
|
+
J[1, 0] = rho - u[2]
|
51
|
+
J[1, 1] = -1.0
|
52
|
+
J[1, 2] = -u[0]
|
53
|
+
|
54
|
+
J[2, 0] = u[1]
|
55
|
+
J[2, 1] = u[0]
|
56
|
+
J[2, 2] = -beta
|
57
|
+
|
58
|
+
return J
|
59
|
+
|
60
|
+
|
61
|
+
@njit
|
62
|
+
def henon_heiles(
|
63
|
+
time: float, u: NDArray[np.float64], parameters: NDArray[np.float64]
|
64
|
+
) -> NDArray[np.float64]:
|
65
|
+
|
66
|
+
dudt = np.zeros_like(u)
|
67
|
+
|
68
|
+
dudt[0] = u[2] # dx / dt = px
|
69
|
+
dudt[1] = u[3] # dy / dt = py
|
70
|
+
dudt[2] = -u[0] - 2 * u[0] * u[1] # d(px) / dt = - x - 2xy
|
71
|
+
dudt[3] = -u[1] - u[0] ** 2 + u[1] ** 2 # d(py) / dt = - y - x^2 + y^2
|
72
|
+
|
73
|
+
return dudt
|
74
|
+
|
75
|
+
|
76
|
+
@njit
|
77
|
+
def henon_heiles_jacobian(
|
78
|
+
time: float, u: NDArray[np.float64], parameters: NDArray[np.float64]
|
79
|
+
) -> NDArray[np.float64]:
|
80
|
+
|
81
|
+
neq = len(u)
|
82
|
+
J = np.zeros((neq, neq), dtype=np.float64)
|
83
|
+
|
84
|
+
J[0, 0] = 0
|
85
|
+
J[0, 1] = 0
|
86
|
+
J[0, 2] = 1
|
87
|
+
J[0, 3] = 0
|
88
|
+
|
89
|
+
J[1, 0] = 0
|
90
|
+
J[1, 1] = 0
|
91
|
+
J[1, 2] = 0
|
92
|
+
J[1, 3] = 1
|
93
|
+
|
94
|
+
J[2, 0] = -1 - 2 * u[1]
|
95
|
+
J[2, 1] = -2 * u[0]
|
96
|
+
J[2, 2] = 0
|
97
|
+
J[2, 3] = 0
|
98
|
+
|
99
|
+
J[3, 0] = -2 * u[0]
|
100
|
+
J[3, 1] = -1 + 2 * u[1]
|
101
|
+
J[3, 2] = 0
|
102
|
+
J[3, 3] = 0
|
103
|
+
|
104
|
+
return J
|
105
|
+
|
106
|
+
|
107
|
+
@njit
|
108
|
+
def rossler_system(
|
109
|
+
time: float, u: NDArray[np.float64], parameters: NDArray[np.float64]
|
110
|
+
) -> NDArray[np.float64]:
|
111
|
+
|
112
|
+
a, b, c = parameters
|
113
|
+
|
114
|
+
dudt = np.zeros_like(u)
|
115
|
+
|
116
|
+
dudt[0] = -u[1] - u[2]
|
117
|
+
dudt[1] = u[0] + a * u[1]
|
118
|
+
dudt[2] = b + u[2] * (u[0] - c)
|
119
|
+
|
120
|
+
return dudt
|
121
|
+
|
122
|
+
|
123
|
+
@njit
|
124
|
+
def rossler_system_jacobian(
|
125
|
+
time: float, u: NDArray[np.float64], parameters: NDArray[np.float64]
|
126
|
+
) -> NDArray[np.float64]:
|
127
|
+
|
128
|
+
a, b, c = parameters
|
129
|
+
|
130
|
+
neq = len(u)
|
131
|
+
J = np.zeros((neq, neq), dtype=np.float64)
|
132
|
+
|
133
|
+
J[0, 0] = 0
|
134
|
+
J[0, 1] = -1
|
135
|
+
J[0, 2] = -1
|
136
|
+
|
137
|
+
J[1, 0] = 1
|
138
|
+
J[1, 1] = a
|
139
|
+
J[1, 2] = 0
|
140
|
+
|
141
|
+
J[2, 0] = u[2]
|
142
|
+
J[2, 1] = 0
|
143
|
+
J[2, 2] = u[0] - c
|
144
|
+
|
145
|
+
return J
|
146
|
+
|
147
|
+
|
148
|
+
@njit
|
149
|
+
def rossler_system_4D(
|
150
|
+
time: float, u: NDArray[np.float64], parameters: NDArray[np.float64]
|
151
|
+
) -> NDArray[np.float64]:
|
152
|
+
|
153
|
+
a, b, c, d = parameters
|
154
|
+
x, y, z, w = u
|
155
|
+
dudt = np.zeros_like(u)
|
156
|
+
|
157
|
+
dudt[0] = -(y + z)
|
158
|
+
dudt[1] = x + a * y + w
|
159
|
+
dudt[2] = b + x * z
|
160
|
+
dudt[3] = -c * z + d * w
|
161
|
+
|
162
|
+
return dudt
|
163
|
+
|
164
|
+
|
165
|
+
@njit
|
166
|
+
def rossler_system_4D_jacobian(
|
167
|
+
time: float, u: NDArray[np.float64], parameters: NDArray[np.float64]
|
168
|
+
) -> NDArray[np.float64]:
|
169
|
+
|
170
|
+
a, b, c, d = parameters
|
171
|
+
x, y, z, w = u
|
172
|
+
|
173
|
+
neq = len(u)
|
174
|
+
J = np.zeros((neq, neq), dtype=np.float64)
|
175
|
+
|
176
|
+
J[0, 0] = 0
|
177
|
+
J[0, 1] = -1
|
178
|
+
J[0, 2] = -1
|
179
|
+
J[0, 3] = 0
|
180
|
+
|
181
|
+
J[1, 0] = 1
|
182
|
+
J[1, 1] = a
|
183
|
+
J[1, 2] = 0
|
184
|
+
J[1, 3] = 1
|
185
|
+
|
186
|
+
J[2, 0] = z
|
187
|
+
J[2, 1] = 0
|
188
|
+
J[2, 2] = x
|
189
|
+
J[2, 3] = 0
|
190
|
+
|
191
|
+
J[3, 0] = 0
|
192
|
+
J[3, 1] = 0
|
193
|
+
J[3, 2] = -c
|
194
|
+
J[3, 3] = d
|
195
|
+
|
196
|
+
return J
|
197
|
+
|
198
|
+
|
199
|
+
@njit
|
200
|
+
def variational_equations(
|
201
|
+
time: float,
|
202
|
+
state: NDArray[np.float64],
|
203
|
+
parameters: NDArray[np.float64],
|
204
|
+
equations_of_motion: Callable[
|
205
|
+
[np.float64, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
|
206
|
+
],
|
207
|
+
jacobian: Callable[
|
208
|
+
[np.float64, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
|
209
|
+
],
|
210
|
+
number_of_deviation_vectors: Optional[int] = None,
|
211
|
+
) -> NDArray[np.float64]:
|
212
|
+
|
213
|
+
state = state.copy()
|
214
|
+
nt = len(state) # Total number of equations
|
215
|
+
|
216
|
+
if number_of_deviation_vectors is not None:
|
217
|
+
ndv = number_of_deviation_vectors
|
218
|
+
neq = round(nt / (1 + ndv)) # Number of system's equation
|
219
|
+
else:
|
220
|
+
neq = round((-1 + np.sqrt(1 + 4 * nt)) / 2)
|
221
|
+
ndv = neq
|
222
|
+
|
223
|
+
# Split the state into state variables, u, and deviation matrix, v
|
224
|
+
u = state[:neq].copy() # State vector
|
225
|
+
v = state[neq:].reshape(neq, ndv).copy() # Deviation matrix
|
226
|
+
# Compute the Jacobian matrix
|
227
|
+
J = jacobian(time, u, parameters)
|
228
|
+
|
229
|
+
# Compute system's dynamics
|
230
|
+
dudt = equations_of_motion(time, u, parameters)
|
231
|
+
|
232
|
+
# Variational equation: dvdt = J * v
|
233
|
+
dvdt = J @ v
|
234
|
+
|
235
|
+
# Combine into a single output vector of length nt = neq + neq * ndv
|
236
|
+
dstatedt = np.zeros(nt, dtype=np.float64)
|
237
|
+
dstatedt[:neq] = dudt
|
238
|
+
dstatedt[neq:] = dvdt.reshape(neq * ndv)
|
239
|
+
|
240
|
+
return dstatedt
|