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,1193 @@
|
|
1
|
+
# hamiltonian_systems.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 Any, Callable, Dict, List, Optional, Sequence, Union, Tuple
|
20
|
+
|
21
|
+
import numpy as np
|
22
|
+
from numpy.typing import NDArray
|
23
|
+
|
24
|
+
from pynamicalsys.hamiltonian_systems.models import (
|
25
|
+
henon_heiles_grad_T,
|
26
|
+
henon_heiles_grad_V,
|
27
|
+
henon_heiles_hess_T,
|
28
|
+
henon_heiles_hess_V,
|
29
|
+
)
|
30
|
+
|
31
|
+
from pynamicalsys.hamiltonian_systems.numerical_integrators import (
|
32
|
+
velocity_verlet_2nd_step_tangent,
|
33
|
+
yoshida_4th_step,
|
34
|
+
velocity_verlet_2nd_step,
|
35
|
+
yoshida_4th_step_tangent,
|
36
|
+
)
|
37
|
+
|
38
|
+
from pynamicalsys.hamiltonian_systems.validators import (
|
39
|
+
validate_initial_conditions,
|
40
|
+
validate_non_negative,
|
41
|
+
validate_parameters,
|
42
|
+
)
|
43
|
+
|
44
|
+
from pynamicalsys.common.utils import qr, householder_qr, fit_poly
|
45
|
+
|
46
|
+
from pynamicalsys.hamiltonian_systems.trajectory_analysis import (
|
47
|
+
generate_trajectory,
|
48
|
+
ensemble_trajectories,
|
49
|
+
generate_poincare_section,
|
50
|
+
ensemble_poincare_section,
|
51
|
+
)
|
52
|
+
|
53
|
+
from pynamicalsys.hamiltonian_systems.chaotic_indicators import (
|
54
|
+
lyapunov_spectrum,
|
55
|
+
maximum_lyapunov_exponent,
|
56
|
+
SALI,
|
57
|
+
LDI,
|
58
|
+
GALI,
|
59
|
+
recurrence_time_entropy,
|
60
|
+
hurst_exponent_wrapped,
|
61
|
+
)
|
62
|
+
|
63
|
+
|
64
|
+
class HamiltonianSystem:
|
65
|
+
"""
|
66
|
+
Class for defining, simulating, and analyzing Hamiltonian dynamical systems.
|
67
|
+
|
68
|
+
This class provides access to predefined Hamiltonian models (e.g., the
|
69
|
+
Hénon-Heiles system) or allows the user to define a custom Hamiltonian
|
70
|
+
system via gradient and optional Hessian functions. It supports multiple
|
71
|
+
numerical symplectic integrators and a variety of trajectory and chaos
|
72
|
+
analysis tools, such as Lyapunov exponents, SALI, LDI, and GALI.
|
73
|
+
|
74
|
+
Examples
|
75
|
+
--------
|
76
|
+
>>> from pynamicalsys import HamiltonianSystem
|
77
|
+
>>> hs = HamiltonianSystem(model="henon heiles")
|
78
|
+
>>> hs.available_models()
|
79
|
+
['henon heiles']
|
80
|
+
>>> hs.available_integrators()
|
81
|
+
['svy4', 'vv2']
|
82
|
+
"""
|
83
|
+
|
84
|
+
__AVAILABLE_MODELS: Dict[str, Dict[str, Any]] = {
|
85
|
+
"henon heiles": {
|
86
|
+
"description": "two d.o.f. Hénon-Heiles Hamiltonian system",
|
87
|
+
"has hessian": True,
|
88
|
+
"grad_T": henon_heiles_grad_T,
|
89
|
+
"grad_V": henon_heiles_grad_V,
|
90
|
+
"hess_T": henon_heiles_hess_T,
|
91
|
+
"hess_V": henon_heiles_hess_V,
|
92
|
+
"degrees of freedom": 2,
|
93
|
+
"number of parameters": 0,
|
94
|
+
"parameters": [],
|
95
|
+
},
|
96
|
+
}
|
97
|
+
|
98
|
+
__AVAILABLE_INTEGRATORS: Dict[str, Dict[str, Any]] = {
|
99
|
+
"svy4": {
|
100
|
+
"description": "4th order Yoshida method",
|
101
|
+
"integrator": yoshida_4th_step,
|
102
|
+
"tangent integrator": yoshida_4th_step_tangent,
|
103
|
+
},
|
104
|
+
"vv2": {
|
105
|
+
"description": "2nd order velocity Verlet method",
|
106
|
+
"integrator": velocity_verlet_2nd_step,
|
107
|
+
"tangent integrator": velocity_verlet_2nd_step_tangent,
|
108
|
+
},
|
109
|
+
}
|
110
|
+
|
111
|
+
def __init__(
|
112
|
+
self,
|
113
|
+
model: Optional[str] = None,
|
114
|
+
grad_T: Optional[Callable] = None,
|
115
|
+
grad_V: Optional[Callable] = None,
|
116
|
+
hess_T: Optional[Callable] = None,
|
117
|
+
hess_V: Optional[Callable] = None,
|
118
|
+
degrees_of_freedom: Optional[int] = None,
|
119
|
+
number_of_parameters: Optional[int] = None,
|
120
|
+
) -> None:
|
121
|
+
if model is not None and (grad_T is not None or grad_V is not None):
|
122
|
+
raise ValueError("Cannot specify both model and custom system")
|
123
|
+
|
124
|
+
if model is not None:
|
125
|
+
model = model.lower()
|
126
|
+
if model not in self.__AVAILABLE_MODELS:
|
127
|
+
available = "\n".join(
|
128
|
+
f"- {name}: {info['description']}"
|
129
|
+
for name, info in self.__AVAILABLE_MODELS.items()
|
130
|
+
)
|
131
|
+
raise ValueError(
|
132
|
+
f"Model '{model}' not implemented. Available models:\n{available}"
|
133
|
+
)
|
134
|
+
|
135
|
+
model_info = self.__AVAILABLE_MODELS[model]
|
136
|
+
self.__model = model
|
137
|
+
self.__grad_T = model_info["grad_T"]
|
138
|
+
self.__grad_V = model_info["grad_V"]
|
139
|
+
self.__hess_T = model_info["hess_T"]
|
140
|
+
self.__hess_V = model_info["hess_V"]
|
141
|
+
self.__degrees_of_freedom = model_info["degrees of freedom"]
|
142
|
+
self.__number_of_parameters = model_info["number of parameters"]
|
143
|
+
elif (
|
144
|
+
grad_T is not None
|
145
|
+
and grad_V is not None
|
146
|
+
and degrees_of_freedom is not None
|
147
|
+
and number_of_parameters is not None
|
148
|
+
):
|
149
|
+
if not callable(grad_T) or not callable(grad_V):
|
150
|
+
raise TypeError(
|
151
|
+
"The custom system (grad V and grad T) must be callable"
|
152
|
+
)
|
153
|
+
|
154
|
+
self.__grad_T = grad_T
|
155
|
+
self.__grad_V = grad_V
|
156
|
+
|
157
|
+
self.__hess_T = hess_T
|
158
|
+
self.__hess_V = hess_V
|
159
|
+
|
160
|
+
if (
|
161
|
+
self.__hess_T is not None
|
162
|
+
and self.__hess_V is not None
|
163
|
+
and not callable(self.__hess_T)
|
164
|
+
and not callable(self.__hess_V)
|
165
|
+
):
|
166
|
+
raise TypeError("Custom Hessian functions must be callable")
|
167
|
+
|
168
|
+
validate_non_negative(degrees_of_freedom, "degrees_of_freedom", Integral)
|
169
|
+
validate_non_negative(
|
170
|
+
number_of_parameters, "number_of_parameters", Integral
|
171
|
+
)
|
172
|
+
|
173
|
+
self.__degrees_of_freedom = degrees_of_freedom
|
174
|
+
self.__number_of_parameters = number_of_parameters
|
175
|
+
else:
|
176
|
+
raise ValueError(
|
177
|
+
"Must specify either a model name or custom system function (grad V and grad T) with its dimension and number of paramters."
|
178
|
+
)
|
179
|
+
|
180
|
+
self.__integrator = "svy4"
|
181
|
+
self.__integrator_func = yoshida_4th_step
|
182
|
+
self.__tangent_integrator_func = yoshida_4th_step_tangent
|
183
|
+
self.__time_step = 1e-2
|
184
|
+
|
185
|
+
@classmethod
|
186
|
+
def available_models(cls) -> List[str]:
|
187
|
+
"""
|
188
|
+
List the available predefined Hamiltonian models.
|
189
|
+
|
190
|
+
Returns
|
191
|
+
-------
|
192
|
+
list of str
|
193
|
+
Names of the supported models.
|
194
|
+
"""
|
195
|
+
return list(cls.__AVAILABLE_MODELS.keys())
|
196
|
+
|
197
|
+
@classmethod
|
198
|
+
def available_integrators(cls) -> List[str]:
|
199
|
+
"""
|
200
|
+
List the available predefined Hamiltonian models.
|
201
|
+
|
202
|
+
Returns
|
203
|
+
-------
|
204
|
+
list of str
|
205
|
+
Names of the supported models.
|
206
|
+
"""
|
207
|
+
return list(cls.__AVAILABLE_INTEGRATORS.keys())
|
208
|
+
|
209
|
+
@property
|
210
|
+
def info(self) -> Dict[str, Any]:
|
211
|
+
"""
|
212
|
+
Information dictionary for the selected model.
|
213
|
+
|
214
|
+
Returns
|
215
|
+
-------
|
216
|
+
dict
|
217
|
+
Dictionary containing metadata such as description, gradients,
|
218
|
+
Hessians, degrees of freedom, and parameters.
|
219
|
+
|
220
|
+
Raises
|
221
|
+
------
|
222
|
+
ValueError
|
223
|
+
If no predefined model was used to initialize the system.
|
224
|
+
"""
|
225
|
+
|
226
|
+
if self.__model is None:
|
227
|
+
raise ValueError(
|
228
|
+
"The 'info' property is only available when a model is provided."
|
229
|
+
)
|
230
|
+
|
231
|
+
model = self.__model.lower()
|
232
|
+
|
233
|
+
return self.__AVAILABLE_MODELS[model]
|
234
|
+
|
235
|
+
@property
|
236
|
+
def integrator_info(self) -> Dict[str, Any]:
|
237
|
+
"""
|
238
|
+
Information dictionary for the current integrator.
|
239
|
+
|
240
|
+
Returns
|
241
|
+
-------
|
242
|
+
dict
|
243
|
+
Dictionary containing the integrator description and associated
|
244
|
+
step functions.
|
245
|
+
"""
|
246
|
+
integrator = self.__integrator.lower()
|
247
|
+
|
248
|
+
return self.__AVAILABLE_INTEGRATORS[integrator]
|
249
|
+
|
250
|
+
def integrator(self, integrator, time_step=1e-2) -> None:
|
251
|
+
"""
|
252
|
+
Set the numerical integrator and time step.
|
253
|
+
|
254
|
+
Parameters
|
255
|
+
----------
|
256
|
+
integrator : str
|
257
|
+
Name of the integrator. Options:
|
258
|
+
- 'svy4' : 4th order Yoshida method
|
259
|
+
- 'vv2' : 2nd order velocity-Verlet
|
260
|
+
time_step : float, optional
|
261
|
+
Integration time step (default is 1e-2).
|
262
|
+
|
263
|
+
Raises
|
264
|
+
------
|
265
|
+
ValueError
|
266
|
+
If `time_step` is negative or if `integrator` is not available.
|
267
|
+
TypeError
|
268
|
+
If `integrator` is not a string or `time_step` is not numeric.
|
269
|
+
|
270
|
+
Examples
|
271
|
+
--------
|
272
|
+
>>> from pynamicalsys import HamiltonianSystem
|
273
|
+
>>> HamiltonianSystem.available_integrators()
|
274
|
+
['svy4', 'vv2']
|
275
|
+
>>> hs = HamiltonianSystem(model="henon heiles")
|
276
|
+
>>> hs.integrator("svy4", time_step=0.001) # To use the SVY4 integrator with a time step of 10^{-3}
|
277
|
+
>>> hs.integrator("vv2", time_step=0.001) # To use the VV2 integrator
|
278
|
+
"""
|
279
|
+
|
280
|
+
if not isinstance(integrator, str):
|
281
|
+
raise ValueError("integrator must be a string.")
|
282
|
+
validate_non_negative(time_step, "time_step", type_=Real)
|
283
|
+
|
284
|
+
if integrator in self.__AVAILABLE_INTEGRATORS:
|
285
|
+
self.__integrator = integrator.lower()
|
286
|
+
integrator_info = self.__AVAILABLE_INTEGRATORS[self.__integrator]
|
287
|
+
self.__integrator_func = integrator_info["integrator"]
|
288
|
+
self.__tangent_integrator_func = integrator_info["tangent integrator"]
|
289
|
+
self.__time_step = time_step
|
290
|
+
|
291
|
+
else:
|
292
|
+
integrator = integrator.lower()
|
293
|
+
if integrator not in self.__AVAILABLE_INTEGRATORS:
|
294
|
+
available = "\n".join(
|
295
|
+
f"- {name}: {info['description']}"
|
296
|
+
for name, info in self.__AVAILABLE_INTEGRATORS.items()
|
297
|
+
)
|
298
|
+
raise ValueError(
|
299
|
+
f"Integrator '{integrator}' not implemented. Available integrators:\n{available}"
|
300
|
+
)
|
301
|
+
|
302
|
+
def step(
|
303
|
+
self,
|
304
|
+
q: NDArray[np.float64],
|
305
|
+
p: NDArray[np.float64],
|
306
|
+
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
307
|
+
) -> NDArray[np.float64]:
|
308
|
+
"""
|
309
|
+
Advance the system by one integration step.
|
310
|
+
|
311
|
+
Parameters
|
312
|
+
----------
|
313
|
+
q : ndarray
|
314
|
+
Initial generalized coordinates (shape: (dof,) or (N, dof)).
|
315
|
+
p : ndarray
|
316
|
+
Initial generalized momenta (same shape as `q`).
|
317
|
+
parameters : array-like, optional
|
318
|
+
System parameters, if required.
|
319
|
+
|
320
|
+
Returns
|
321
|
+
-------
|
322
|
+
q_new, p_new : tuple of ndarray
|
323
|
+
Updated coordinates and momenta after one integration step.
|
324
|
+
|
325
|
+
Raises
|
326
|
+
------
|
327
|
+
ValueError
|
328
|
+
If `q` and `p` have mismatched shapes.
|
329
|
+
TypeError
|
330
|
+
If inputs are not scalar or array-like.
|
331
|
+
"""
|
332
|
+
q = validate_initial_conditions(q, self.__degrees_of_freedom)
|
333
|
+
q = q.copy()
|
334
|
+
p = validate_initial_conditions(p, self.__degrees_of_freedom)
|
335
|
+
p = p.copy()
|
336
|
+
|
337
|
+
if q.ndim != p.ndim:
|
338
|
+
raise ValueError("q and p must have the same dimension and shape")
|
339
|
+
|
340
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
341
|
+
|
342
|
+
if q.ndim == 1:
|
343
|
+
q, p = self.__integrator_func(
|
344
|
+
q, p, self.__time_step, self.__grad_T, self.__grad_V
|
345
|
+
)
|
346
|
+
else:
|
347
|
+
num_ic = q.shape[0]
|
348
|
+
for i in range(num_ic):
|
349
|
+
q[i], p[i] = self.__integrator_func(
|
350
|
+
q[i], p[i], self.__time_step, self.__grad_T, self.__grad_V
|
351
|
+
)
|
352
|
+
|
353
|
+
return q, p
|
354
|
+
|
355
|
+
def trajectory(
|
356
|
+
self,
|
357
|
+
q: NDArray[np.float64],
|
358
|
+
p: NDArray[np.float64],
|
359
|
+
total_time: np.float64,
|
360
|
+
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
361
|
+
) -> NDArray[np.float64]:
|
362
|
+
"""
|
363
|
+
Generate a trajectory for the system.
|
364
|
+
|
365
|
+
Parameters
|
366
|
+
----------
|
367
|
+
q : ndarray
|
368
|
+
Initial coordinates.
|
369
|
+
p : ndarray
|
370
|
+
Initial momenta.
|
371
|
+
total_time : float
|
372
|
+
Total integration time.
|
373
|
+
parameters : array-like, optional
|
374
|
+
System parameters.
|
375
|
+
|
376
|
+
Returns
|
377
|
+
-------
|
378
|
+
trajectory : ndarray
|
379
|
+
Trajectory data with shape depending on whether ensemble or single
|
380
|
+
ICs are provided.
|
381
|
+
|
382
|
+
Raises
|
383
|
+
------
|
384
|
+
ValueError
|
385
|
+
If `q` and `p` shapes mismatch or if `total_time` is negative.
|
386
|
+
"""
|
387
|
+
q = validate_initial_conditions(q, self.__degrees_of_freedom)
|
388
|
+
q = q.copy()
|
389
|
+
|
390
|
+
p = validate_initial_conditions(p, self.__degrees_of_freedom)
|
391
|
+
p = p.copy()
|
392
|
+
|
393
|
+
if q.ndim != p.ndim:
|
394
|
+
raise ValueError("q and p must have the same dimension and shape")
|
395
|
+
|
396
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
397
|
+
|
398
|
+
validate_non_negative(total_time, "total time", type_=Real)
|
399
|
+
|
400
|
+
if q.ndim == 1:
|
401
|
+
trajectory_function = generate_trajectory
|
402
|
+
else:
|
403
|
+
trajectory_function = ensemble_trajectories
|
404
|
+
|
405
|
+
return trajectory_function(
|
406
|
+
q,
|
407
|
+
p,
|
408
|
+
total_time,
|
409
|
+
parameters,
|
410
|
+
self.__grad_T,
|
411
|
+
self.__grad_V,
|
412
|
+
self.__time_step,
|
413
|
+
self.__integrator_func,
|
414
|
+
)
|
415
|
+
|
416
|
+
def poincare_section(
|
417
|
+
self,
|
418
|
+
q: NDArray[np.float64],
|
419
|
+
p: NDArray[np.float64],
|
420
|
+
num_intersections: int,
|
421
|
+
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
422
|
+
section_index: int = 0,
|
423
|
+
section_value: float = 0.0,
|
424
|
+
crossing: int = 1,
|
425
|
+
) -> NDArray[np.float64]:
|
426
|
+
"""
|
427
|
+
Compute a Poincaré section of the trajectory.
|
428
|
+
|
429
|
+
Parameters
|
430
|
+
----------
|
431
|
+
q, p : ndarray
|
432
|
+
Initial coordinates and momenta.
|
433
|
+
total_time : float
|
434
|
+
Total simulation time.
|
435
|
+
parameters : array-like, optional
|
436
|
+
System parameters.
|
437
|
+
section_index : int, default=0
|
438
|
+
Index of the phase space coordinate for the section.
|
439
|
+
section_value : float, default=0.0
|
440
|
+
Value at which the section is taken.
|
441
|
+
crossing : {-1, 0, 1}, default=1
|
442
|
+
Direction of crossing:
|
443
|
+
-1 : downward
|
444
|
+
0 : all crossings
|
445
|
+
1 : upward
|
446
|
+
|
447
|
+
Returns
|
448
|
+
-------
|
449
|
+
section_points : ndarray
|
450
|
+
Points of the trajectory lying on the Poincaré section.
|
451
|
+
|
452
|
+
Raises
|
453
|
+
------
|
454
|
+
ValueError
|
455
|
+
If shapes mismatch, if `section_index` is invalid,
|
456
|
+
or if `crossing` not in {-1, 0, 1}.
|
457
|
+
TypeError
|
458
|
+
If `section_value` is not numeric.
|
459
|
+
"""
|
460
|
+
q = validate_initial_conditions(q, self.__degrees_of_freedom)
|
461
|
+
q = q.copy()
|
462
|
+
|
463
|
+
p = validate_initial_conditions(p, self.__degrees_of_freedom)
|
464
|
+
p = p.copy()
|
465
|
+
|
466
|
+
if q.ndim != p.ndim:
|
467
|
+
raise ValueError("q and p must have the same dimension and shape")
|
468
|
+
|
469
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
470
|
+
|
471
|
+
validate_non_negative(
|
472
|
+
num_intersections, "num_intersections time", type_=Integral
|
473
|
+
)
|
474
|
+
|
475
|
+
validate_non_negative(section_index, "section_index")
|
476
|
+
if section_index >= 2 * self.__degrees_of_freedom:
|
477
|
+
raise ValueError(
|
478
|
+
"section_index must be less or equal to the system dimension"
|
479
|
+
)
|
480
|
+
|
481
|
+
if not isinstance(section_value, Real):
|
482
|
+
raise TypeError("section_value must be a valid number")
|
483
|
+
|
484
|
+
if crossing not in [-1, 0, 1]:
|
485
|
+
raise ValueError(
|
486
|
+
"crossing must be either -1, 0, or 1, indicating downward, all crossings, and upward crossings, respectively"
|
487
|
+
)
|
488
|
+
|
489
|
+
if q.ndim == 1:
|
490
|
+
poincare_section_function = generate_poincare_section
|
491
|
+
else:
|
492
|
+
poincare_section_function = ensemble_poincare_section
|
493
|
+
|
494
|
+
return poincare_section_function(
|
495
|
+
q,
|
496
|
+
p,
|
497
|
+
num_intersections,
|
498
|
+
parameters,
|
499
|
+
self.__grad_T,
|
500
|
+
self.__grad_V,
|
501
|
+
self.__time_step,
|
502
|
+
self.__integrator_func,
|
503
|
+
section_index,
|
504
|
+
section_value,
|
505
|
+
crossing,
|
506
|
+
)
|
507
|
+
|
508
|
+
def lyapunov(
|
509
|
+
self,
|
510
|
+
q: NDArray[np.float64],
|
511
|
+
p: NDArray[np.float64],
|
512
|
+
total_time: np.float64,
|
513
|
+
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
514
|
+
num_exponents: Optional[int] = None,
|
515
|
+
return_history: bool = False,
|
516
|
+
seed: int = 13,
|
517
|
+
log_base: np.float64 = np.e,
|
518
|
+
qr_interval: int = 1,
|
519
|
+
method: str = "QR",
|
520
|
+
) -> NDArray[np.float64]:
|
521
|
+
"""
|
522
|
+
Compute Lyapunov exponents.
|
523
|
+
|
524
|
+
Parameters
|
525
|
+
----------
|
526
|
+
q, p : ndarray
|
527
|
+
Initial conditions (single orbit only).
|
528
|
+
total_time : float
|
529
|
+
Total integration time.
|
530
|
+
parameters : array-like, optional
|
531
|
+
System parameters.
|
532
|
+
num_exponents : int, optional
|
533
|
+
Number of exponents to compute (default: full spectrum).
|
534
|
+
return_history : bool, default=False
|
535
|
+
If True, return time evolution instead of final values.
|
536
|
+
seed : int, default=13
|
537
|
+
Random seed for initial deviation vectors.
|
538
|
+
log_base : float, default=e
|
539
|
+
Logarithm base for exponent calculation.
|
540
|
+
qr_interval : int, default=1
|
541
|
+
Interval for reorthonormalization.
|
542
|
+
method : {'QR', 'QR_HH'}, default='QR'
|
543
|
+
QR decomposition method.
|
544
|
+
|
545
|
+
Returns
|
546
|
+
-------
|
547
|
+
exponents : ndarray
|
548
|
+
Computed Lyapunov exponents.
|
549
|
+
|
550
|
+
Raises
|
551
|
+
------
|
552
|
+
ValueError
|
553
|
+
If Hessians are missing, if inputs are invalid, or if base=1.
|
554
|
+
TypeError
|
555
|
+
If types are inconsistent with expectations.
|
556
|
+
"""
|
557
|
+
|
558
|
+
if self.__hess_T is None or self.__hess_V is None:
|
559
|
+
raise ValueError(
|
560
|
+
"Hessian functions are required to compute the Lyapunov exponents"
|
561
|
+
)
|
562
|
+
|
563
|
+
q = validate_initial_conditions(
|
564
|
+
q, self.__degrees_of_freedom, allow_ensemble=False
|
565
|
+
)
|
566
|
+
q = q.copy()
|
567
|
+
p = validate_initial_conditions(
|
568
|
+
p, self.__degrees_of_freedom, allow_ensemble=False
|
569
|
+
)
|
570
|
+
p = p.copy()
|
571
|
+
|
572
|
+
validate_non_negative(total_time, "total time", type_=Real)
|
573
|
+
|
574
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
575
|
+
|
576
|
+
if num_exponents is None:
|
577
|
+
num_exponents = 2 * self.__degrees_of_freedom
|
578
|
+
elif num_exponents > 2 * self.__degrees_of_freedom or num_exponents < 1:
|
579
|
+
raise ValueError("num_exponents must be <= system_dimension")
|
580
|
+
else:
|
581
|
+
validate_non_negative(num_exponents, "num_exponents", Integral)
|
582
|
+
|
583
|
+
if not isinstance(return_history, bool):
|
584
|
+
raise TypeError("return_history must be True or False")
|
585
|
+
|
586
|
+
if not isinstance(seed, Integral):
|
587
|
+
raise TypeError("seed must be an integer")
|
588
|
+
|
589
|
+
validate_non_negative(log_base, "log_base", Real)
|
590
|
+
if log_base == 1:
|
591
|
+
raise ValueError("The logarithm function is not defined with base 1")
|
592
|
+
|
593
|
+
validate_non_negative(qr_interval, "qr_interval", Integral)
|
594
|
+
|
595
|
+
if not isinstance(method, str):
|
596
|
+
raise TypeError("method must be a string")
|
597
|
+
|
598
|
+
method = method.upper()
|
599
|
+
if method == "QR":
|
600
|
+
qr_func = qr
|
601
|
+
elif method == "QR_HH":
|
602
|
+
qr_func = householder_qr
|
603
|
+
else:
|
604
|
+
raise ValueError("method must be QR or QR_HH")
|
605
|
+
|
606
|
+
if num_exponents > 1:
|
607
|
+
result = lyapunov_spectrum(
|
608
|
+
q,
|
609
|
+
p,
|
610
|
+
total_time,
|
611
|
+
self.__time_step,
|
612
|
+
parameters,
|
613
|
+
self.__grad_T,
|
614
|
+
self.__grad_V,
|
615
|
+
self.__hess_T,
|
616
|
+
self.__hess_V,
|
617
|
+
num_exponents,
|
618
|
+
qr_interval,
|
619
|
+
return_history,
|
620
|
+
seed,
|
621
|
+
log_base,
|
622
|
+
qr_func,
|
623
|
+
self.__integrator_func,
|
624
|
+
self.__tangent_integrator_func,
|
625
|
+
)
|
626
|
+
else:
|
627
|
+
result = maximum_lyapunov_exponent(
|
628
|
+
q,
|
629
|
+
p,
|
630
|
+
total_time,
|
631
|
+
self.__time_step,
|
632
|
+
parameters,
|
633
|
+
self.__grad_T,
|
634
|
+
self.__grad_V,
|
635
|
+
self.__hess_T,
|
636
|
+
self.__hess_V,
|
637
|
+
return_history,
|
638
|
+
seed,
|
639
|
+
log_base,
|
640
|
+
self.__integrator_func,
|
641
|
+
self.__tangent_integrator_func,
|
642
|
+
)
|
643
|
+
|
644
|
+
if return_history:
|
645
|
+
return np.array(result)
|
646
|
+
else:
|
647
|
+
return np.array(result[0])
|
648
|
+
|
649
|
+
def SALI(
|
650
|
+
self,
|
651
|
+
q: NDArray[np.float64],
|
652
|
+
p: NDArray[np.float64],
|
653
|
+
total_time: float,
|
654
|
+
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
655
|
+
return_history: bool = False,
|
656
|
+
seed: int = 13,
|
657
|
+
threshold: float = 1e-16,
|
658
|
+
) -> NDArray[np.float64]:
|
659
|
+
"""
|
660
|
+
Compute the Smaller Alignment Index (SALI).
|
661
|
+
|
662
|
+
SALI distinguishes between chaotic and regular motion by evolving two
|
663
|
+
deviation vectors and monitoring their alignment over time. In chaotic
|
664
|
+
systems, SALI tends exponentially to zero; in regular systems, it
|
665
|
+
stabilizes to a nonzero value.
|
666
|
+
|
667
|
+
Parameters
|
668
|
+
----------
|
669
|
+
q, p : ndarray
|
670
|
+
Initial coordinates and momenta (1D arrays).
|
671
|
+
total_time : float
|
672
|
+
Total integration time.
|
673
|
+
parameters : array-like, optional
|
674
|
+
System parameters.
|
675
|
+
return_history : bool, default=False
|
676
|
+
If True, return SALI evolution over time.
|
677
|
+
seed : int, default=13
|
678
|
+
Random seed for initializing deviation vectors.
|
679
|
+
threshold : float, default=1e-8
|
680
|
+
Early termination threshold for SALI. If SALI ≤ threshold,
|
681
|
+
integration stops.
|
682
|
+
|
683
|
+
Returns
|
684
|
+
-------
|
685
|
+
sali : ndarray
|
686
|
+
- If `return_history=True`, array of shape (N, 2) with columns
|
687
|
+
[time, SALI].
|
688
|
+
- If `return_history=False`, array of shape (1, 2) with the final
|
689
|
+
[time, SALI].
|
690
|
+
|
691
|
+
Raises
|
692
|
+
------
|
693
|
+
ValueError
|
694
|
+
If Hessians are missing, if `total_time` is negative, or if
|
695
|
+
`threshold` ≤ 0.
|
696
|
+
TypeError
|
697
|
+
If input types are invalid (e.g., non-numeric values).
|
698
|
+
"""
|
699
|
+
if self.__hess_T is None or self.__hess_V is None:
|
700
|
+
raise ValueError(
|
701
|
+
"Hessian functions are required to compute the Lyapunov exponents"
|
702
|
+
)
|
703
|
+
|
704
|
+
q = validate_initial_conditions(
|
705
|
+
q, self.__degrees_of_freedom, allow_ensemble=False
|
706
|
+
)
|
707
|
+
q = q.copy()
|
708
|
+
p = validate_initial_conditions(
|
709
|
+
p, self.__degrees_of_freedom, allow_ensemble=False
|
710
|
+
)
|
711
|
+
p = p.copy()
|
712
|
+
|
713
|
+
validate_non_negative(total_time, "total time", type_=Real)
|
714
|
+
|
715
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
716
|
+
|
717
|
+
if not isinstance(return_history, bool):
|
718
|
+
raise TypeError("return_history must be True or False")
|
719
|
+
|
720
|
+
if not isinstance(seed, Integral):
|
721
|
+
raise TypeError("seed must be an integer")
|
722
|
+
|
723
|
+
validate_non_negative(threshold, "threshold", Real)
|
724
|
+
|
725
|
+
result = SALI(
|
726
|
+
q,
|
727
|
+
p,
|
728
|
+
total_time,
|
729
|
+
self.__time_step,
|
730
|
+
parameters,
|
731
|
+
self.__grad_T,
|
732
|
+
self.__grad_V,
|
733
|
+
self.__hess_T,
|
734
|
+
self.__hess_V,
|
735
|
+
return_history,
|
736
|
+
seed,
|
737
|
+
self.__integrator_func,
|
738
|
+
self.__tangent_integrator_func,
|
739
|
+
threshold,
|
740
|
+
)
|
741
|
+
|
742
|
+
if return_history:
|
743
|
+
return np.array(result)
|
744
|
+
else:
|
745
|
+
return result[0]
|
746
|
+
|
747
|
+
def LDI(
|
748
|
+
self,
|
749
|
+
q: NDArray[np.float64],
|
750
|
+
p: NDArray[np.float64],
|
751
|
+
total_time: float,
|
752
|
+
k: int,
|
753
|
+
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
754
|
+
return_history: bool = False,
|
755
|
+
seed: int = 13,
|
756
|
+
threshold: float = 1e-16,
|
757
|
+
) -> NDArray[np.float64]:
|
758
|
+
"""
|
759
|
+
Compute the Linear Dependence Index (LDI).
|
760
|
+
|
761
|
+
LDI measures the linear dependence among `k` deviation vectors evolved
|
762
|
+
along a trajectory. It is computed from the product of singular values
|
763
|
+
of the deviation matrix. In chaotic systems, LDI tends rapidly to zero;
|
764
|
+
in regular systems, it remains bounded away from zero.
|
765
|
+
|
766
|
+
Parameters
|
767
|
+
----------
|
768
|
+
q, p : ndarray
|
769
|
+
Initial coordinates and momenta (1D arrays).
|
770
|
+
total_time : float
|
771
|
+
Total integration time.
|
772
|
+
parameters : array-like, optional
|
773
|
+
System parameters.
|
774
|
+
k : int, default=2
|
775
|
+
Number of deviation vectors to evolve.
|
776
|
+
return_history : bool, default=False
|
777
|
+
If True, return LDI evolution over time.
|
778
|
+
seed : int, default=13
|
779
|
+
Random seed for initializing deviation vectors.
|
780
|
+
threshold : float, default=1e-8
|
781
|
+
Early termination threshold for LDI. If LDI ≤ threshold,
|
782
|
+
integration stops.
|
783
|
+
|
784
|
+
Returns
|
785
|
+
-------
|
786
|
+
ldi : ndarray
|
787
|
+
- If `return_history=True`, array of shape (N, 2) with columns
|
788
|
+
[time, LDI].
|
789
|
+
- If `return_history=False`, array of shape (1, 2) with the final
|
790
|
+
[time, LDI].
|
791
|
+
|
792
|
+
Raises
|
793
|
+
------
|
794
|
+
ValueError
|
795
|
+
If Hessians are missing, if `k` ≤ 1, if `total_time` is negative,
|
796
|
+
or if `threshold` ≤ 0.
|
797
|
+
TypeError
|
798
|
+
If input types are invalid (e.g., `k` not an integer).
|
799
|
+
"""
|
800
|
+
if self.__hess_T is None or self.__hess_V is None:
|
801
|
+
raise ValueError(
|
802
|
+
"Hessian functions are required to compute the Lyapunov exponents"
|
803
|
+
)
|
804
|
+
|
805
|
+
q = validate_initial_conditions(
|
806
|
+
q, self.__degrees_of_freedom, allow_ensemble=False
|
807
|
+
)
|
808
|
+
q = q.copy()
|
809
|
+
p = validate_initial_conditions(
|
810
|
+
p, self.__degrees_of_freedom, allow_ensemble=False
|
811
|
+
)
|
812
|
+
p = p.copy()
|
813
|
+
|
814
|
+
validate_non_negative(total_time, "total time", type_=Real)
|
815
|
+
|
816
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
817
|
+
|
818
|
+
validate_non_negative(k, "k", Integral)
|
819
|
+
if k <= 1 or k > 2 * self.__degrees_of_freedom:
|
820
|
+
raise ValueError("k must be 2 < k < system dimension")
|
821
|
+
|
822
|
+
if not isinstance(return_history, bool):
|
823
|
+
raise TypeError("return_history must be True or False")
|
824
|
+
|
825
|
+
if not isinstance(seed, Integral):
|
826
|
+
raise TypeError("seed must be an integer")
|
827
|
+
|
828
|
+
validate_non_negative(threshold, "threshold", Real)
|
829
|
+
|
830
|
+
result = LDI(
|
831
|
+
q,
|
832
|
+
p,
|
833
|
+
total_time,
|
834
|
+
self.__time_step,
|
835
|
+
parameters,
|
836
|
+
self.__grad_T,
|
837
|
+
self.__grad_V,
|
838
|
+
self.__hess_T,
|
839
|
+
self.__hess_V,
|
840
|
+
k,
|
841
|
+
return_history,
|
842
|
+
seed,
|
843
|
+
self.__integrator_func,
|
844
|
+
self.__tangent_integrator_func,
|
845
|
+
threshold,
|
846
|
+
)
|
847
|
+
|
848
|
+
if return_history:
|
849
|
+
return np.array(result)
|
850
|
+
else:
|
851
|
+
return result[0]
|
852
|
+
|
853
|
+
def GALI(
|
854
|
+
self,
|
855
|
+
q: NDArray[np.float64],
|
856
|
+
p: NDArray[np.float64],
|
857
|
+
total_time: float,
|
858
|
+
k: int,
|
859
|
+
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
860
|
+
return_history: bool = False,
|
861
|
+
seed: int = 13,
|
862
|
+
threshold: float = 1e-16,
|
863
|
+
) -> NDArray[np.float64]:
|
864
|
+
"""
|
865
|
+
Compute the Generalized Alignment Index (GALI).
|
866
|
+
|
867
|
+
GALI extends SALI by considering the evolution of `k` deviation
|
868
|
+
vectors. It is defined as the volume of the parallelepiped formed by
|
869
|
+
the normalized deviation vectors (via the wedge product). In chaotic
|
870
|
+
systems, GALI decays exponentially; in regular systems, it follows a
|
871
|
+
power law or stabilizes.
|
872
|
+
|
873
|
+
Parameters
|
874
|
+
----------
|
875
|
+
q, p : ndarray
|
876
|
+
Initial coordinates and momenta (1D arrays).
|
877
|
+
total_time : float
|
878
|
+
Total integration time.
|
879
|
+
parameters : array-like, optional
|
880
|
+
System parameters.
|
881
|
+
k : int, default=2
|
882
|
+
Number of deviation vectors to evolve.
|
883
|
+
return_history : bool, default=False
|
884
|
+
If True, return GALI evolution over time.
|
885
|
+
seed : int, default=13
|
886
|
+
Random seed for initializing deviation vectors.
|
887
|
+
threshold : float, default=1e-8
|
888
|
+
Early termination threshold for GALI. If GALI ≤ threshold,
|
889
|
+
integration stops.
|
890
|
+
|
891
|
+
Returns
|
892
|
+
-------
|
893
|
+
gali : ndarray
|
894
|
+
- If `return_history=True`, array of shape (N, 2) with columns
|
895
|
+
[time, GALI].
|
896
|
+
- If `return_history=False`, array of shape (1, 2) with the final
|
897
|
+
[time, GALI].
|
898
|
+
|
899
|
+
Raises
|
900
|
+
------
|
901
|
+
ValueError
|
902
|
+
If Hessians are missing, if `k` ≤ 1, if `total_time` is negative,
|
903
|
+
or if `threshold` ≤ 0.
|
904
|
+
TypeError
|
905
|
+
If input types are invalid (e.g., `k` not an integer).
|
906
|
+
"""
|
907
|
+
if self.__hess_T is None or self.__hess_V is None:
|
908
|
+
raise ValueError(
|
909
|
+
"Hessian functions are required to compute the Lyapunov exponents"
|
910
|
+
)
|
911
|
+
|
912
|
+
q = validate_initial_conditions(
|
913
|
+
q, self.__degrees_of_freedom, allow_ensemble=False
|
914
|
+
)
|
915
|
+
q = q.copy()
|
916
|
+
p = validate_initial_conditions(
|
917
|
+
p, self.__degrees_of_freedom, allow_ensemble=False
|
918
|
+
)
|
919
|
+
p = p.copy()
|
920
|
+
|
921
|
+
validate_non_negative(total_time, "total time", type_=Real)
|
922
|
+
|
923
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
924
|
+
|
925
|
+
validate_non_negative(k, "k", Integral)
|
926
|
+
if k <= 1 or k > 2 * self.__degrees_of_freedom:
|
927
|
+
raise ValueError("k must be 2 < k < system dimension")
|
928
|
+
|
929
|
+
if not isinstance(return_history, bool):
|
930
|
+
raise TypeError("return_history must be True or False")
|
931
|
+
|
932
|
+
if not isinstance(seed, Integral):
|
933
|
+
raise TypeError("seed must be an integer")
|
934
|
+
|
935
|
+
validate_non_negative(threshold, "threshold", Real)
|
936
|
+
|
937
|
+
result = GALI(
|
938
|
+
q,
|
939
|
+
p,
|
940
|
+
total_time,
|
941
|
+
self.__time_step,
|
942
|
+
parameters,
|
943
|
+
self.__grad_T,
|
944
|
+
self.__grad_V,
|
945
|
+
self.__hess_T,
|
946
|
+
self.__hess_V,
|
947
|
+
k,
|
948
|
+
return_history,
|
949
|
+
seed,
|
950
|
+
self.__integrator_func,
|
951
|
+
self.__tangent_integrator_func,
|
952
|
+
threshold,
|
953
|
+
)
|
954
|
+
|
955
|
+
if return_history:
|
956
|
+
return np.array(result)
|
957
|
+
else:
|
958
|
+
return result[0]
|
959
|
+
|
960
|
+
def recurrence_time_entropy(
|
961
|
+
self,
|
962
|
+
q: Union[NDArray[np.float64], Sequence[float]],
|
963
|
+
p: Union[NDArray[np.float64], Sequence[float]],
|
964
|
+
num_intersections: int,
|
965
|
+
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
966
|
+
section_index: int = 0,
|
967
|
+
section_value: float = 0.0,
|
968
|
+
crossing: int = 1,
|
969
|
+
**kwargs,
|
970
|
+
):
|
971
|
+
"""
|
972
|
+
Compute the recurrence time entropy (RTE) for a Hamiltonian system.
|
973
|
+
|
974
|
+
Parameters
|
975
|
+
----------
|
976
|
+
q, p : Union[NDArray[np.float64], Sequence[float]]
|
977
|
+
Initial coordinates and momenta (1D arrays).
|
978
|
+
num_intersections: int
|
979
|
+
Number of intersections to record in the Poincaré section.
|
980
|
+
parameters : array-like, optional
|
981
|
+
System parameters.
|
982
|
+
section_index : Optional[int]
|
983
|
+
Index of the coordinate to define the Poincaré section (0-based). Only used when map_type="PS".
|
984
|
+
section_value : Optional[float]
|
985
|
+
Value of the coordinate at which the section is defined. Only used when map_type="PS".
|
986
|
+
crossing : Optional[int]
|
987
|
+
Specifies the type of crossing to consider:
|
988
|
+
- 1 : positive crossing (from below to above section_value)
|
989
|
+
- -1 : negative crossing (from above to below section_value)
|
990
|
+
- 0 : all crossings
|
991
|
+
metric: {"supremum", "euclidean", "manhattan"}, default = "supremum"
|
992
|
+
Distance metric used for phase space reconstruction.
|
993
|
+
std_metric: {"supremum", "euclidean", "manhattan"}, default = "supremum"
|
994
|
+
Distance metric used for standard deviation calculation.
|
995
|
+
lmin: int, default = 1
|
996
|
+
Minimum line length to consider in recurrence quantification.
|
997
|
+
threshold: float, default = 0.1
|
998
|
+
Recurrence threshold(relative to data range).
|
999
|
+
threshold_std: bool, default = True
|
1000
|
+
Whether to scale threshold by data standard deviation.
|
1001
|
+
return_final_state: bool, default = False
|
1002
|
+
Whether to return the final system state in results.
|
1003
|
+
return_recmat: bool, default = False
|
1004
|
+
Whether to return the recurrence matrix.
|
1005
|
+
return_p: bool, default = False
|
1006
|
+
Whether to return white vertical line length distribution.
|
1007
|
+
|
1008
|
+
Returns
|
1009
|
+
-------
|
1010
|
+
Union[float, Tuple[float, NDArray[np.float64]]]
|
1011
|
+
- float: RTE value(base case)
|
1012
|
+
- Tuple: (RTE, white_line_distribution) if return_distribution = True
|
1013
|
+
|
1014
|
+
Raises
|
1015
|
+
------
|
1016
|
+
ValueError
|
1017
|
+
- If `q` or `p` are not a 1D array matching the number of degrees of freedom.
|
1018
|
+
- If `parameters` is not `None` and does not match the expected number of parameters.
|
1019
|
+
- If `parameters` is `None` but the system expects parameters.
|
1020
|
+
- If `parameters` is a scalar or array-like but not 1D.
|
1021
|
+
- If `section_index` is negative or ≥ system dimension.
|
1022
|
+
- If `crossing` is not one of {-1, 0, 1}.
|
1023
|
+
TypeError
|
1024
|
+
- If `q` or `p` are not a scalar or array-like type.
|
1025
|
+
- If `parameters` is not a scalar or array-like type.
|
1026
|
+
- If `section_value` is not a real.
|
1027
|
+
- If `crossing` is not an integer.
|
1028
|
+
- If `sampling_time` is not a real number.
|
1029
|
+
|
1030
|
+
Notes
|
1031
|
+
-----
|
1032
|
+
- Higher RTE indicates more complex dynamics
|
1033
|
+
- Set min_recurrence_time = 2 to ignore single-point recurrences
|
1034
|
+
- Implementation follows [1]
|
1035
|
+
|
1036
|
+
References
|
1037
|
+
----------
|
1038
|
+
[1] Sales et al., Chaos 33, 033140 (2023)
|
1039
|
+
"""
|
1040
|
+
q = validate_initial_conditions(
|
1041
|
+
q, self.__degrees_of_freedom, allow_ensemble=False
|
1042
|
+
)
|
1043
|
+
q = q.copy()
|
1044
|
+
p = validate_initial_conditions(
|
1045
|
+
p, self.__degrees_of_freedom, allow_ensemble=False
|
1046
|
+
)
|
1047
|
+
p = p.copy()
|
1048
|
+
|
1049
|
+
validate_non_negative(num_intersections, "num_intersections", type_=Real)
|
1050
|
+
|
1051
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
1052
|
+
|
1053
|
+
validate_non_negative(section_index, "section_index")
|
1054
|
+
if section_index >= 2 * self.__degrees_of_freedom:
|
1055
|
+
raise ValueError(
|
1056
|
+
"section_index must be less or equal to the system dimension"
|
1057
|
+
)
|
1058
|
+
|
1059
|
+
if not isinstance(section_value, Real):
|
1060
|
+
raise TypeError("section_value must be a valid number")
|
1061
|
+
|
1062
|
+
if crossing not in [-1, 0, 1]:
|
1063
|
+
raise ValueError(
|
1064
|
+
"crossing must be either -1, 0, or 1, indicating downward, all crossings, and upward crossings, respectively"
|
1065
|
+
)
|
1066
|
+
|
1067
|
+
return recurrence_time_entropy(
|
1068
|
+
q,
|
1069
|
+
p,
|
1070
|
+
num_intersections,
|
1071
|
+
parameters,
|
1072
|
+
self.__grad_T,
|
1073
|
+
self.__grad_V,
|
1074
|
+
self.__time_step,
|
1075
|
+
self.__integrator_func,
|
1076
|
+
section_index,
|
1077
|
+
section_value,
|
1078
|
+
crossing,
|
1079
|
+
**kwargs,
|
1080
|
+
)
|
1081
|
+
|
1082
|
+
def hurst_exponent(
|
1083
|
+
self,
|
1084
|
+
q: Union[NDArray[np.float64], Sequence[float]],
|
1085
|
+
p: Union[NDArray[np.float64], Sequence[float]],
|
1086
|
+
num_intersections: int,
|
1087
|
+
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
1088
|
+
transient_time: Optional[float] = None,
|
1089
|
+
wmin: int = 2,
|
1090
|
+
section_index: int = 0,
|
1091
|
+
section_value: float = 0.0,
|
1092
|
+
crossing: int = 1,
|
1093
|
+
) -> Union[float, Tuple[float, NDArray[np.float64]]]:
|
1094
|
+
"""
|
1095
|
+
Estimate the Hurst exponent for a system trajectory using the rescaled range (R/S) method.
|
1096
|
+
|
1097
|
+
Parameters
|
1098
|
+
----------
|
1099
|
+
u : NDArray[np.float64]
|
1100
|
+
Initial condition vector of shape (n,).
|
1101
|
+
parameters : Union[None, float, Sequence[np.float64], NDArray[np.float64]], optional
|
1102
|
+
Parameters passed to the mapping function.
|
1103
|
+
total_time : int
|
1104
|
+
Total number of iterations used to generate the trajectory.
|
1105
|
+
transient_time : Optional[int], optional
|
1106
|
+
Number of initial iterations to discard as transient. If `None`, no transient is removed. Default is `None`.
|
1107
|
+
wmin : int, optional
|
1108
|
+
Minimum window size for the rescaled range calculation. Default is 2.
|
1109
|
+
section_index : Optional[int]
|
1110
|
+
Index of the coordinate to define the Poincaré section (0-based). Only used when map_type="PS".
|
1111
|
+
section_value : Optional[float]
|
1112
|
+
Value of the coordinate at which the section is defined. Only used when map_type="PS".
|
1113
|
+
crossing : Optional[int]
|
1114
|
+
Specifies the type of crossing to consider:
|
1115
|
+
- 1 : positive crossing (from below to above section_value)
|
1116
|
+
- -1 : negative crossing (from above to below section_value)
|
1117
|
+
- 0 : all crossings
|
1118
|
+
|
1119
|
+
Returns
|
1120
|
+
-------
|
1121
|
+
NDArray[np.float64]
|
1122
|
+
Estimated Hurst exponents for each dimension of the system (2 * dof).
|
1123
|
+
|
1124
|
+
Raises
|
1125
|
+
------
|
1126
|
+
TypeError
|
1127
|
+
- If `map_type` is not a string.
|
1128
|
+
- If `section_value` is not a real number.
|
1129
|
+
- If `crossing` is not an integer.
|
1130
|
+
ValueError
|
1131
|
+
- If `section_index` is negative or ≥ system dimension.
|
1132
|
+
- If `crossing` is not in {-1, 0, 1}.
|
1133
|
+
- If `wmin` is less than 2 or greater than or equal to `num_intersections // 2`.
|
1134
|
+
|
1135
|
+
Notes
|
1136
|
+
-----
|
1137
|
+
The Hurst exponent is a measure of the long-term memory of a time series:
|
1138
|
+
|
1139
|
+
- H = 0.5 indicates a random walk (no memory).
|
1140
|
+
- H > 0.5 indicates persistent behavior (positive autocorrelation).
|
1141
|
+
- H < 0.5 indicates anti-persistent behavior (negative autocorrelation).
|
1142
|
+
|
1143
|
+
This implementation computes the rescaled range (R/S) for various window sizes and
|
1144
|
+
performs a linear regression in log-log space to estimate the exponent.
|
1145
|
+
|
1146
|
+
The function supports multivariate time series, estimating one Hurst exponent per dimension.
|
1147
|
+
"""
|
1148
|
+
q = validate_initial_conditions(
|
1149
|
+
q, self.__degrees_of_freedom, allow_ensemble=False
|
1150
|
+
)
|
1151
|
+
q = q.copy()
|
1152
|
+
p = validate_initial_conditions(
|
1153
|
+
p, self.__degrees_of_freedom, allow_ensemble=False
|
1154
|
+
)
|
1155
|
+
p = p.copy()
|
1156
|
+
|
1157
|
+
validate_non_negative(num_intersections, "num_intersections", type_=Real)
|
1158
|
+
|
1159
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
1160
|
+
|
1161
|
+
validate_non_negative(section_index, "section_index")
|
1162
|
+
if section_index >= 2 * self.__degrees_of_freedom:
|
1163
|
+
raise ValueError(
|
1164
|
+
"section_index must be less or equal to the system dimension"
|
1165
|
+
)
|
1166
|
+
|
1167
|
+
if not isinstance(section_value, Real):
|
1168
|
+
raise TypeError("section_value must be a valid number")
|
1169
|
+
|
1170
|
+
if crossing not in [-1, 0, 1]:
|
1171
|
+
raise ValueError(
|
1172
|
+
"crossing must be either -1, 0, or 1, indicating downward, all crossings, and upward crossings, respectively"
|
1173
|
+
)
|
1174
|
+
|
1175
|
+
if wmin < 2 or wmin >= num_intersections // 2:
|
1176
|
+
raise ValueError(
|
1177
|
+
f"`wmin` must be an integer >= 2 and <= total_time / 2. Got {wmin}."
|
1178
|
+
)
|
1179
|
+
|
1180
|
+
return hurst_exponent_wrapped(
|
1181
|
+
q,
|
1182
|
+
p,
|
1183
|
+
num_intersections,
|
1184
|
+
parameters,
|
1185
|
+
self.__grad_T,
|
1186
|
+
self.__grad_V,
|
1187
|
+
self.__time_step,
|
1188
|
+
self.__integrator_func,
|
1189
|
+
section_index,
|
1190
|
+
section_value,
|
1191
|
+
crossing,
|
1192
|
+
wmin,
|
1193
|
+
)
|