pynamicalsys 1.3.1__py3-none-any.whl → 1.4.0__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 +2 -0
- pynamicalsys/__version__.py +2 -2
- pynamicalsys/common/time_series_metrics.py +85 -0
- pynamicalsys/continuous_time/chaotic_indicators.py +305 -7
- pynamicalsys/continuous_time/models.py +25 -0
- pynamicalsys/continuous_time/trajectory_analysis.py +457 -10
- pynamicalsys/core/continuous_dynamical_systems.py +933 -35
- pynamicalsys/core/discrete_dynamical_systems.py +20 -9
- pynamicalsys/core/hamiltonian_systems.py +1193 -0
- pynamicalsys/core/time_series_metrics.py +65 -0
- pynamicalsys/discrete_time/dynamical_indicators.py +5 -94
- pynamicalsys/hamiltonian_systems/__init__.py +16 -0
- pynamicalsys/hamiltonian_systems/chaotic_indicators.py +638 -0
- pynamicalsys/hamiltonian_systems/models.py +68 -0
- pynamicalsys/hamiltonian_systems/numerical_integrators.py +248 -0
- pynamicalsys/hamiltonian_systems/trajectory_analysis.py +293 -0
- pynamicalsys/hamiltonian_systems/validators.py +114 -0
- {pynamicalsys-1.3.1.dist-info → pynamicalsys-1.4.0.dist-info}/METADATA +37 -8
- pynamicalsys-1.4.0.dist-info/RECORD +36 -0
- pynamicalsys-1.3.1.dist-info/RECORD +0 -28
- {pynamicalsys-1.3.1.dist-info → pynamicalsys-1.4.0.dist-info}/WHEEL +0 -0
- {pynamicalsys-1.3.1.dist-info → pynamicalsys-1.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,638 @@
|
|
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 Callable
|
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, wedge_norm, fit_poly
|
24
|
+
|
25
|
+
from pynamicalsys.common.recurrence_quantification_analysis import (
|
26
|
+
RTEConfig,
|
27
|
+
recurrence_matrix,
|
28
|
+
white_vertline_distr,
|
29
|
+
)
|
30
|
+
|
31
|
+
from pynamicalsys.hamiltonian_systems.trajectory_analysis import (
|
32
|
+
generate_poincare_section,
|
33
|
+
)
|
34
|
+
|
35
|
+
from pynamicalsys.common.time_series_metrics import hurst_exponent
|
36
|
+
|
37
|
+
|
38
|
+
@njit
|
39
|
+
def lyapunov_spectrum(
|
40
|
+
q: NDArray[np.float64],
|
41
|
+
p: NDArray[np.float64],
|
42
|
+
total_time: float,
|
43
|
+
time_step: float,
|
44
|
+
parameters: NDArray[np.float64],
|
45
|
+
grad_T: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
46
|
+
grad_V: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
47
|
+
hess_T: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
48
|
+
hess_V: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
49
|
+
num_exponents: int,
|
50
|
+
qr_interval: int,
|
51
|
+
return_history: bool,
|
52
|
+
seed: int,
|
53
|
+
log_base: float,
|
54
|
+
QR: Callable[
|
55
|
+
[NDArray[np.float64]], tuple[NDArray[np.float64], NDArray[np.float64]]
|
56
|
+
],
|
57
|
+
integrator: Callable,
|
58
|
+
tangent_integrator: Callable,
|
59
|
+
) -> NDArray[np.float64]:
|
60
|
+
"""
|
61
|
+
Compute the full Lyapunov spectrum of a Hamiltonian system.
|
62
|
+
|
63
|
+
Parameters
|
64
|
+
----------
|
65
|
+
q : NDArray[np.float64], shape (dof,)
|
66
|
+
Initial generalized coordinates.
|
67
|
+
p : NDArray[np.float64], shape (dof,)
|
68
|
+
Initial generalized momenta.
|
69
|
+
total_time : float
|
70
|
+
Total integration time.
|
71
|
+
time_step : float
|
72
|
+
Integration step size.
|
73
|
+
parameters : NDArray[np.float64]
|
74
|
+
Additional system parameters.
|
75
|
+
grad_T : Callable
|
76
|
+
Gradient of kinetic energy with respect to momenta.
|
77
|
+
grad_V : Callable
|
78
|
+
Gradient of potential energy with respect to coordinates.
|
79
|
+
hess_T : Callable
|
80
|
+
Hessian of kinetic energy with respect to momenta.
|
81
|
+
hess_V : Callable
|
82
|
+
Hessian of potential energy with respect to coordinates.
|
83
|
+
num_exponents : int
|
84
|
+
Number of Lyapunov exponents to compute.
|
85
|
+
qr_interval : int
|
86
|
+
Interval (in steps) between QR re-orthonormalizations.
|
87
|
+
return_history : bool
|
88
|
+
If True, return time evolution of exponents; if False, return only final values.
|
89
|
+
seed : int
|
90
|
+
Random seed for deviation vector initialization.
|
91
|
+
log_base : float
|
92
|
+
Base of the logarithm used for normalization.
|
93
|
+
QR : Callable
|
94
|
+
Function for orthonormalization (returns Q, R).
|
95
|
+
integrator : Callable
|
96
|
+
Symplectic integrator for the main trajectory.
|
97
|
+
tangent_integrator : Callable
|
98
|
+
Tangent map integrator for deviation vectors.
|
99
|
+
|
100
|
+
Returns
|
101
|
+
-------
|
102
|
+
spectrum : NDArray[np.float64], shape (num_steps/qr_interval, num_exponents+1) or (1, num_exponents)
|
103
|
+
- If `return_history=True`: time and instantaneous Lyapunov exponents.
|
104
|
+
- If `return_history=False`: final averaged Lyapunov spectrum.
|
105
|
+
"""
|
106
|
+
num_steps = round(total_time / time_step)
|
107
|
+
dof = len(q)
|
108
|
+
neq = 2 * dof
|
109
|
+
|
110
|
+
np.random.seed(seed)
|
111
|
+
dv = -1 + 2 * np.random.rand(neq, num_exponents)
|
112
|
+
dv, _ = QR(dv)
|
113
|
+
|
114
|
+
exponents = np.zeros(num_exponents, dtype=np.float64)
|
115
|
+
history = np.zeros((round(num_steps / qr_interval), num_exponents + 1))
|
116
|
+
count = 0
|
117
|
+
for i in range(num_steps):
|
118
|
+
time = (i + 1) * time_step
|
119
|
+
# Evolve trajectory
|
120
|
+
q, p = integrator(q, p, time_step, grad_T, grad_V, parameters)
|
121
|
+
|
122
|
+
# Evolve deviation vectors
|
123
|
+
for j in range(num_exponents):
|
124
|
+
dq = dv[:dof, j].copy()
|
125
|
+
dp = dv[dof:, j].copy()
|
126
|
+
dq, dp = tangent_integrator(
|
127
|
+
q, p, dq, dp, time_step, hess_T, hess_V, parameters
|
128
|
+
)
|
129
|
+
dv[:dof, j] = dq.copy()
|
130
|
+
dv[dof:, j] = dp.copy()
|
131
|
+
|
132
|
+
if i % qr_interval == 0:
|
133
|
+
count += 1
|
134
|
+
# Orthonormalize the deviation vectors
|
135
|
+
dv, R = QR(dv)
|
136
|
+
|
137
|
+
# Acculate the log
|
138
|
+
exponents += np.log(np.abs(np.diag(R)))
|
139
|
+
|
140
|
+
if return_history:
|
141
|
+
result = np.zeros(num_exponents + 1)
|
142
|
+
result[0] = time
|
143
|
+
for j in range(num_exponents):
|
144
|
+
result[j + 1] = exponents[j] / time
|
145
|
+
history[count - 1, :] = result
|
146
|
+
|
147
|
+
if return_history:
|
148
|
+
history = history / np.log(log_base)
|
149
|
+
return history
|
150
|
+
else:
|
151
|
+
spectrum = np.zeros((1, num_exponents))
|
152
|
+
spectrum[0, :] = exponents / (total_time * np.log(log_base))
|
153
|
+
return spectrum
|
154
|
+
|
155
|
+
|
156
|
+
@njit
|
157
|
+
def maximum_lyapunov_exponent(
|
158
|
+
q: NDArray[np.float64],
|
159
|
+
p: NDArray[np.float64],
|
160
|
+
total_time: float,
|
161
|
+
time_step: float,
|
162
|
+
parameters: NDArray[np.float64],
|
163
|
+
grad_T: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
164
|
+
grad_V: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
165
|
+
hess_T: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
166
|
+
hess_V: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
167
|
+
return_history: bool,
|
168
|
+
seed: int,
|
169
|
+
log_base: float,
|
170
|
+
integrator: Callable,
|
171
|
+
tangent_integrator: Callable,
|
172
|
+
) -> NDArray[np.float64]:
|
173
|
+
"""
|
174
|
+
Compute the maximum Lyapunov exponent (MLE).
|
175
|
+
|
176
|
+
Parameters
|
177
|
+
----------
|
178
|
+
q : NDArray[np.float64], shape (dof,)
|
179
|
+
Initial coordinates.
|
180
|
+
p : NDArray[np.float64], shape (dof,)
|
181
|
+
Initial momenta.
|
182
|
+
total_time : float
|
183
|
+
Total integration time.
|
184
|
+
time_step : float
|
185
|
+
Integration step size.
|
186
|
+
parameters : NDArray[np.float64]
|
187
|
+
System parameters.
|
188
|
+
grad_T, grad_V, hess_T, hess_V : Callable
|
189
|
+
Gradient and Hessian functions of kinetic/potential energies.
|
190
|
+
return_history : bool
|
191
|
+
If True, return time series of MLE estimates.
|
192
|
+
seed : int
|
193
|
+
Random seed for initial deviation vector.
|
194
|
+
log_base : float
|
195
|
+
Base of logarithm.
|
196
|
+
integrator : Callable
|
197
|
+
Symplectic trajectory integrator.
|
198
|
+
tangent_integrator : Callable
|
199
|
+
Tangent map integrator.
|
200
|
+
|
201
|
+
Returns
|
202
|
+
-------
|
203
|
+
mle : NDArray[np.float64], shape (num_steps, 2) or (1, 1)
|
204
|
+
- If `return_history=True`: time vs. running MLE.
|
205
|
+
- If `return_history=False`: final MLE value.
|
206
|
+
"""
|
207
|
+
num_steps = round(total_time / time_step)
|
208
|
+
dof = len(q)
|
209
|
+
|
210
|
+
np.random.seed(seed)
|
211
|
+
dq = np.random.uniform(-1, 1, dof)
|
212
|
+
dp = np.random.uniform(-1, 1, dof)
|
213
|
+
norm = np.sqrt((dq**2).sum() + (dp**2).sum())
|
214
|
+
dq /= norm
|
215
|
+
dp /= norm
|
216
|
+
|
217
|
+
lyapunov_exponent = 0
|
218
|
+
history = np.zeros((num_steps, 2))
|
219
|
+
for i in range(num_steps):
|
220
|
+
time = (i + 1) * time_step
|
221
|
+
# Evolve trajectory
|
222
|
+
q, p = integrator(q, p, time_step, grad_T, grad_V, parameters)
|
223
|
+
|
224
|
+
# Evolve deviation vector
|
225
|
+
dq, dp = tangent_integrator(q, p, dq, dp, time_step, hess_T, hess_V, parameters)
|
226
|
+
|
227
|
+
# Norm of the deviation vector
|
228
|
+
norm = np.sqrt((dq**2).sum() + (dp**2).sum())
|
229
|
+
|
230
|
+
# Acculate the log
|
231
|
+
lyapunov_exponent += np.log(norm)
|
232
|
+
|
233
|
+
# Renormalize the deviation vector
|
234
|
+
dq /= norm
|
235
|
+
dp /= norm
|
236
|
+
|
237
|
+
if return_history:
|
238
|
+
history[i, 0] = time
|
239
|
+
history[i, 1] = lyapunov_exponent / time
|
240
|
+
|
241
|
+
if return_history:
|
242
|
+
history = history / np.log(log_base)
|
243
|
+
return history
|
244
|
+
else:
|
245
|
+
result = np.zeros((1, 1))
|
246
|
+
result[0, 0] = lyapunov_exponent / time
|
247
|
+
return result
|
248
|
+
|
249
|
+
|
250
|
+
@njit
|
251
|
+
def SALI(
|
252
|
+
q: NDArray[np.float64],
|
253
|
+
p: NDArray[np.float64],
|
254
|
+
total_time: float,
|
255
|
+
time_step: float,
|
256
|
+
parameters: NDArray[np.float64],
|
257
|
+
grad_T: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
258
|
+
grad_V: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
259
|
+
hess_T: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
260
|
+
hess_V: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
261
|
+
return_history: bool,
|
262
|
+
seed: int,
|
263
|
+
integrator: Callable,
|
264
|
+
tangent_integrator: Callable,
|
265
|
+
threshold: float,
|
266
|
+
) -> list[list[float]]:
|
267
|
+
"""
|
268
|
+
Compute the Smaller Alignment Index (SALI).
|
269
|
+
|
270
|
+
Parameters
|
271
|
+
----------
|
272
|
+
q, p : NDArray[np.float64], shape (dof,)
|
273
|
+
Initial conditions.
|
274
|
+
total_time : float
|
275
|
+
Total integration time.
|
276
|
+
time_step : float
|
277
|
+
Integration step size.
|
278
|
+
parameters : NDArray[np.float64]
|
279
|
+
System parameters.
|
280
|
+
grad_T, grad_V, hess_T, hess_V : Callable
|
281
|
+
Gradient and Hessian functions of the Hamiltonian.
|
282
|
+
return_history : bool
|
283
|
+
If True, return time evolution of SALI.
|
284
|
+
seed : int
|
285
|
+
Random seed for deviation vectors.
|
286
|
+
integrator : Callable
|
287
|
+
Symplectic trajectory integrator.
|
288
|
+
tangent_integrator : Callable
|
289
|
+
Tangent map integrator.
|
290
|
+
threshold : float
|
291
|
+
Early termination threshold for SALI.
|
292
|
+
|
293
|
+
Returns
|
294
|
+
-------
|
295
|
+
sali : list of [time, value]
|
296
|
+
Time evolution of SALI (or final value if `return_history=False`).
|
297
|
+
"""
|
298
|
+
num_steps = round(total_time / time_step)
|
299
|
+
dof = len(q)
|
300
|
+
neq = 2 * dof
|
301
|
+
|
302
|
+
np.random.seed(seed)
|
303
|
+
dv = -1 + 2 * np.random.rand(neq, 2)
|
304
|
+
dv, _ = qr(dv)
|
305
|
+
|
306
|
+
history = []
|
307
|
+
for i in range(num_steps):
|
308
|
+
time = (i + 1) * time_step
|
309
|
+
# Evolve trajectory
|
310
|
+
q, p = integrator(q, p, time_step, grad_T, grad_V, parameters)
|
311
|
+
|
312
|
+
# Evolve deviation vectors
|
313
|
+
for j in range(2):
|
314
|
+
dq = dv[:dof, j].copy()
|
315
|
+
dp = dv[dof:, j].copy()
|
316
|
+
dq, dp = tangent_integrator(
|
317
|
+
q, p, dq, dp, time_step, hess_T, hess_V, parameters
|
318
|
+
)
|
319
|
+
norm = np.sqrt((dq**2).sum() + (dp**2).sum())
|
320
|
+
dv[:dof, j] = dq.copy() / norm
|
321
|
+
dv[dof:, j] = dp.copy() / norm
|
322
|
+
|
323
|
+
pai = np.linalg.norm(dv[:, 0] + dv[:, 1])
|
324
|
+
aai = np.linalg.norm(dv[:, 0] - dv[:, 1])
|
325
|
+
|
326
|
+
sali = min(pai, aai)
|
327
|
+
|
328
|
+
if return_history:
|
329
|
+
result = [time, sali]
|
330
|
+
history.append(result)
|
331
|
+
|
332
|
+
if sali <= threshold:
|
333
|
+
break
|
334
|
+
|
335
|
+
if return_history:
|
336
|
+
return history
|
337
|
+
else:
|
338
|
+
return [[time, sali]]
|
339
|
+
|
340
|
+
|
341
|
+
def LDI(
|
342
|
+
q: NDArray[np.float64],
|
343
|
+
p: NDArray[np.float64],
|
344
|
+
total_time: float,
|
345
|
+
time_step: float,
|
346
|
+
parameters: NDArray[np.float64],
|
347
|
+
grad_T: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
348
|
+
grad_V: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
349
|
+
hess_T: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
350
|
+
hess_V: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
351
|
+
k: int,
|
352
|
+
return_history: bool,
|
353
|
+
seed: int,
|
354
|
+
integrator: Callable,
|
355
|
+
tangent_integrator: Callable,
|
356
|
+
threshold: float,
|
357
|
+
) -> list[list[float]]:
|
358
|
+
"""
|
359
|
+
Compute the Linear Dependence Index (LDI).
|
360
|
+
|
361
|
+
Parameters
|
362
|
+
----------
|
363
|
+
q, p : NDArray[np.float64], shape (dof,)
|
364
|
+
Initial conditions.
|
365
|
+
total_time : float
|
366
|
+
Total integration time.
|
367
|
+
time_step : float
|
368
|
+
Integration step size.
|
369
|
+
parameters : NDArray[np.float64]
|
370
|
+
System parameters.
|
371
|
+
grad_T, grad_V, hess_T, hess_V : Callable
|
372
|
+
Gradient and Hessian functions.
|
373
|
+
k : int
|
374
|
+
Number of deviation vectors.
|
375
|
+
return_history : bool
|
376
|
+
If True, return LDI time series.
|
377
|
+
seed : int
|
378
|
+
Random seed for initialization.
|
379
|
+
integrator : Callable
|
380
|
+
Symplectic trajectory integrator.
|
381
|
+
tangent_integrator : Callable
|
382
|
+
Tangent map integrator.
|
383
|
+
threshold : float
|
384
|
+
Early termination threshold.
|
385
|
+
|
386
|
+
Returns
|
387
|
+
-------
|
388
|
+
ldi : list of [time, value]
|
389
|
+
LDI evolution (or final value).
|
390
|
+
"""
|
391
|
+
num_steps = round(total_time / time_step)
|
392
|
+
dof = len(q)
|
393
|
+
neq = 2 * dof
|
394
|
+
|
395
|
+
np.random.seed(seed)
|
396
|
+
dv = -1 + 2 * np.random.rand(neq, k)
|
397
|
+
dv, _ = qr(dv)
|
398
|
+
|
399
|
+
history = []
|
400
|
+
for i in range(num_steps):
|
401
|
+
time = (i + 1) * time_step
|
402
|
+
# Evolve trajectory
|
403
|
+
q, p = integrator(q, p, time_step, grad_T, grad_V, parameters)
|
404
|
+
|
405
|
+
# Evolve deviation vectors
|
406
|
+
for j in range(k):
|
407
|
+
dq = dv[:dof, j].copy()
|
408
|
+
dp = dv[dof:, j].copy()
|
409
|
+
dq, dp = tangent_integrator(
|
410
|
+
q, p, dq, dp, time_step, hess_T, hess_V, parameters
|
411
|
+
)
|
412
|
+
norm = np.sqrt((dq**2).sum() + (dp**2).sum())
|
413
|
+
dv[:dof, j] = dq.copy() / norm
|
414
|
+
dv[dof:, j] = dp.copy() / norm
|
415
|
+
|
416
|
+
# Calculate the singular values
|
417
|
+
S = np.linalg.svd(dv, full_matrices=False, compute_uv=False)
|
418
|
+
ldi = np.exp(np.sum(np.log(S))) # LDI is the product of all singular values
|
419
|
+
|
420
|
+
if return_history:
|
421
|
+
result = [time, ldi]
|
422
|
+
history.append(result)
|
423
|
+
|
424
|
+
# Early termination
|
425
|
+
if ldi <= threshold:
|
426
|
+
break
|
427
|
+
|
428
|
+
if return_history:
|
429
|
+
return history
|
430
|
+
else:
|
431
|
+
return [[time, ldi]]
|
432
|
+
|
433
|
+
|
434
|
+
def GALI(
|
435
|
+
q: NDArray[np.float64],
|
436
|
+
p: NDArray[np.float64],
|
437
|
+
total_time: float,
|
438
|
+
time_step: float,
|
439
|
+
parameters: NDArray[np.float64],
|
440
|
+
grad_T: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
441
|
+
grad_V: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
442
|
+
hess_T: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
443
|
+
hess_V: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
444
|
+
k: int,
|
445
|
+
return_history: bool,
|
446
|
+
seed: int,
|
447
|
+
integrator: Callable,
|
448
|
+
tangent_integrator: Callable,
|
449
|
+
threshold: float,
|
450
|
+
) -> list[list[float]]:
|
451
|
+
"""
|
452
|
+
Compute the Generalized Alignment Index (GALI).
|
453
|
+
|
454
|
+
Parameters
|
455
|
+
----------
|
456
|
+
q, p : NDArray[np.float64], shape (dof,)
|
457
|
+
Initial conditions.
|
458
|
+
total_time : float
|
459
|
+
Total integration time.
|
460
|
+
time_step : float
|
461
|
+
Integration step size.
|
462
|
+
parameters : NDArray[np.float64]
|
463
|
+
System parameters.
|
464
|
+
grad_T, grad_V, hess_T, hess_V : Callable
|
465
|
+
Gradient and Hessian functions.
|
466
|
+
k : int
|
467
|
+
Number of deviation vectors.
|
468
|
+
return_history : bool
|
469
|
+
If True, return GALI time series.
|
470
|
+
seed : int
|
471
|
+
Random seed for initialization.
|
472
|
+
integrator : Callable
|
473
|
+
Symplectic trajectory integrator.
|
474
|
+
tangent_integrator : Callable
|
475
|
+
Tangent map integrator.
|
476
|
+
threshold : float
|
477
|
+
Early termination threshold.
|
478
|
+
|
479
|
+
Returns
|
480
|
+
-------
|
481
|
+
gali : list of [time, value]
|
482
|
+
GALI evolution (or final value).
|
483
|
+
"""
|
484
|
+
num_steps = round(total_time / time_step)
|
485
|
+
dof = len(q)
|
486
|
+
neq = 2 * dof
|
487
|
+
|
488
|
+
np.random.seed(seed)
|
489
|
+
dv = -1 + 2 * np.random.rand(neq, k)
|
490
|
+
dv, _ = qr(dv)
|
491
|
+
|
492
|
+
history = []
|
493
|
+
for i in range(num_steps):
|
494
|
+
time = (i + 1) * time_step
|
495
|
+
# Evolve trajectory
|
496
|
+
q, p = integrator(q, p, time_step, grad_T, grad_V, parameters)
|
497
|
+
|
498
|
+
# Evolve deviation vectors
|
499
|
+
for j in range(k):
|
500
|
+
dq = dv[:dof, j].copy()
|
501
|
+
dp = dv[dof:, j].copy()
|
502
|
+
dq, dp = tangent_integrator(
|
503
|
+
q, p, dq, dp, time_step, hess_T, hess_V, parameters
|
504
|
+
)
|
505
|
+
norm = np.sqrt((dq**2).sum() + (dp**2).sum())
|
506
|
+
dv[:dof, j] = dq.copy() / norm
|
507
|
+
dv[dof:, j] = dp.copy() / norm
|
508
|
+
|
509
|
+
# Calculate GALI
|
510
|
+
gali = wedge_norm(dv)
|
511
|
+
|
512
|
+
if return_history:
|
513
|
+
result = [time, gali]
|
514
|
+
history.append(result)
|
515
|
+
|
516
|
+
# Early termination
|
517
|
+
if gali <= threshold:
|
518
|
+
break
|
519
|
+
|
520
|
+
if return_history:
|
521
|
+
return history
|
522
|
+
else:
|
523
|
+
return [[time, gali]]
|
524
|
+
|
525
|
+
|
526
|
+
def recurrence_time_entropy(
|
527
|
+
q,
|
528
|
+
p,
|
529
|
+
num_points,
|
530
|
+
parameters,
|
531
|
+
grad_T,
|
532
|
+
grad_V,
|
533
|
+
time_step,
|
534
|
+
integrator,
|
535
|
+
section_index,
|
536
|
+
section_value,
|
537
|
+
crossing,
|
538
|
+
**kwargs,
|
539
|
+
):
|
540
|
+
|
541
|
+
# Configuration handling
|
542
|
+
config = RTEConfig(**kwargs)
|
543
|
+
|
544
|
+
# Metric setup
|
545
|
+
metric_map = {"supremum": np.inf, "euclidean": 2, "manhattan": 1}
|
546
|
+
|
547
|
+
try:
|
548
|
+
ord = metric_map[config.std_metric.lower()]
|
549
|
+
except KeyError:
|
550
|
+
raise ValueError(
|
551
|
+
f"Invalid std_metric: {config.std_metric}. Must be {list(metric_map.keys())}"
|
552
|
+
)
|
553
|
+
|
554
|
+
# Generate the Poincaré section or stroboscopic map
|
555
|
+
points = generate_poincare_section(
|
556
|
+
q,
|
557
|
+
p,
|
558
|
+
num_points,
|
559
|
+
parameters,
|
560
|
+
grad_T,
|
561
|
+
grad_V,
|
562
|
+
time_step,
|
563
|
+
integrator,
|
564
|
+
section_index,
|
565
|
+
section_value,
|
566
|
+
crossing,
|
567
|
+
)
|
568
|
+
data = points[:, 1:] # Remove time
|
569
|
+
data = np.delete(data, section_index, axis=1)
|
570
|
+
|
571
|
+
# Threshold calculation
|
572
|
+
if config.threshold_std:
|
573
|
+
std = np.std(data, axis=0)
|
574
|
+
eps = config.threshold * np.linalg.norm(std, ord=ord)
|
575
|
+
if eps <= 0:
|
576
|
+
eps = 0.1
|
577
|
+
else:
|
578
|
+
eps = config.threshold
|
579
|
+
|
580
|
+
# Recurrence matrix calculation
|
581
|
+
recmat = recurrence_matrix(data, float(eps), metric=config.metric)
|
582
|
+
|
583
|
+
# White line distribution
|
584
|
+
P = white_vertline_distr(recmat)[config.lmin :]
|
585
|
+
P = P[P > 0] # Remove zeros
|
586
|
+
P /= P.sum() # Normalize
|
587
|
+
|
588
|
+
# Entropy calculation
|
589
|
+
rte = -np.sum(P * np.log(P))
|
590
|
+
|
591
|
+
# Prepare output
|
592
|
+
result = [rte]
|
593
|
+
if config.return_final_state:
|
594
|
+
result.append(points[-1])
|
595
|
+
if config.return_recmat:
|
596
|
+
result.append(recmat)
|
597
|
+
if config.return_p:
|
598
|
+
result.append(P)
|
599
|
+
|
600
|
+
return result[0] if len(result) == 1 else tuple(result)
|
601
|
+
|
602
|
+
|
603
|
+
def hurst_exponent_wrapped(
|
604
|
+
q: NDArray[np.float64],
|
605
|
+
p: NDArray[np.float64],
|
606
|
+
num_points: int,
|
607
|
+
parameters: NDArray[np.float64],
|
608
|
+
grad_T: Callable,
|
609
|
+
grad_V: Callable,
|
610
|
+
time_step: float,
|
611
|
+
integrator: Callable,
|
612
|
+
section_index: int,
|
613
|
+
section_value: float,
|
614
|
+
crossing: int,
|
615
|
+
wmin: int = 2,
|
616
|
+
) -> NDArray[np.float64]:
|
617
|
+
|
618
|
+
q = q.copy()
|
619
|
+
p = p.copy()
|
620
|
+
|
621
|
+
# Generate the Poincaré section or stroboscopic map
|
622
|
+
points = generate_poincare_section(
|
623
|
+
q,
|
624
|
+
p,
|
625
|
+
num_points,
|
626
|
+
parameters,
|
627
|
+
grad_T,
|
628
|
+
grad_V,
|
629
|
+
time_step,
|
630
|
+
integrator,
|
631
|
+
section_index,
|
632
|
+
section_value,
|
633
|
+
crossing,
|
634
|
+
)
|
635
|
+
data = points[:, 1:] # Remove time
|
636
|
+
data = np.delete(data, section_index, axis=1)
|
637
|
+
|
638
|
+
return hurst_exponent(data, wmin=wmin)
|
@@ -0,0 +1,68 @@
|
|
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
|
+
import numpy as np
|
19
|
+
from numba import njit
|
20
|
+
from numpy.typing import NDArray
|
21
|
+
from typing import Union, Sequence
|
22
|
+
|
23
|
+
|
24
|
+
@njit
|
25
|
+
def henon_heiles_grad_T(
|
26
|
+
p: NDArray[np.float64],
|
27
|
+
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
28
|
+
) -> NDArray[np.float64]:
|
29
|
+
"""Gradient of T(p)=0.5*(p0^2+p1^2). Returns [dT/dp0, dT/dp1]."""
|
30
|
+
p0, p1 = p[0], p[1]
|
31
|
+
return np.array([p0, p1])
|
32
|
+
|
33
|
+
|
34
|
+
@njit
|
35
|
+
def henon_heiles_hess_T(
|
36
|
+
p=None,
|
37
|
+
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
38
|
+
) -> NDArray[np.float64]:
|
39
|
+
"""Hessian of T (unit-mass) - constant 2x2 identity matrix.
|
40
|
+
p argument unused, kept for API symmetry with other functions."""
|
41
|
+
return np.array([[1.0, 0.0], [0.0, 1.0]])
|
42
|
+
|
43
|
+
|
44
|
+
@njit
|
45
|
+
def henon_heiles_grad_V(
|
46
|
+
q,
|
47
|
+
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
48
|
+
) -> NDArray[np.float64]:
|
49
|
+
"""Gradient of Hénon–Heiles potential V at q = [q0, q1].
|
50
|
+
Returns [dV/dq0, dV/dq1]."""
|
51
|
+
q0, q1 = q[0], q[1]
|
52
|
+
dV_dq0 = q0 * (1.0 + 2.0 * q1)
|
53
|
+
dV_dq1 = q1 + q0 * q0 - q1 * q1
|
54
|
+
return np.array([dV_dq0, dV_dq1])
|
55
|
+
|
56
|
+
|
57
|
+
@njit
|
58
|
+
def henon_heiles_hess_V(
|
59
|
+
q,
|
60
|
+
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
61
|
+
) -> NDArray[np.float64]:
|
62
|
+
"""Hessian of Hénon–Heiles potential V at q = [q0, q1].
|
63
|
+
Returns a 2x2 nested list [[H00, H01], [H10, H11]]."""
|
64
|
+
q0, q1 = q[0], q[1]
|
65
|
+
H00 = 1.0 + 2.0 * q1
|
66
|
+
H01 = 2.0 * q0
|
67
|
+
H11 = 1.0 - 2.0 * q1
|
68
|
+
return np.array([[H00, H01], [H01, H11]])
|