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
pynamicalsys/__init__.py
CHANGED
@@ -17,6 +17,7 @@
|
|
17
17
|
|
18
18
|
from pynamicalsys.core.discrete_dynamical_systems import DiscreteDynamicalSystem
|
19
19
|
from pynamicalsys.core.continuous_dynamical_systems import ContinuousDynamicalSystem
|
20
|
+
from pynamicalsys.core.hamiltonian_systems import HamiltonianSystem
|
20
21
|
from pynamicalsys.core.basin_metrics import BasinMetrics
|
21
22
|
from pynamicalsys.core.plot_styler import PlotStyler
|
22
23
|
from pynamicalsys.core.time_series_metrics import TimeSeriesMetrics
|
@@ -25,6 +26,7 @@ from .__version__ import __version__
|
|
25
26
|
__all__ = [
|
26
27
|
"DiscreteDynamicalSystem",
|
27
28
|
"ContinuousDynamicalSystem",
|
29
|
+
"HamiltonianSystem",
|
28
30
|
"PlotStyler",
|
29
31
|
"TimeSeriesMetrics",
|
30
32
|
"BasinMetrics",
|
pynamicalsys/__version__.py
CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
28
28
|
commit_id: COMMIT_ID
|
29
29
|
__commit_id__: COMMIT_ID
|
30
30
|
|
31
|
-
__version__ = version = '1.
|
32
|
-
__version_tuple__ = version_tuple = (1,
|
31
|
+
__version__ = version = '1.4.0'
|
32
|
+
__version_tuple__ = version_tuple = (1, 4, 0)
|
33
33
|
|
34
34
|
__commit_id__ = commit_id = None
|
@@ -0,0 +1,85 @@
|
|
1
|
+
from typing import Optional, Tuple, Union, Callable
|
2
|
+
from numpy.typing import NDArray
|
3
|
+
import numpy as np
|
4
|
+
from numba import njit, prange
|
5
|
+
|
6
|
+
from pynamicalsys.common.utils import fit_poly
|
7
|
+
|
8
|
+
|
9
|
+
@njit(parallel=True)
|
10
|
+
def hurst_exponent(
|
11
|
+
time_series: NDArray[np.float64],
|
12
|
+
wmin: int = 2,
|
13
|
+
) -> NDArray[np.float64]:
|
14
|
+
|
15
|
+
time_series = time_series.copy()
|
16
|
+
|
17
|
+
sample_size, neq = time_series.shape
|
18
|
+
|
19
|
+
H = np.zeros(neq)
|
20
|
+
|
21
|
+
ells = np.arange(wmin, sample_size // 2)
|
22
|
+
log_ells = np.log(ells)
|
23
|
+
RS = np.empty((ells.shape[0], neq))
|
24
|
+
|
25
|
+
for j in prange(neq):
|
26
|
+
series = time_series[:, j]
|
27
|
+
|
28
|
+
# Precompute cumulative sums and cumulative sums of squares
|
29
|
+
cum_sum = np.zeros(sample_size)
|
30
|
+
cum_sum_sq = np.zeros(sample_size)
|
31
|
+
cum_sum[0] = series[0]
|
32
|
+
cum_sum_sq[0] = series[0] ** 2
|
33
|
+
for t in range(1, sample_size):
|
34
|
+
cum_sum[t] = cum_sum[t - 1] + series[t]
|
35
|
+
cum_sum_sq[t] = cum_sum_sq[t - 1] + series[t] ** 2
|
36
|
+
|
37
|
+
for i, ell in enumerate(ells):
|
38
|
+
num_blocks = sample_size // ell
|
39
|
+
R_over_S = np.zeros(num_blocks)
|
40
|
+
|
41
|
+
for block in range(num_blocks):
|
42
|
+
start = block * ell
|
43
|
+
end = start + ell
|
44
|
+
|
45
|
+
# Mean using cumulative sums
|
46
|
+
block_sum = cum_sum[end - 1] - (cum_sum[start - 1] if start > 0 else 0)
|
47
|
+
block_mean = block_sum / ell
|
48
|
+
|
49
|
+
# Variance using cumulative sums of squares
|
50
|
+
block_sum_sq = cum_sum_sq[end - 1] - (
|
51
|
+
cum_sum_sq[start - 1] if start > 0 else 0
|
52
|
+
)
|
53
|
+
var = block_sum_sq / ell - block_mean**2
|
54
|
+
S = np.sqrt(var) if var > 0 else 0
|
55
|
+
|
56
|
+
# Cumulative sum of mean-adjusted series for range
|
57
|
+
max_Z = 0.0
|
58
|
+
min_Z = 0.0
|
59
|
+
cumsum = 0.0
|
60
|
+
for k in range(start, end):
|
61
|
+
cumsum += series[k] - block_mean
|
62
|
+
if cumsum > max_Z:
|
63
|
+
max_Z = cumsum
|
64
|
+
if cumsum < min_Z:
|
65
|
+
min_Z = cumsum
|
66
|
+
R = max_Z - min_Z
|
67
|
+
|
68
|
+
R_over_S[block] = R / S if S > 0 else 0.0
|
69
|
+
|
70
|
+
positive_mask = R_over_S > 0
|
71
|
+
RS[i, j] = (
|
72
|
+
np.mean(R_over_S[positive_mask]) if np.any(positive_mask) else 0.0
|
73
|
+
)
|
74
|
+
|
75
|
+
# Linear regression in log-log space
|
76
|
+
positive_inds = np.where(RS[:, j] > 0)[0]
|
77
|
+
if positive_inds.size == 0:
|
78
|
+
H[j] = 0.0
|
79
|
+
else:
|
80
|
+
x_fit = log_ells[positive_inds]
|
81
|
+
y_fit = np.log(RS[positive_inds, j])
|
82
|
+
fitting = fit_poly(x_fit, y_fit, 1)
|
83
|
+
H[j] = fitting[0]
|
84
|
+
|
85
|
+
return H
|
@@ -15,17 +15,33 @@
|
|
15
15
|
# You should have received a copy of the GNU General Public License
|
16
16
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
17
17
|
|
18
|
-
from typing import Optional, Callable,
|
18
|
+
from typing import Optional, Callable, Tuple
|
19
19
|
from numpy.typing import NDArray
|
20
20
|
import numpy as np
|
21
21
|
from numba import njit
|
22
22
|
|
23
|
-
from pynamicalsys.common.utils import qr, wedge_norm
|
24
|
-
|
23
|
+
from pynamicalsys.common.utils import qr, wedge_norm, fit_poly
|
24
|
+
|
25
|
+
from pynamicalsys.continuous_time.trajectory_analysis import (
|
26
|
+
generate_maxima_map,
|
27
|
+
step,
|
28
|
+
evolve_system,
|
29
|
+
generate_poincare_section,
|
30
|
+
generate_stroboscopic_map,
|
31
|
+
)
|
32
|
+
|
25
33
|
from pynamicalsys.continuous_time.numerical_integrators import rk4_step_wrapped
|
26
34
|
|
35
|
+
from pynamicalsys.common.recurrence_quantification_analysis import (
|
36
|
+
RTEConfig,
|
37
|
+
recurrence_matrix,
|
38
|
+
white_vertline_distr,
|
39
|
+
)
|
40
|
+
|
41
|
+
from pynamicalsys.common.time_series_metrics import hurst_exponent
|
27
42
|
|
28
|
-
|
43
|
+
|
44
|
+
@njit
|
29
45
|
def lyapunov_exponents(
|
30
46
|
u: NDArray[np.float64],
|
31
47
|
parameters: NDArray[np.float64],
|
@@ -44,7 +60,6 @@ def lyapunov_exponents(
|
|
44
60
|
integrator=rk4_step_wrapped,
|
45
61
|
return_history: bool = False,
|
46
62
|
seed: int = 13,
|
47
|
-
log_base: float = np.e,
|
48
63
|
QR: Callable[
|
49
64
|
[NDArray[np.float64]], Tuple[NDArray[np.float64], NDArray[np.float64]]
|
50
65
|
] = qr,
|
@@ -110,7 +125,7 @@ def lyapunov_exponents(
|
|
110
125
|
# Perform the QR decomposition
|
111
126
|
v, R = QR(v)
|
112
127
|
# Accumulate the log
|
113
|
-
exponents += np.log(np.abs(np.diag(R)))
|
128
|
+
exponents += np.log(np.abs(np.diag(R)))
|
114
129
|
|
115
130
|
if return_history:
|
116
131
|
result = [time]
|
@@ -136,6 +151,102 @@ def lyapunov_exponents(
|
|
136
151
|
return [result]
|
137
152
|
|
138
153
|
|
154
|
+
@njit
|
155
|
+
def maximum_lyapunov_exponent(
|
156
|
+
u: NDArray[np.float64],
|
157
|
+
parameters: NDArray[np.float64],
|
158
|
+
total_time: float,
|
159
|
+
equations_of_motion: Callable[
|
160
|
+
[np.float64, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
|
161
|
+
],
|
162
|
+
jacobian: Callable[
|
163
|
+
[np.float64, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
|
164
|
+
],
|
165
|
+
transient_time: Optional[float] = None,
|
166
|
+
time_step: float = 0.01,
|
167
|
+
atol: float = 1e-6,
|
168
|
+
rtol: float = 1e-3,
|
169
|
+
integrator=rk4_step_wrapped,
|
170
|
+
return_history: bool = False,
|
171
|
+
seed: int = 13,
|
172
|
+
) -> NDArray[np.float64]:
|
173
|
+
|
174
|
+
neq = len(u) # Number of equations of the system
|
175
|
+
nt = neq + neq # system + variational equations
|
176
|
+
|
177
|
+
u = u.copy()
|
178
|
+
|
179
|
+
# Handle transient time
|
180
|
+
if transient_time is not None:
|
181
|
+
u = evolve_system(
|
182
|
+
u,
|
183
|
+
parameters,
|
184
|
+
transient_time,
|
185
|
+
equations_of_motion,
|
186
|
+
time_step=time_step,
|
187
|
+
atol=atol,
|
188
|
+
rtol=rtol,
|
189
|
+
integrator=integrator,
|
190
|
+
)
|
191
|
+
sample_time = total_time - transient_time
|
192
|
+
time = transient_time
|
193
|
+
else:
|
194
|
+
sample_time = total_time
|
195
|
+
time = 0
|
196
|
+
|
197
|
+
# State + deviation vectors
|
198
|
+
uv = np.zeros(nt)
|
199
|
+
uv[:neq] = u.copy()
|
200
|
+
|
201
|
+
# Randomly define the deviation vectors and orthonormalize them
|
202
|
+
np.random.seed(seed)
|
203
|
+
uv[neq:] = -1 + 2 * np.random.rand(nt - neq)
|
204
|
+
norm = np.linalg.norm(uv[neq:])
|
205
|
+
uv[neq:] /= norm
|
206
|
+
|
207
|
+
exponent = 0.0
|
208
|
+
history = []
|
209
|
+
|
210
|
+
while time < total_time:
|
211
|
+
if time + time_step > total_time:
|
212
|
+
time_step = total_time - time
|
213
|
+
|
214
|
+
uv, time, time_step = step(
|
215
|
+
time,
|
216
|
+
uv,
|
217
|
+
parameters,
|
218
|
+
equations_of_motion,
|
219
|
+
jacobian=jacobian,
|
220
|
+
time_step=time_step,
|
221
|
+
atol=atol,
|
222
|
+
rtol=rtol,
|
223
|
+
integrator=integrator,
|
224
|
+
number_of_deviation_vectors=1,
|
225
|
+
)
|
226
|
+
|
227
|
+
norm = np.linalg.norm(uv[neq:])
|
228
|
+
|
229
|
+
exponent += np.log(np.abs(norm))
|
230
|
+
|
231
|
+
uv[neq:] /= norm
|
232
|
+
|
233
|
+
if return_history:
|
234
|
+
result = [time]
|
235
|
+
result.append(
|
236
|
+
exponent
|
237
|
+
/ (time - (transient_time if transient_time is not None else 0))
|
238
|
+
)
|
239
|
+
history.append(result)
|
240
|
+
|
241
|
+
if return_history:
|
242
|
+
return history
|
243
|
+
else:
|
244
|
+
result = [
|
245
|
+
exponent / (time - (transient_time if transient_time is not None else 0))
|
246
|
+
]
|
247
|
+
return [result]
|
248
|
+
|
249
|
+
|
139
250
|
@njit
|
140
251
|
def SALI(
|
141
252
|
u: NDArray[np.float64],
|
@@ -238,7 +349,6 @@ def SALI(
|
|
238
349
|
return [[time, sali]]
|
239
350
|
|
240
351
|
|
241
|
-
# @njit
|
242
352
|
def LDI(
|
243
353
|
u: NDArray[np.float64],
|
244
354
|
parameters: NDArray[np.float64],
|
@@ -441,3 +551,191 @@ def GALI(
|
|
441
551
|
return history
|
442
552
|
else:
|
443
553
|
return [[time, gali]]
|
554
|
+
|
555
|
+
|
556
|
+
def recurrence_time_entropy(
|
557
|
+
u,
|
558
|
+
parameters,
|
559
|
+
num_points,
|
560
|
+
transient_time,
|
561
|
+
equations_of_motion,
|
562
|
+
time_step,
|
563
|
+
atol,
|
564
|
+
rtol,
|
565
|
+
integrator,
|
566
|
+
map_type,
|
567
|
+
section_index,
|
568
|
+
section_value,
|
569
|
+
crossing,
|
570
|
+
sampling_time,
|
571
|
+
maxima_index,
|
572
|
+
**kwargs,
|
573
|
+
):
|
574
|
+
|
575
|
+
# Configuration handling
|
576
|
+
config = RTEConfig(**kwargs)
|
577
|
+
|
578
|
+
# Metric setup
|
579
|
+
metric_map = {"supremum": np.inf, "euclidean": 2, "manhattan": 1}
|
580
|
+
|
581
|
+
try:
|
582
|
+
ord = metric_map[config.std_metric.lower()]
|
583
|
+
except KeyError:
|
584
|
+
raise ValueError(
|
585
|
+
f"Invalid std_metric: {config.std_metric}. Must be {list(metric_map.keys())}"
|
586
|
+
)
|
587
|
+
|
588
|
+
# Generate the Poincaré section or stroboscopic map
|
589
|
+
if map_type == "PS":
|
590
|
+
points = generate_poincare_section(
|
591
|
+
u,
|
592
|
+
parameters,
|
593
|
+
num_points,
|
594
|
+
equations_of_motion,
|
595
|
+
transient_time,
|
596
|
+
time_step,
|
597
|
+
atol,
|
598
|
+
rtol,
|
599
|
+
integrator,
|
600
|
+
section_index,
|
601
|
+
section_value,
|
602
|
+
crossing,
|
603
|
+
)
|
604
|
+
data = points[:, 1:] # Remove time
|
605
|
+
data = np.delete(data, section_index, axis=1)
|
606
|
+
elif map_type == "SM":
|
607
|
+
points = generate_stroboscopic_map(
|
608
|
+
u,
|
609
|
+
parameters,
|
610
|
+
num_points,
|
611
|
+
sampling_time,
|
612
|
+
equations_of_motion,
|
613
|
+
transient_time,
|
614
|
+
time_step,
|
615
|
+
atol,
|
616
|
+
rtol,
|
617
|
+
integrator,
|
618
|
+
)
|
619
|
+
|
620
|
+
data = points[:, 1:] # Remove time
|
621
|
+
else:
|
622
|
+
points = generate_maxima_map(
|
623
|
+
u,
|
624
|
+
parameters,
|
625
|
+
num_points,
|
626
|
+
maxima_index,
|
627
|
+
equations_of_motion,
|
628
|
+
transient_time,
|
629
|
+
time_step,
|
630
|
+
atol,
|
631
|
+
rtol,
|
632
|
+
integrator,
|
633
|
+
)
|
634
|
+
|
635
|
+
data = points[:, 1:] # Remove time
|
636
|
+
|
637
|
+
# Threshold calculation
|
638
|
+
if config.threshold_std:
|
639
|
+
std = np.std(data, axis=0)
|
640
|
+
eps = config.threshold * np.linalg.norm(std, ord=ord)
|
641
|
+
if eps <= 0:
|
642
|
+
eps = 0.1
|
643
|
+
else:
|
644
|
+
eps = config.threshold
|
645
|
+
|
646
|
+
# Recurrence matrix calculation
|
647
|
+
recmat = recurrence_matrix(data, float(eps), metric=config.metric)
|
648
|
+
|
649
|
+
# White line distribution
|
650
|
+
P = white_vertline_distr(recmat)[config.lmin :]
|
651
|
+
P = P[P > 0] # Remove zeros
|
652
|
+
P /= P.sum() # Normalize
|
653
|
+
|
654
|
+
# Entropy calculation
|
655
|
+
rte = -np.sum(P * np.log(P))
|
656
|
+
|
657
|
+
# Prepare output
|
658
|
+
result = [rte]
|
659
|
+
if config.return_final_state:
|
660
|
+
result.append(points[-1])
|
661
|
+
if config.return_recmat:
|
662
|
+
result.append(recmat)
|
663
|
+
if config.return_p:
|
664
|
+
result.append(P)
|
665
|
+
|
666
|
+
return result[0] if len(result) == 1 else tuple(result)
|
667
|
+
|
668
|
+
|
669
|
+
def hurst_exponent_wrapped(
|
670
|
+
u: NDArray[np.float64],
|
671
|
+
parameters: NDArray[np.float64],
|
672
|
+
num_points: int,
|
673
|
+
equations_of_motion: Callable,
|
674
|
+
time_step: float,
|
675
|
+
atol: float,
|
676
|
+
rtol: float,
|
677
|
+
integrator: Callable,
|
678
|
+
map_type: str,
|
679
|
+
section_index: int,
|
680
|
+
section_value: float,
|
681
|
+
crossing: int,
|
682
|
+
sampling_time: float,
|
683
|
+
maxima_index: int,
|
684
|
+
wmin: int = 2,
|
685
|
+
transient_time: Optional[int] = None,
|
686
|
+
) -> NDArray[np.float64]:
|
687
|
+
|
688
|
+
u = u.copy()
|
689
|
+
neq = len(u)
|
690
|
+
H = np.zeros(neq)
|
691
|
+
|
692
|
+
# Generate the Poincaré section or stroboscopic map
|
693
|
+
if map_type == "PS":
|
694
|
+
points = generate_poincare_section(
|
695
|
+
u,
|
696
|
+
parameters,
|
697
|
+
num_points,
|
698
|
+
equations_of_motion,
|
699
|
+
transient_time,
|
700
|
+
time_step,
|
701
|
+
atol,
|
702
|
+
rtol,
|
703
|
+
integrator,
|
704
|
+
section_index,
|
705
|
+
section_value,
|
706
|
+
crossing,
|
707
|
+
)
|
708
|
+
data = points[:, 1:] # Remove time
|
709
|
+
data = np.delete(data, section_index, axis=1)
|
710
|
+
elif map_type == "SM":
|
711
|
+
points = generate_stroboscopic_map(
|
712
|
+
u,
|
713
|
+
parameters,
|
714
|
+
num_points,
|
715
|
+
sampling_time,
|
716
|
+
equations_of_motion,
|
717
|
+
transient_time,
|
718
|
+
time_step,
|
719
|
+
atol,
|
720
|
+
rtol,
|
721
|
+
integrator,
|
722
|
+
)
|
723
|
+
|
724
|
+
data = points[:, 1:] # Remove time
|
725
|
+
else:
|
726
|
+
points = generate_maxima_map(
|
727
|
+
u,
|
728
|
+
parameters,
|
729
|
+
num_points,
|
730
|
+
maxima_index,
|
731
|
+
equations_of_motion,
|
732
|
+
transient_time,
|
733
|
+
time_step,
|
734
|
+
atol,
|
735
|
+
rtol,
|
736
|
+
integrator,
|
737
|
+
)
|
738
|
+
|
739
|
+
data = points[:, 1:] # Remove time
|
740
|
+
|
741
|
+
return hurst_exponent(data, wmin=wmin)
|
@@ -196,6 +196,31 @@ def rossler_system_4D_jacobian(
|
|
196
196
|
return J
|
197
197
|
|
198
198
|
|
199
|
+
@njit
|
200
|
+
def duffing(time, u, parameters):
|
201
|
+
delta, alpha, beta, gamma, omega = parameters
|
202
|
+
dudt = np.zeros_like(u)
|
203
|
+
dudt[0] = u[1]
|
204
|
+
dudt[1] = (
|
205
|
+
-delta * u[1] + alpha * u[0] - beta * u[0] ** 3 + gamma * np.cos(omega * time)
|
206
|
+
)
|
207
|
+
|
208
|
+
return dudt
|
209
|
+
|
210
|
+
|
211
|
+
@njit
|
212
|
+
def duffing_jacobian(time, u, parameters):
|
213
|
+
delta, alpha, beta, gamma, omega = parameters
|
214
|
+
neq = len(u)
|
215
|
+
J = np.zeros((neq, neq), dtype=np.float64)
|
216
|
+
|
217
|
+
J[0, 0] = 0
|
218
|
+
J[0, 1] = 1
|
219
|
+
J[1, 0] = alpha - 3 * beta * u[0] ** 2
|
220
|
+
J[1, 1] = -delta
|
221
|
+
return J
|
222
|
+
|
223
|
+
|
199
224
|
@njit
|
200
225
|
def variational_equations(
|
201
226
|
time: float,
|