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 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__ = ["DiscreteDynamicalSystem", "PlotStyler", "TimeSeriesMetrics", "BasinMetrics"]
25
+ __all__ = [
26
+ "DiscreteDynamicalSystem",
27
+ "ContinuousDynamicalSystem",
28
+ "PlotStyler",
29
+ "TimeSeriesMetrics",
30
+ "BasinMetrics",
31
+ ]
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '1.0.1'
21
- __version_tuple__ = version_tuple = (1, 0, 1)
20
+ __version__ = version = '1.2.1'
21
+ __version_tuple__ = version_tuple = (1, 2, 1)
@@ -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