pynamicalsys 1.3.0__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.
Files changed (28) hide show
  1. pynamicalsys/__init__.py +2 -0
  2. pynamicalsys/__version__.py +2 -2
  3. pynamicalsys/common/recurrence_quantification_analysis.py +1 -1
  4. pynamicalsys/common/time_series_metrics.py +85 -0
  5. pynamicalsys/common/utils.py +3 -3
  6. pynamicalsys/continuous_time/chaotic_indicators.py +306 -8
  7. pynamicalsys/continuous_time/models.py +25 -0
  8. pynamicalsys/continuous_time/numerical_integrators.py +7 -7
  9. pynamicalsys/continuous_time/trajectory_analysis.py +460 -13
  10. pynamicalsys/core/continuous_dynamical_systems.py +933 -35
  11. pynamicalsys/core/discrete_dynamical_systems.py +20 -9
  12. pynamicalsys/core/hamiltonian_systems.py +1193 -0
  13. pynamicalsys/core/time_series_metrics.py +65 -0
  14. pynamicalsys/discrete_time/dynamical_indicators.py +13 -102
  15. pynamicalsys/discrete_time/models.py +2 -2
  16. pynamicalsys/discrete_time/trajectory_analysis.py +10 -10
  17. pynamicalsys/discrete_time/transport.py +1 -1
  18. pynamicalsys/hamiltonian_systems/__init__.py +16 -0
  19. pynamicalsys/hamiltonian_systems/chaotic_indicators.py +638 -0
  20. pynamicalsys/hamiltonian_systems/models.py +68 -0
  21. pynamicalsys/hamiltonian_systems/numerical_integrators.py +248 -0
  22. pynamicalsys/hamiltonian_systems/trajectory_analysis.py +293 -0
  23. pynamicalsys/hamiltonian_systems/validators.py +114 -0
  24. {pynamicalsys-1.3.0.dist-info → pynamicalsys-1.4.0.dist-info}/METADATA +37 -8
  25. pynamicalsys-1.4.0.dist-info/RECORD +36 -0
  26. pynamicalsys-1.3.0.dist-info/RECORD +0 -28
  27. {pynamicalsys-1.3.0.dist-info → pynamicalsys-1.4.0.dist-info}/WHEEL +0 -0
  28. {pynamicalsys-1.3.0.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]])