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.
- pynamicalsys/__init__.py +2 -0
- pynamicalsys/__version__.py +2 -2
- pynamicalsys/common/recurrence_quantification_analysis.py +1 -1
- pynamicalsys/common/time_series_metrics.py +85 -0
- pynamicalsys/common/utils.py +3 -3
- pynamicalsys/continuous_time/chaotic_indicators.py +306 -8
- pynamicalsys/continuous_time/models.py +25 -0
- pynamicalsys/continuous_time/numerical_integrators.py +7 -7
- pynamicalsys/continuous_time/trajectory_analysis.py +460 -13
- 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 +13 -102
- pynamicalsys/discrete_time/models.py +2 -2
- pynamicalsys/discrete_time/trajectory_analysis.py +10 -10
- pynamicalsys/discrete_time/transport.py +1 -1
- 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.0.dist-info → pynamicalsys-1.4.0.dist-info}/METADATA +37 -8
- pynamicalsys-1.4.0.dist-info/RECORD +36 -0
- pynamicalsys-1.3.0.dist-info/RECORD +0 -28
- {pynamicalsys-1.3.0.dist-info → pynamicalsys-1.4.0.dist-info}/WHEEL +0 -0
- {pynamicalsys-1.3.0.dist-info → pynamicalsys-1.4.0.dist-info}/top_level.txt +0 -0
@@ -16,18 +16,24 @@
|
|
16
16
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
17
17
|
|
18
18
|
from numbers import Integral, Real
|
19
|
-
from typing import Any, Callable, Dict, List, Optional, Sequence, Union
|
19
|
+
from typing import Any, Callable, Dict, List, Optional, Sequence, Union, Tuple
|
20
|
+
from IPython.display import Math
|
20
21
|
|
21
22
|
import numpy as np
|
22
23
|
from numpy.typing import NDArray
|
23
24
|
|
24
25
|
from pynamicalsys.common.utils import householder_qr, qr
|
26
|
+
|
25
27
|
from pynamicalsys.continuous_time.chaotic_indicators import (
|
26
28
|
LDI,
|
27
29
|
SALI,
|
28
30
|
GALI,
|
29
31
|
lyapunov_exponents,
|
32
|
+
maximum_lyapunov_exponent,
|
33
|
+
recurrence_time_entropy,
|
34
|
+
hurst_exponent_wrapped,
|
30
35
|
)
|
36
|
+
|
31
37
|
from pynamicalsys.continuous_time.models import (
|
32
38
|
henon_heiles,
|
33
39
|
henon_heiles_jacobian,
|
@@ -37,17 +43,30 @@ from pynamicalsys.continuous_time.models import (
|
|
37
43
|
rossler_system_4D,
|
38
44
|
rossler_system_4D_jacobian,
|
39
45
|
rossler_system_jacobian,
|
46
|
+
duffing,
|
47
|
+
duffing_jacobian,
|
40
48
|
)
|
49
|
+
|
41
50
|
from pynamicalsys.continuous_time.numerical_integrators import (
|
42
51
|
estimate_initial_step,
|
43
52
|
rk4_step_wrapped,
|
44
53
|
rk45_step_wrapped,
|
45
54
|
)
|
55
|
+
|
46
56
|
from pynamicalsys.continuous_time.trajectory_analysis import (
|
47
|
-
ensemble_trajectories,
|
48
57
|
evolve_system,
|
58
|
+
generate_maxima_map,
|
49
59
|
generate_trajectory,
|
60
|
+
ensemble_trajectories,
|
61
|
+
generate_poincare_section,
|
62
|
+
ensemble_poincare_section,
|
63
|
+
generate_stroboscopic_map,
|
64
|
+
ensemble_stroboscopic_map,
|
65
|
+
generate_maxima_map,
|
66
|
+
ensemble_maxima_map,
|
67
|
+
basin_of_attraction,
|
50
68
|
)
|
69
|
+
|
51
70
|
from pynamicalsys.continuous_time.validators import (
|
52
71
|
validate_initial_conditions,
|
53
72
|
validate_non_negative,
|
@@ -102,6 +121,15 @@ class ContinuousDynamicalSystem:
|
|
102
121
|
__AVAILABLE_MODELS: Dict[str, Dict[str, Any]] = {
|
103
122
|
"lorenz system": {
|
104
123
|
"description": "3D Lorenz system",
|
124
|
+
"equation": Math(
|
125
|
+
r"""
|
126
|
+
\dot{x} = \sigma (y - x), \quad
|
127
|
+
\dot{y} = x (\rho - z) - y, \quad
|
128
|
+
\dot{z} = xy - \beta z
|
129
|
+
"""
|
130
|
+
),
|
131
|
+
"equation_readable": "x' = σ(y − x), y' = x(ρ − z) − y, z' = xy − βz",
|
132
|
+
"notes": "Classic Lorenz 1963 model of atmospheric convection. Exhibits chaotic dynamics for some parameter values.",
|
105
133
|
"has_jacobian": True,
|
106
134
|
"has_variational_equations": True,
|
107
135
|
"equations_of_motion": lorenz_system,
|
@@ -111,7 +139,16 @@ class ContinuousDynamicalSystem:
|
|
111
139
|
"parameters": ["sigma", "rho", "beta"],
|
112
140
|
},
|
113
141
|
"henon heiles": {
|
114
|
-
"description": "Two d.o.f. Hénon
|
142
|
+
"description": "Two d.o.f. Hénon–Heiles system",
|
143
|
+
"equation": Math(
|
144
|
+
r"""
|
145
|
+
H = \frac{1}{2}(p_x^2 + p_y^2) +
|
146
|
+
\frac{1}{2}(x^2 + y^2) +
|
147
|
+
x^2 y - \frac{1}{3}y^3
|
148
|
+
"""
|
149
|
+
),
|
150
|
+
"equation_readable": "H = ½(pₓ² + pᵧ²) + ½(x² + y²) + x²y − y³/3",
|
151
|
+
"notes": "Hamiltonian system modeling stellar motion near a galactic center; classic example of a mixed chaotic/regular system.",
|
115
152
|
"has_jacobian": True,
|
116
153
|
"has_variational_equations": True,
|
117
154
|
"equations_of_motion": henon_heiles,
|
@@ -122,6 +159,15 @@ class ContinuousDynamicalSystem:
|
|
122
159
|
},
|
123
160
|
"rossler system": {
|
124
161
|
"description": "3D Rössler system",
|
162
|
+
"equation": Math(
|
163
|
+
r"""
|
164
|
+
\dot{x} = -y - z, \quad
|
165
|
+
\dot{y} = x + a y, \quad
|
166
|
+
\dot{z} = b + z(x - c)
|
167
|
+
"""
|
168
|
+
),
|
169
|
+
"equation_readable": "x' = −y − z, y' = x + a y, z' = b + z(x − c)",
|
170
|
+
"notes": "Continuous-time chaotic system proposed by Otto Rössler (1976).",
|
125
171
|
"has_jacobian": True,
|
126
172
|
"has_variational_equations": True,
|
127
173
|
"equations_of_motion": rossler_system,
|
@@ -132,6 +178,16 @@ class ContinuousDynamicalSystem:
|
|
132
178
|
},
|
133
179
|
"4d rossler system": {
|
134
180
|
"description": "4D Rössler system",
|
181
|
+
"equation": Math(
|
182
|
+
r"""
|
183
|
+
\dot{x} = -y - z, \quad
|
184
|
+
\dot{y} = x + a y + w, \quad
|
185
|
+
\dot{z} = b + z(x - c), \quad
|
186
|
+
\dot{w} = -d y
|
187
|
+
"""
|
188
|
+
),
|
189
|
+
"equation_readable": "x' = −y − z, y' = x + a y + w, z' = b + z(x − c), w' = −d y",
|
190
|
+
"notes": "A 4D generalization of the Rössler attractor with an added variable w.",
|
135
191
|
"has_jacobian": True,
|
136
192
|
"has_variational_equations": True,
|
137
193
|
"equations_of_motion": rossler_system_4D,
|
@@ -140,6 +196,21 @@ class ContinuousDynamicalSystem:
|
|
140
196
|
"number_of_parameters": 4,
|
141
197
|
"parameters": ["a", "b", "c", "d"],
|
142
198
|
},
|
199
|
+
"duffing": {
|
200
|
+
"description": "Duffing oscillator (nonlinear forced damped oscillator)",
|
201
|
+
"equation": Math(
|
202
|
+
r"\ddot{x} + \delta \dot{x} - \alpha x + \beta x^3 = \gamma \cos(\omega t)"
|
203
|
+
),
|
204
|
+
"equation_readable": "x'' + δ x' − α x + β x³ = γ cos(ω t)",
|
205
|
+
"notes": "A nonlinear oscillator with a double-well potential, forced and damped; exhibits chaos under some parameters.",
|
206
|
+
"has_jacobian": True,
|
207
|
+
"has_variational_equations": True,
|
208
|
+
"equations_of_motion": duffing,
|
209
|
+
"jacobian": duffing_jacobian,
|
210
|
+
"dimension": 2,
|
211
|
+
"number_of_parameters": 5,
|
212
|
+
"parameters": ["delta", "alpha", "beta", "gamma", "omega"],
|
213
|
+
},
|
143
214
|
}
|
144
215
|
|
145
216
|
__AVAILABLE_INTEGRATORS: Dict[str, Dict[str, Any]] = {
|
@@ -325,7 +396,7 @@ class ContinuousDynamicalSystem:
|
|
325
396
|
|
326
397
|
def evolve_system(
|
327
398
|
self,
|
328
|
-
u: NDArray[np.float64],
|
399
|
+
u: Union[NDArray[np.float64], Sequence[float]],
|
329
400
|
total_time: float,
|
330
401
|
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
331
402
|
) -> NDArray[np.float64]:
|
@@ -334,7 +405,7 @@ class ContinuousDynamicalSystem:
|
|
334
405
|
|
335
406
|
Parameters
|
336
407
|
----------
|
337
|
-
u : NDArray[np.float64]
|
408
|
+
u : Union[NDArray[np.float64], Sequence[float]]
|
338
409
|
Initial conditions of the system. Must match the system's dimension.
|
339
410
|
total_time : float
|
340
411
|
Total time over which to evolve the system.
|
@@ -392,7 +463,7 @@ class ContinuousDynamicalSystem:
|
|
392
463
|
|
393
464
|
def trajectory(
|
394
465
|
self,
|
395
|
-
u: NDArray[np.float64],
|
466
|
+
u: Union[NDArray[np.float64], Sequence[float]],
|
396
467
|
total_time: float,
|
397
468
|
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
398
469
|
transient_time: Optional[float] = None,
|
@@ -402,7 +473,7 @@ class ContinuousDynamicalSystem:
|
|
402
473
|
|
403
474
|
Parameters
|
404
475
|
----------
|
405
|
-
u : NDArray[np.float64]
|
476
|
+
u : Union[NDArray[np.float64], Sequence[float]]
|
406
477
|
Initial conditions of the system. Must match the system's dimension.
|
407
478
|
total_time : float
|
408
479
|
Total time over which to evolve the system (including transient).
|
@@ -432,7 +503,7 @@ class ContinuousDynamicalSystem:
|
|
432
503
|
--------
|
433
504
|
>>> from pynamicalsys import ContinuousDynamicalSystem as cds
|
434
505
|
>>> ds = cds(model="lorenz system")
|
435
|
-
>>> u = [0.1, 0.1, 0.1]
|
506
|
+
>>> u = [0.1, 0.1, 0.1] # Initial condition
|
436
507
|
>>> parameters = [10, 28, 8/3]
|
437
508
|
>>> total_time = 700
|
438
509
|
>>> transient_time = 500
|
@@ -480,9 +551,482 @@ class ContinuousDynamicalSystem:
|
|
480
551
|
integrator=self.__integrator_func,
|
481
552
|
)
|
482
553
|
|
554
|
+
def poincare_section(
|
555
|
+
self,
|
556
|
+
u: Union[NDArray[np.float64], Sequence[float]],
|
557
|
+
num_intersections: int,
|
558
|
+
section_index: int,
|
559
|
+
section_value: float,
|
560
|
+
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
561
|
+
transient_time: Optional[float] = None,
|
562
|
+
crossing: int = 1,
|
563
|
+
) -> NDArray[np.float64]:
|
564
|
+
"""
|
565
|
+
Compute the Poincaré section of the dynamical system for given initial conditions.
|
566
|
+
|
567
|
+
A Poincaré section records the points where a trajectory intersects a chosen hypersurface
|
568
|
+
in phase space (e.g. x = constant). This reduces a continuous flow to a lower-dimensional
|
569
|
+
map, making it easier to identify periodic orbits, quasi-periodic motion, or chaotic
|
570
|
+
structures.
|
571
|
+
|
572
|
+
Parameters
|
573
|
+
----------
|
574
|
+
u : Union[NDArray[np.float64], Sequence[float]]
|
575
|
+
Initial conditions of the system. Must match the system's dimension.
|
576
|
+
num_intersections : int
|
577
|
+
Number of intersections to record in the Poincaré section.
|
578
|
+
section_index : int
|
579
|
+
Index of the coordinate to define the Poincaré section (0-based).
|
580
|
+
section_value : float
|
581
|
+
Value of the coordinate at which the section is defined.
|
582
|
+
parameters : Union[None, Sequence[float], NDArray[np.float64]], optional
|
583
|
+
Parameters of the system, by default None. Can be a scalar, a sequence of floats, or a numpy array.
|
584
|
+
transient_time : float, optional
|
585
|
+
Initial time to discard before recording the section.
|
586
|
+
crossing : int, default=1
|
587
|
+
Specifies the type of crossing to consider:
|
588
|
+
- 1 : positive crossing (from below to above section_value)
|
589
|
+
- -1 : negative crossing (from above to below section_value)
|
590
|
+
- 0 : all crossings
|
591
|
+
|
592
|
+
Returns
|
593
|
+
-------
|
594
|
+
result : NDArray[np.float64]
|
595
|
+
The Poincaré section points.
|
596
|
+
|
597
|
+
- For a single initial condition (u.ndim = 1), returns a 2D array of shape
|
598
|
+
(num_intersections, neq), where each row is a system state at a crossing.
|
599
|
+
- For multiple initial conditions (u.ndim = 2), returns a 3D array of shape
|
600
|
+
(num_ic, num_intersections, neq).
|
601
|
+
|
602
|
+
Raises
|
603
|
+
------
|
604
|
+
ValueError
|
605
|
+
- If the initial condition dimension does not match the system dimension.
|
606
|
+
- If the number of parameters does not match the system.
|
607
|
+
- If section_index is larger than the system dimension.
|
608
|
+
TypeError
|
609
|
+
- If `section_value` is not a real number.
|
610
|
+
- If `num_intersections` or `transient_time` are not valid numbers.
|
611
|
+
|
612
|
+
Examples
|
613
|
+
--------
|
614
|
+
>>> from pynamicalsys import ContinuousDynamicalSystem as cds
|
615
|
+
>>> ds = cds(model="lorenz system")
|
616
|
+
>>> u = [0.1, 0.1, 0.1] # Initial condition
|
617
|
+
>>> parameters = [10, 28, 8/3]
|
618
|
+
>>> num_intersections = 500
|
619
|
+
>>> section_index = 2
|
620
|
+
>>> section_value = 25.0
|
621
|
+
>>> ps = ds.poincare_section(u, num_intersections, section_index, section_value, parameters=parameters)
|
622
|
+
(500, 3)
|
623
|
+
>>> u = [[0.1, 0.1, 0.1],
|
624
|
+
... [0.2, 0.2, 0.2]] # Two initial conditions
|
625
|
+
>>> ps_ensemble = ds.poincare_section(u, num_intersections, section_index, section_value, parameters=parameters)
|
626
|
+
(2, 500, 3)
|
627
|
+
"""
|
628
|
+
|
629
|
+
u = validate_initial_conditions(u, self.__system_dimension)
|
630
|
+
u = u.copy()
|
631
|
+
|
632
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
633
|
+
|
634
|
+
validate_non_negative(num_intersections, "num_intersections", Integral)
|
635
|
+
|
636
|
+
validate_non_negative(section_index, "section_index", Integral)
|
637
|
+
if section_index > self.__system_dimension:
|
638
|
+
raise ValueError("section_index must be smaller than the sustem_dimension")
|
639
|
+
|
640
|
+
if not isinstance(section_value, Real):
|
641
|
+
raise TypeError("section_value must be a valid real number")
|
642
|
+
|
643
|
+
if transient_time is not None:
|
644
|
+
validate_non_negative(transient_time, "transient_time", Real)
|
645
|
+
|
646
|
+
if not isinstance(crossing, Integral):
|
647
|
+
raise TypeError("crossing must be an integer number")
|
648
|
+
elif crossing not in [-1, 0, 1]:
|
649
|
+
raise ValueError(
|
650
|
+
"crossing must be -1 (downward crossings), 0 (all crossings), or 1 (upward crossing)"
|
651
|
+
)
|
652
|
+
|
653
|
+
time_step = self.__get_initial_time_step(u, parameters)
|
654
|
+
|
655
|
+
if u.ndim == 1:
|
656
|
+
return generate_poincare_section(
|
657
|
+
u,
|
658
|
+
parameters,
|
659
|
+
num_intersections,
|
660
|
+
self.__equations_of_motion,
|
661
|
+
transient_time,
|
662
|
+
time_step,
|
663
|
+
self.__atol,
|
664
|
+
self.__rtol,
|
665
|
+
self.__integrator_func,
|
666
|
+
section_index,
|
667
|
+
section_value,
|
668
|
+
crossing,
|
669
|
+
)
|
670
|
+
else:
|
671
|
+
return ensemble_poincare_section(
|
672
|
+
u,
|
673
|
+
parameters,
|
674
|
+
num_intersections,
|
675
|
+
self.__equations_of_motion,
|
676
|
+
transient_time,
|
677
|
+
time_step,
|
678
|
+
self.__atol,
|
679
|
+
self.__rtol,
|
680
|
+
self.__integrator_func,
|
681
|
+
section_index,
|
682
|
+
section_value,
|
683
|
+
crossing,
|
684
|
+
)
|
685
|
+
|
686
|
+
def stroboscopic_map(
|
687
|
+
self,
|
688
|
+
u: Union[NDArray[np.float64], Sequence[float]],
|
689
|
+
num_samples: int,
|
690
|
+
sampling_time: float,
|
691
|
+
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
692
|
+
transient_time: Optional[float] = None,
|
693
|
+
) -> NDArray[np.float64]:
|
694
|
+
"""
|
695
|
+
Compute the stroboscopic map of the dynamical system for given initial conditions.
|
696
|
+
|
697
|
+
A stroboscopic map samples the state of a time-periodic or driven system at fixed time
|
698
|
+
intervals (typically one driving period). This converts the continuous-time dynamics
|
699
|
+
into a discrete-time sequence that highlights periodicity, phase locking, and
|
700
|
+
bifurcations.
|
701
|
+
|
702
|
+
Parameters
|
703
|
+
----------
|
704
|
+
u : Union[NDArray[np.float64], Sequence[float]]
|
705
|
+
Initial conditions of the system. Must match the system's dimension.
|
706
|
+
num_samples : int
|
707
|
+
Number of samples to record in the stroboscopic map.
|
708
|
+
sampling_time : float
|
709
|
+
Time interval between consecutive samples.
|
710
|
+
parameters : Union[None, Sequence[float], NDArray[np.float64]], optional
|
711
|
+
Parameters of the system, by default None. Can be a scalar, a sequence of floats, or a numpy array.
|
712
|
+
transient_time : float, optional
|
713
|
+
Initial time to discard before recording the map.
|
714
|
+
|
715
|
+
Returns
|
716
|
+
-------
|
717
|
+
result : NDArray[np.float64]
|
718
|
+
The stroboscopic map points.
|
719
|
+
|
720
|
+
- For a single initial condition (u.ndim = 1), returns a 2D array of shape
|
721
|
+
(num_samples, neq + 1), where the first column is the time and the remaining
|
722
|
+
columns are the system coordinates at each sampled time.
|
723
|
+
- For multiple initial conditions (u.ndim = 2), returns a 3D array of shape
|
724
|
+
(num_ic, num_samples, neq + 1).
|
725
|
+
|
726
|
+
Raises
|
727
|
+
------
|
728
|
+
ValueError
|
729
|
+
- If the initial condition dimension does not match the system dimension.
|
730
|
+
- If the number of parameters does not match the system.
|
731
|
+
TypeError
|
732
|
+
- If `num_samples` or `sampling_time` are not valid numbers.
|
733
|
+
- If `transient_time` is provided and is not a valid number.
|
734
|
+
|
735
|
+
Examples
|
736
|
+
--------
|
737
|
+
>>> from pynamicalsys import ContinuousDynamicalSystem as cds
|
738
|
+
>>> ds = cds(model="lorenz system")
|
739
|
+
>>> u = [0.1, 0.1, 0.1] # Initial condition
|
740
|
+
>>> parameters = [10, 28, 8/3]
|
741
|
+
>>> num_samples = 500
|
742
|
+
>>> sampling_time = 0.1
|
743
|
+
>>> smap = ds.stroboscopic_map(u, num_samples, sampling_time, parameters=parameters)
|
744
|
+
(500, 4)
|
745
|
+
>>> u = [[0.1, 0.1, 0.1],
|
746
|
+
... [0.2, 0.2, 0.2]] # Two initial conditions
|
747
|
+
>>> smap_ensemble = ds.stroboscopic_map(u, num_samples, sampling_time, parameters=parameters)
|
748
|
+
(2, 500, 4)
|
749
|
+
"""
|
750
|
+
|
751
|
+
u = validate_initial_conditions(u, self.__system_dimension)
|
752
|
+
u = u.copy()
|
753
|
+
|
754
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
755
|
+
|
756
|
+
validate_non_negative(num_samples, "num_samples", Integral)
|
757
|
+
|
758
|
+
validate_non_negative(sampling_time, "sampling_time", Real)
|
759
|
+
|
760
|
+
if transient_time is not None:
|
761
|
+
validate_non_negative(transient_time, "transient_time", Real)
|
762
|
+
|
763
|
+
time_step = self.__get_initial_time_step(u, parameters)
|
764
|
+
|
765
|
+
if u.ndim == 1:
|
766
|
+
return generate_stroboscopic_map(
|
767
|
+
u,
|
768
|
+
parameters,
|
769
|
+
num_samples,
|
770
|
+
sampling_time,
|
771
|
+
self.__equations_of_motion,
|
772
|
+
transient_time,
|
773
|
+
time_step,
|
774
|
+
self.__atol,
|
775
|
+
self.__rtol,
|
776
|
+
self.__integrator_func,
|
777
|
+
)
|
778
|
+
else:
|
779
|
+
return ensemble_stroboscopic_map(
|
780
|
+
u,
|
781
|
+
parameters,
|
782
|
+
num_samples,
|
783
|
+
sampling_time,
|
784
|
+
self.__equations_of_motion,
|
785
|
+
transient_time,
|
786
|
+
time_step,
|
787
|
+
self.__atol,
|
788
|
+
self.__rtol,
|
789
|
+
self.__integrator_func,
|
790
|
+
)
|
791
|
+
|
792
|
+
def maxima_map(
|
793
|
+
self,
|
794
|
+
u: Union[NDArray[np.float64], Sequence[float]],
|
795
|
+
num_points: int,
|
796
|
+
maxima_index: int,
|
797
|
+
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
798
|
+
transient_time: Optional[float] = None,
|
799
|
+
) -> NDArray[np.float64]:
|
800
|
+
"""
|
801
|
+
Compute the maxima map of the dynamical system for given initial conditions.
|
802
|
+
|
803
|
+
A maxima map records the local maxima of a chosen system variable along the trajectory.
|
804
|
+
By plotting successive maxima, one obtains a discrete return map that reveals
|
805
|
+
oscillation amplitudes, period-doubling cascades, and other nonlinear behaviours.
|
806
|
+
|
807
|
+
Parameters
|
808
|
+
----------
|
809
|
+
u : Union[NDArray[np.float64], Sequence[float]]
|
810
|
+
Initial conditions of the system. Must match the system's dimension.
|
811
|
+
num_points : int
|
812
|
+
Number of points to record in the maxima map.
|
813
|
+
maxima_index : int
|
814
|
+
Index of the variable whose maxima are to be recorded.
|
815
|
+
parameters : Union[None, Sequence[float], NDArray[np.float64]], optional
|
816
|
+
Parameters of the system, by default None. Can be a scalar, a sequence of floats, or a numpy array.
|
817
|
+
transient_time : float, optional
|
818
|
+
Initial time to discard before recording the map.
|
819
|
+
|
820
|
+
Returns
|
821
|
+
-------
|
822
|
+
result : NDArray[np.float64]
|
823
|
+
The maxima map points.
|
824
|
+
|
825
|
+
- For a single initial condition (u.ndim = 1), returns a 2D array of shape
|
826
|
+
(num_points, neq + 1), where the first column is the time and the remaining
|
827
|
+
columns are the system coordinates at each maxima point.
|
828
|
+
- For multiple initial conditions (u.ndim = 2), returns a 3D array of shape
|
829
|
+
(num_ic, num_points, neq + 1).
|
830
|
+
|
831
|
+
Raises
|
832
|
+
------
|
833
|
+
ValueError
|
834
|
+
- If the initial condition dimension does not match the system dimension.
|
835
|
+
- If the number of parameters does not match the system.
|
836
|
+
TypeError
|
837
|
+
- If `num_points` or `maxima_index` are not valid numbers.
|
838
|
+
- If `transient_time` is provided and is not a valid number.
|
839
|
+
|
840
|
+
Examples
|
841
|
+
--------
|
842
|
+
>>> from pynamicalsys import ContinuousDynamicalSystem as cds
|
843
|
+
>>> ds = cds(model="lorenz system")
|
844
|
+
>>> u = [0.1, 0.1, 0.1] # Initial condition
|
845
|
+
>>> parameters = [10, 28, 8/3]
|
846
|
+
>>> num_points = 500
|
847
|
+
>>> maxima_index = 0
|
848
|
+
>>> smap = ds.maxima_map(u, num_points, maxima_index, parameters=parameters)
|
849
|
+
>>> smap.shape
|
850
|
+
(500, 4)
|
851
|
+
>>> u = [[0.1, 0.1, 0.1],
|
852
|
+
... [0.2, 0.2, 0.2]] # Two initial conditions
|
853
|
+
>>> smap_ensemble = ds.stroboscopic_map(u, num_samples, sampling_time, parameters=parameters)
|
854
|
+
>>> smap_ensemble.shape
|
855
|
+
(2, 500, 4)
|
856
|
+
"""
|
857
|
+
|
858
|
+
u = validate_initial_conditions(u, self.__system_dimension)
|
859
|
+
u = u.copy()
|
860
|
+
|
861
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
862
|
+
|
863
|
+
validate_non_negative(num_points, "num_samples", Integral)
|
864
|
+
|
865
|
+
validate_non_negative(maxima_index, "maxima_index", Integral)
|
866
|
+
|
867
|
+
if transient_time is not None:
|
868
|
+
validate_non_negative(transient_time, "transient_time", Real)
|
869
|
+
|
870
|
+
time_step = self.__get_initial_time_step(u, parameters)
|
871
|
+
|
872
|
+
if u.ndim == 1:
|
873
|
+
return generate_maxima_map(
|
874
|
+
u,
|
875
|
+
parameters,
|
876
|
+
num_points,
|
877
|
+
maxima_index,
|
878
|
+
self.__equations_of_motion,
|
879
|
+
transient_time,
|
880
|
+
time_step,
|
881
|
+
self.__atol,
|
882
|
+
self.__rtol,
|
883
|
+
self.__integrator_func,
|
884
|
+
)
|
885
|
+
else:
|
886
|
+
return ensemble_maxima_map(
|
887
|
+
u,
|
888
|
+
parameters,
|
889
|
+
num_points,
|
890
|
+
maxima_index,
|
891
|
+
self.__equations_of_motion,
|
892
|
+
transient_time,
|
893
|
+
time_step,
|
894
|
+
self.__atol,
|
895
|
+
self.__rtol,
|
896
|
+
self.__integrator_func,
|
897
|
+
)
|
898
|
+
|
899
|
+
def basin_of_attraction(
|
900
|
+
self,
|
901
|
+
u: Union[NDArray[np.float64], Sequence[float]],
|
902
|
+
num_intersections: int,
|
903
|
+
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
904
|
+
transient_time: Optional[float] = None,
|
905
|
+
map_type: str = "SM",
|
906
|
+
section_index: Optional[int] = None,
|
907
|
+
section_value: Optional[float] = None,
|
908
|
+
crossing: Optional[int] = None,
|
909
|
+
sampling_time: Optional[float] = None,
|
910
|
+
eps: float = 0.05,
|
911
|
+
min_samples: int = 1,
|
912
|
+
) -> NDArray[np.int32]:
|
913
|
+
"""
|
914
|
+
Compute the basin of attraction for a dynamical system for a set of initial conditions.
|
915
|
+
|
916
|
+
Parameters
|
917
|
+
----------
|
918
|
+
u : Union[NDArray[np.float64], Sequence[float]]
|
919
|
+
Initial conditions for the dynamical system.
|
920
|
+
num_intersections : int
|
921
|
+
Number of intersections (or samples) to use in constructing the map (stroboscopic or Poincaré).
|
922
|
+
parameters : Union[None, Sequence[float], NDArray[np.float64]], optional
|
923
|
+
System parameters. If None, defaults will be used. Default is None.
|
924
|
+
transient_time : float, optional
|
925
|
+
Transient time to discard before analyzing the trajectories. Default is None.
|
926
|
+
map_type : str, default "SM"
|
927
|
+
Type of map to compute:
|
928
|
+
- "SM" : stroboscopic map
|
929
|
+
- "PS" : Poincaré section
|
930
|
+
section_index : int, optional
|
931
|
+
Index of the coordinate used for the Poincaré section (required if map_type="PS").
|
932
|
+
section_value : float, optional
|
933
|
+
Value of the section plane (required if map_type="PS").
|
934
|
+
crossing : int, optional
|
935
|
+
Crossing direction for Poincaré section:
|
936
|
+
- -1 : downward crossings
|
937
|
+
- 0 : all crossings
|
938
|
+
- 1 : upward crossings
|
939
|
+
Required if map_type="PS".
|
940
|
+
sampling_time : float, optional
|
941
|
+
Sampling time for stroboscopic map (required if map_type="SM").
|
942
|
+
eps : float, default 0.05
|
943
|
+
The maximum distance between points to be considered in the same cluster (DBSCAN parameter).
|
944
|
+
min_samples : int, default 1
|
945
|
+
The minimum number of points to form a cluster (DBSCAN parameter).
|
946
|
+
|
947
|
+
Returns
|
948
|
+
-------
|
949
|
+
NDArray[np.int32]
|
950
|
+
Array of integer labels indicating which attractor each initial condition belongs to.
|
951
|
+
Label `-1` indicates points classified as noise (not part of any attractor).
|
952
|
+
|
953
|
+
Notes
|
954
|
+
-----
|
955
|
+
The basin of attraction is determined by first constructing either a stroboscopic map
|
956
|
+
or a Poincaré section from the trajectories. Then, the attractors are identified by
|
957
|
+
clustering the trajectory centroids using the DBSCAN algorithm from scikit-learn.
|
958
|
+
|
959
|
+
DBSCAN groups points that are close to each other in phase space, with `eps` defining
|
960
|
+
the neighborhood radius and `min_samples` specifying the minimum number of points to
|
961
|
+
form a cluster. Each cluster corresponds to a distinct attractor, and initial conditions
|
962
|
+
whose trajectories end up in the same cluster are considered to belong to the same basin
|
963
|
+
of attraction.
|
964
|
+
"""
|
965
|
+
u = validate_initial_conditions(u, self.__system_dimension)
|
966
|
+
u = u.copy()
|
967
|
+
|
968
|
+
validate_non_negative(num_intersections, "num_intersections", Integral)
|
969
|
+
|
970
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
971
|
+
|
972
|
+
if transient_time is not None:
|
973
|
+
validate_non_negative(transient_time, "transient_time", Real)
|
974
|
+
|
975
|
+
if not isinstance(map_type, str):
|
976
|
+
raise TypeError("map_type must a valid string")
|
977
|
+
if map_type not in ["SM", "PS"]:
|
978
|
+
raise ValueError(
|
979
|
+
"map_type must be either SM (stroboscopic map) or PS (Poicaré section)"
|
980
|
+
)
|
981
|
+
|
982
|
+
if section_index is not None:
|
983
|
+
validate_non_negative(section_index, "section_index", Integral)
|
984
|
+
if section_index > self.__system_dimension:
|
985
|
+
raise ValueError("section_index must be <= system_dimension")
|
986
|
+
|
987
|
+
if section_value is not None:
|
988
|
+
if not isinstance(section_value, Real):
|
989
|
+
raise TypeError("section_value must be a valid real number")
|
990
|
+
|
991
|
+
if crossing is not None:
|
992
|
+
if not isinstance(crossing, Integral):
|
993
|
+
raise TypeError("crossing must be an integer number")
|
994
|
+
elif crossing not in [-1, 0, 1]:
|
995
|
+
raise ValueError(
|
996
|
+
"crossing must be -1 (downward crossings), 0 (all crossings), or 1 (upward crossing)"
|
997
|
+
)
|
998
|
+
|
999
|
+
if sampling_time is not None:
|
1000
|
+
validate_non_negative(sampling_time, "sampling_time", Real)
|
1001
|
+
|
1002
|
+
validate_non_negative(eps, "eps", Real)
|
1003
|
+
|
1004
|
+
validate_non_negative(min_samples, "min_samples", Integral)
|
1005
|
+
|
1006
|
+
time_step = self.__get_initial_time_step(u, parameters)
|
1007
|
+
|
1008
|
+
return basin_of_attraction(
|
1009
|
+
u,
|
1010
|
+
parameters,
|
1011
|
+
num_intersections,
|
1012
|
+
self.__equations_of_motion,
|
1013
|
+
transient_time,
|
1014
|
+
time_step,
|
1015
|
+
self.__atol,
|
1016
|
+
self.__rtol,
|
1017
|
+
self.__integrator_func,
|
1018
|
+
map_type,
|
1019
|
+
section_index,
|
1020
|
+
section_value,
|
1021
|
+
crossing,
|
1022
|
+
sampling_time,
|
1023
|
+
eps,
|
1024
|
+
min_samples,
|
1025
|
+
)
|
1026
|
+
|
483
1027
|
def lyapunov(
|
484
1028
|
self,
|
485
|
-
u: NDArray[np.float64],
|
1029
|
+
u: Union[NDArray[np.float64], Sequence[float]],
|
486
1030
|
total_time: float,
|
487
1031
|
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
488
1032
|
transient_time: Optional[float] = None,
|
@@ -499,7 +1043,7 @@ class ContinuousDynamicalSystem:
|
|
499
1043
|
|
500
1044
|
Parameters
|
501
1045
|
----------
|
502
|
-
u : NDArray[np.float64]
|
1046
|
+
u : Union[NDArray[np.float64], Sequence[float]]
|
503
1047
|
Initial conditions of the system. Must match the system's dimension.
|
504
1048
|
total_time : float
|
505
1049
|
Total time over which to evolve the system (including transient).
|
@@ -603,31 +1147,46 @@ class ContinuousDynamicalSystem:
|
|
603
1147
|
if log_base == 1:
|
604
1148
|
raise ValueError("The logarithm function is not defined with base 1")
|
605
1149
|
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
1150
|
+
if num_exponents == 1:
|
1151
|
+
result = maximum_lyapunov_exponent(
|
1152
|
+
u,
|
1153
|
+
parameters,
|
1154
|
+
total_time,
|
1155
|
+
self.__equations_of_motion,
|
1156
|
+
self.__jacobian,
|
1157
|
+
transient_time,
|
1158
|
+
time_step,
|
1159
|
+
self.__atol,
|
1160
|
+
self.__rtol,
|
1161
|
+
self.__integrator_func,
|
1162
|
+
return_history,
|
1163
|
+
seed,
|
1164
|
+
)
|
1165
|
+
else:
|
1166
|
+
result = lyapunov_exponents(
|
1167
|
+
u,
|
1168
|
+
parameters,
|
1169
|
+
total_time,
|
1170
|
+
self.__equations_of_motion,
|
1171
|
+
self.__jacobian,
|
1172
|
+
num_exponents,
|
1173
|
+
transient_time=transient_time,
|
1174
|
+
time_step=time_step,
|
1175
|
+
atol=self.__atol,
|
1176
|
+
rtol=self.__rtol,
|
1177
|
+
integrator=self.__integrator_func,
|
1178
|
+
return_history=return_history,
|
1179
|
+
seed=seed,
|
1180
|
+
QR=qr_func,
|
1181
|
+
)
|
623
1182
|
if return_history:
|
624
|
-
return np.array(result)
|
1183
|
+
return np.array(result) / np.log(log_base)
|
625
1184
|
else:
|
626
|
-
return np.array(result[0])
|
1185
|
+
return np.array(result[0]) / np.log(log_base)
|
627
1186
|
|
628
1187
|
def SALI(
|
629
1188
|
self,
|
630
|
-
u: NDArray[np.float64],
|
1189
|
+
u: Union[NDArray[np.float64], Sequence[float]],
|
631
1190
|
total_time: float,
|
632
1191
|
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
633
1192
|
transient_time: Optional[float] = None,
|
@@ -640,7 +1199,7 @@ class ContinuousDynamicalSystem:
|
|
640
1199
|
|
641
1200
|
Parameters
|
642
1201
|
----------
|
643
|
-
u : NDArray[np.float64]
|
1202
|
+
u : Union[NDArray[np.float64], Sequence[float]]
|
644
1203
|
Initial conditions of the system. Must match the system's dimension.
|
645
1204
|
total_time : float
|
646
1205
|
Total time over which to evolve the system (including transient).
|
@@ -738,7 +1297,7 @@ class ContinuousDynamicalSystem:
|
|
738
1297
|
|
739
1298
|
def LDI(
|
740
1299
|
self,
|
741
|
-
u: NDArray[np.float64],
|
1300
|
+
u: Union[NDArray[np.float64], Sequence[float]],
|
742
1301
|
total_time: float,
|
743
1302
|
k: int,
|
744
1303
|
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
@@ -752,7 +1311,7 @@ class ContinuousDynamicalSystem:
|
|
752
1311
|
|
753
1312
|
Parameters
|
754
1313
|
----------
|
755
|
-
u : NDArray[np.float64]
|
1314
|
+
u : Union[NDArray[np.float64], Sequence[float]]
|
756
1315
|
Initial conditions of the system. Must match the system's dimension.
|
757
1316
|
total_time : float
|
758
1317
|
Total time over which to evolve the system (including transient).
|
@@ -853,7 +1412,7 @@ class ContinuousDynamicalSystem:
|
|
853
1412
|
|
854
1413
|
def GALI(
|
855
1414
|
self,
|
856
|
-
u: NDArray[np.float64],
|
1415
|
+
u: Union[NDArray[np.float64], Sequence[float]],
|
857
1416
|
total_time: float,
|
858
1417
|
k: int,
|
859
1418
|
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
@@ -867,7 +1426,7 @@ class ContinuousDynamicalSystem:
|
|
867
1426
|
|
868
1427
|
Parameters
|
869
1428
|
----------
|
870
|
-
u : NDArray[np.float64]
|
1429
|
+
u : Union[NDArray[np.float64], Sequence[float]]
|
871
1430
|
Initial conditions of the system. Must match the system's dimension.
|
872
1431
|
total_time : float
|
873
1432
|
Total time over which to evolve the system (including transient).
|
@@ -965,3 +1524,342 @@ class ContinuousDynamicalSystem:
|
|
965
1524
|
return np.array(result)
|
966
1525
|
else:
|
967
1526
|
return np.array(result[0])
|
1527
|
+
|
1528
|
+
def recurrence_time_entropy(
|
1529
|
+
self,
|
1530
|
+
u: Union[NDArray[np.float64], Sequence[float]],
|
1531
|
+
num_intersections: int,
|
1532
|
+
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
1533
|
+
transient_time: Optional[float] = None,
|
1534
|
+
map_type: str = "SM",
|
1535
|
+
section_index: Optional[int] = None,
|
1536
|
+
section_value: Optional[float] = None,
|
1537
|
+
crossing: Optional[int] = None,
|
1538
|
+
sampling_time: Optional[float] = None,
|
1539
|
+
maxima_index: Optional[float] = None,
|
1540
|
+
**kwargs,
|
1541
|
+
) -> Union[float, Tuple[float, NDArray[np.float64]]]:
|
1542
|
+
"""Compute the Recurrence Time Entropy (RTE) for a dynamical system.
|
1543
|
+
|
1544
|
+
Parameters
|
1545
|
+
----------
|
1546
|
+
u: Union[NDArray[np.float64], Sequence[float]]
|
1547
|
+
Initial condition of shape(d,) where d is system dimension
|
1548
|
+
num_intersections: int
|
1549
|
+
Number of intersections to record in the Poincaré section or stroboscopic map.
|
1550
|
+
parameters: Union[None, float, Sequence[np.float64], NDArray[np.float64]], optional
|
1551
|
+
System parameters of shape(p,) passed to mapping function
|
1552
|
+
transient_time : float, optional
|
1553
|
+
Initial time to discard before recording the section.
|
1554
|
+
map_type : str
|
1555
|
+
Which map to use: stroboscopic map or Poincaré section, by default "SM"
|
1556
|
+
section_index : Optional[int]
|
1557
|
+
Index of the coordinate to define the Poincaré section (0-based). Only used when map_type="PS".
|
1558
|
+
section_value : Optional[float]
|
1559
|
+
Value of the coordinate at which the section is defined. Only used when map_type="PS".
|
1560
|
+
crossing : Optional[int]
|
1561
|
+
Specifies the type of crossing to consider:
|
1562
|
+
- 1 : positive crossing (from below to above section_value)
|
1563
|
+
- -1 : negative crossing (from above to below section_value)
|
1564
|
+
- 0 : all crossings
|
1565
|
+
|
1566
|
+
Only used when map_type="PS".
|
1567
|
+
sampling_time : float
|
1568
|
+
Time interval between consecutive samples in the stroboscopic map. Only used when map_type="SM".
|
1569
|
+
maxima_index : Optional[int]
|
1570
|
+
Index of the coordinate whose maxima will be recorded. Only used when map_type="MM".
|
1571
|
+
metric: {"supremum", "euclidean", "manhattan"}, default = "supremum"
|
1572
|
+
Distance metric used for phase space reconstruction.
|
1573
|
+
std_metric: {"supremum", "euclidean", "manhattan"}, default = "supremum"
|
1574
|
+
Distance metric used for standard deviation calculation.
|
1575
|
+
lmin: int, default = 1
|
1576
|
+
Minimum line length to consider in recurrence quantification.
|
1577
|
+
threshold: float, default = 0.1
|
1578
|
+
Recurrence threshold(relative to data range).
|
1579
|
+
threshold_std: bool, default = True
|
1580
|
+
Whether to scale threshold by data standard deviation.
|
1581
|
+
return_final_state: bool, default = False
|
1582
|
+
Whether to return the final system state in results.
|
1583
|
+
return_recmat: bool, default = False
|
1584
|
+
Whether to return the recurrence matrix.
|
1585
|
+
return_p: bool, default = False
|
1586
|
+
Whether to return white vertical line length distribution.
|
1587
|
+
|
1588
|
+
Returns
|
1589
|
+
-------
|
1590
|
+
Union[float, Tuple[float, NDArray[np.float64]]]
|
1591
|
+
- float: RTE value(base case)
|
1592
|
+
- Tuple: (RTE, white_line_distribution) if return_distribution = True
|
1593
|
+
|
1594
|
+
Raises
|
1595
|
+
------
|
1596
|
+
ValueError
|
1597
|
+
- If `u` is not a 1D array matching the system dimension.
|
1598
|
+
- If `parameters` is not `None` and does not match the expected number of parameters.
|
1599
|
+
- If `parameters` is `None` but the system expects parameters.
|
1600
|
+
- If `parameters` is a scalar or array-like but not 1D.
|
1601
|
+
- If `transient_time` is negative.
|
1602
|
+
- If `map_type` is not one of {"SM", "PS"}.
|
1603
|
+
- If `map_type="PS"` but any of `section_index`, `section_value`, or `crossing` is `None`.
|
1604
|
+
- If `section_index` is negative or ≥ system dimension.
|
1605
|
+
- If `crossing` is not one of {-1, 0, 1}.
|
1606
|
+
- If `map_type="SM"` but `sampling_time` is `None` or negative.
|
1607
|
+
TypeError
|
1608
|
+
- If `u` is not a scalar or array-like type.
|
1609
|
+
- If `parameters` is not a scalar or array-like type.
|
1610
|
+
- If `map_type` is not a string.
|
1611
|
+
- If `section_value` is not a real number when `map_type="PS"`.
|
1612
|
+
- If `crossing` is not an integer when `map_type="PS"`.
|
1613
|
+
- If `sampling_time` is not a real number when `map_type="SM"`.
|
1614
|
+
|
1615
|
+
Notes
|
1616
|
+
-----
|
1617
|
+
- Higher RTE indicates more complex dynamics
|
1618
|
+
- Set min_recurrence_time = 2 to ignore single-point recurrences
|
1619
|
+
- Implementation follows [1]
|
1620
|
+
|
1621
|
+
References
|
1622
|
+
----------
|
1623
|
+
[1] Sales et al., Chaos 33, 033140 (2023)
|
1624
|
+
|
1625
|
+
Examples
|
1626
|
+
--------
|
1627
|
+
>>> # Basic usage
|
1628
|
+
>>> rte = system.recurrence_time_entropy(u0, params, 5000)
|
1629
|
+
|
1630
|
+
>>> # With distribution output
|
1631
|
+
>>> rte, dist = system.recurrence_time_entropy(
|
1632
|
+
... u0, params, 5000,
|
1633
|
+
... return_distribution=True,
|
1634
|
+
... recurrence_threshold=0.1
|
1635
|
+
...)
|
1636
|
+
"""
|
1637
|
+
u = validate_initial_conditions(
|
1638
|
+
u, self.__system_dimension, allow_ensemble=False
|
1639
|
+
)
|
1640
|
+
u = u.copy()
|
1641
|
+
|
1642
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
1643
|
+
|
1644
|
+
validate_non_negative(transient_time, "transient_time", Real)
|
1645
|
+
|
1646
|
+
if not isinstance(map_type, str):
|
1647
|
+
raise TypeError("map_type must be a string")
|
1648
|
+
|
1649
|
+
if map_type == "PS":
|
1650
|
+
if section_index is None or section_value is None or crossing is None:
|
1651
|
+
raise ValueError(
|
1652
|
+
'When using map_type="PS", you must inform section_index, section_value, and crossing'
|
1653
|
+
)
|
1654
|
+
|
1655
|
+
validate_non_negative(section_index, "section_index", Integral)
|
1656
|
+
if section_index >= self.__system_dimension:
|
1657
|
+
raise ValueError("section_index must be in [0, system_dimension)")
|
1658
|
+
|
1659
|
+
if not isinstance(section_value, Real):
|
1660
|
+
raise TypeError("section_value must be a valid real number")
|
1661
|
+
|
1662
|
+
if not isinstance(crossing, Integral):
|
1663
|
+
raise TypeError("crossing must be a valid integer number")
|
1664
|
+
elif crossing not in [-1, 0, 1]:
|
1665
|
+
raise ValueError("crossing must be -1, 0, or 1")
|
1666
|
+
|
1667
|
+
elif map_type == "SM":
|
1668
|
+
|
1669
|
+
if sampling_time is not None:
|
1670
|
+
validate_non_negative(sampling_time, "sampling_time", Real)
|
1671
|
+
else:
|
1672
|
+
raise ValueError(
|
1673
|
+
'When using map_type="SM" you must inform sampling_time'
|
1674
|
+
)
|
1675
|
+
elif map_type == "MM":
|
1676
|
+
if maxima_index is not None:
|
1677
|
+
validate_non_negative(maxima_index, "maxima_index", Integral)
|
1678
|
+
else:
|
1679
|
+
raise ValueError(
|
1680
|
+
'When using map_type="MM" you must inform maxima_index'
|
1681
|
+
)
|
1682
|
+
else:
|
1683
|
+
raise ValueError(
|
1684
|
+
"map_type must be SM (stroboscopic map), PS (Poincaré section), or MM (Maxima map)"
|
1685
|
+
)
|
1686
|
+
|
1687
|
+
time_step = self.__get_initial_time_step(u, parameters)
|
1688
|
+
|
1689
|
+
return recurrence_time_entropy(
|
1690
|
+
u,
|
1691
|
+
parameters,
|
1692
|
+
num_intersections,
|
1693
|
+
transient_time,
|
1694
|
+
self.__equations_of_motion,
|
1695
|
+
time_step,
|
1696
|
+
self.__atol,
|
1697
|
+
self.__rtol,
|
1698
|
+
self.__integrator_func,
|
1699
|
+
map_type,
|
1700
|
+
section_index,
|
1701
|
+
section_value,
|
1702
|
+
crossing,
|
1703
|
+
sampling_time,
|
1704
|
+
maxima_index,
|
1705
|
+
**kwargs,
|
1706
|
+
)
|
1707
|
+
|
1708
|
+
def hurst_exponent(
|
1709
|
+
self,
|
1710
|
+
u: Union[NDArray[np.float64], Sequence[float]],
|
1711
|
+
num_intersections: int,
|
1712
|
+
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
1713
|
+
transient_time: Optional[float] = None,
|
1714
|
+
wmin: int = 2,
|
1715
|
+
map_type: str = "SM",
|
1716
|
+
section_index: Optional[int] = None,
|
1717
|
+
section_value: Optional[float] = None,
|
1718
|
+
crossing: Optional[int] = None,
|
1719
|
+
sampling_time: Optional[float] = None,
|
1720
|
+
maxima_index: Optional[float] = None,
|
1721
|
+
) -> Union[float, Tuple[float, NDArray[np.float64]]]:
|
1722
|
+
"""
|
1723
|
+
Estimate the Hurst exponent for a system trajectory using the rescaled range (R/S) method.
|
1724
|
+
|
1725
|
+
Parameters
|
1726
|
+
----------
|
1727
|
+
u : NDArray[np.float64]
|
1728
|
+
Initial condition vector of shape (n,).
|
1729
|
+
parameters : Union[None, float, Sequence[np.float64], NDArray[np.float64]], optional
|
1730
|
+
Parameters passed to the mapping function.
|
1731
|
+
total_time : int
|
1732
|
+
Total number of iterations used to generate the trajectory.
|
1733
|
+
transient_time : Optional[int], optional
|
1734
|
+
Number of initial iterations to discard as transient. If `None`, no transient is removed. Default is `None`.
|
1735
|
+
wmin : int, optional
|
1736
|
+
Minimum window size for the rescaled range calculation. Default is 2.
|
1737
|
+
map_type : str
|
1738
|
+
Which map to use: stroboscopic map or Poincaré section, by default "SM"
|
1739
|
+
section_index : Optional[int]
|
1740
|
+
Index of the coordinate to define the Poincaré section (0-based). Only used when map_type="PS".
|
1741
|
+
section_value : Optional[float]
|
1742
|
+
Value of the coordinate at which the section is defined. Only used when map_type="PS".
|
1743
|
+
crossing : Optional[int]
|
1744
|
+
Specifies the type of crossing to consider:
|
1745
|
+
- 1 : positive crossing (from below to above section_value)
|
1746
|
+
- -1 : negative crossing (from above to below section_value)
|
1747
|
+
- 0 : all crossings
|
1748
|
+
|
1749
|
+
Only used when map_type="PS".
|
1750
|
+
sampling_time : float
|
1751
|
+
Time interval between consecutive samples in the stroboscopic map. Only used when map_type="SM".
|
1752
|
+
maxima_index : Optional[int]
|
1753
|
+
Index of the coordinate whose maxima will be recorded. Only used when map_type="MM".
|
1754
|
+
|
1755
|
+
Returns
|
1756
|
+
-------
|
1757
|
+
NDArray[np.float64]
|
1758
|
+
Estimated Hurst exponents for each dimension of the input vector `u`, of shape (n,).
|
1759
|
+
|
1760
|
+
Raises
|
1761
|
+
------
|
1762
|
+
TypeError
|
1763
|
+
- If `map_type` is not a string.
|
1764
|
+
- If `section_value` is not a real number when `map_type="PS"`.
|
1765
|
+
- If `crossing` is not an integer when `map_type="PS"`.
|
1766
|
+
- If `sampling_time` is not a real number when `map_type="SM"`.
|
1767
|
+
- If `maxima_index` is not an integer when `map_type="MM"`.
|
1768
|
+
ValueError
|
1769
|
+
- If `map_type` is not one of {"SM", "PS", "MM"}.
|
1770
|
+
- If `map_type="PS"` and any of `section_index`, `section_value`, or `crossing` is `None`.
|
1771
|
+
- If `section_index` is negative or ≥ system dimension when `map_type="PS"`.
|
1772
|
+
- If `crossing` is not in {-1, 0, 1} when `map_type="PS"`.
|
1773
|
+
- If `map_type="SM"` and `sampling_time` is `None` or negative.
|
1774
|
+
- If `map_type="MM"` and `maxima_index` is `None` or negative.
|
1775
|
+
- If `transient_time` is negative.
|
1776
|
+
- If `wmin` is less than 2 or greater than or equal to `num_intersections // 2`.
|
1777
|
+
|
1778
|
+
Notes
|
1779
|
+
-----
|
1780
|
+
The Hurst exponent is a measure of the long-term memory of a time series:
|
1781
|
+
|
1782
|
+
- H = 0.5 indicates a random walk (no memory).
|
1783
|
+
- H > 0.5 indicates persistent behavior (positive autocorrelation).
|
1784
|
+
- H < 0.5 indicates anti-persistent behavior (negative autocorrelation).
|
1785
|
+
|
1786
|
+
This implementation computes the rescaled range (R/S) for various window sizes and
|
1787
|
+
performs a linear regression in log-log space to estimate the exponent.
|
1788
|
+
|
1789
|
+
The function supports multivariate time series, estimating one Hurst exponent per dimension.
|
1790
|
+
"""
|
1791
|
+
u = validate_initial_conditions(
|
1792
|
+
u, self.__system_dimension, allow_ensemble=False
|
1793
|
+
)
|
1794
|
+
u = u.copy()
|
1795
|
+
|
1796
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
1797
|
+
|
1798
|
+
validate_non_negative(transient_time, "transient_time", Real)
|
1799
|
+
|
1800
|
+
if not isinstance(map_type, str):
|
1801
|
+
raise TypeError("map_type must be a string")
|
1802
|
+
|
1803
|
+
if map_type == "PS":
|
1804
|
+
if section_index is None or section_value is None or crossing is None:
|
1805
|
+
raise ValueError(
|
1806
|
+
'When using map_type="PS", you must inform section_index, section_value, and crossing'
|
1807
|
+
)
|
1808
|
+
|
1809
|
+
validate_non_negative(section_index, "section_index", Integral)
|
1810
|
+
if section_index >= self.__system_dimension:
|
1811
|
+
raise ValueError("section_index must be in [0, system_dimension)")
|
1812
|
+
|
1813
|
+
if not isinstance(section_value, Real):
|
1814
|
+
raise TypeError("section_value must be a valid real number")
|
1815
|
+
|
1816
|
+
if not isinstance(crossing, Integral):
|
1817
|
+
raise TypeError("crossing must be a valid integer number")
|
1818
|
+
elif crossing not in [-1, 0, 1]:
|
1819
|
+
raise ValueError("crossing must be -1, 0, or 1")
|
1820
|
+
|
1821
|
+
elif map_type == "SM":
|
1822
|
+
|
1823
|
+
if sampling_time is not None:
|
1824
|
+
validate_non_negative(sampling_time, "sampling_time", Real)
|
1825
|
+
else:
|
1826
|
+
raise ValueError(
|
1827
|
+
'When using map_type="SM" you must inform sampling_time'
|
1828
|
+
)
|
1829
|
+
elif map_type == "MM":
|
1830
|
+
if maxima_index is not None:
|
1831
|
+
validate_non_negative(maxima_index, "maxima_index", Integral)
|
1832
|
+
else:
|
1833
|
+
raise ValueError(
|
1834
|
+
'When using map_type="MM" you must inform maxima_index'
|
1835
|
+
)
|
1836
|
+
else:
|
1837
|
+
raise ValueError(
|
1838
|
+
"map_type must be SM (stroboscopic map), PS (Poincaré section), or MM (Maxima map)"
|
1839
|
+
)
|
1840
|
+
|
1841
|
+
if wmin < 2 or wmin >= num_intersections // 2:
|
1842
|
+
raise ValueError(
|
1843
|
+
f"`wmin` must be an integer >= 2 and <= total_time / 2. Got {wmin}."
|
1844
|
+
)
|
1845
|
+
|
1846
|
+
time_step = self.__get_initial_time_step(u, parameters)
|
1847
|
+
|
1848
|
+
return hurst_exponent_wrapped(
|
1849
|
+
u,
|
1850
|
+
parameters,
|
1851
|
+
num_intersections,
|
1852
|
+
self.__equations_of_motion,
|
1853
|
+
time_step,
|
1854
|
+
self.__atol,
|
1855
|
+
self.__rtol,
|
1856
|
+
self.__integrator_func,
|
1857
|
+
map_type,
|
1858
|
+
section_index,
|
1859
|
+
section_value,
|
1860
|
+
crossing,
|
1861
|
+
sampling_time,
|
1862
|
+
maxima_index,
|
1863
|
+
wmin,
|
1864
|
+
transient_time,
|
1865
|
+
)
|