pynamicalsys 1.3.0__py3-none-any.whl → 1.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. pynamicalsys/__init__.py +2 -0
  2. pynamicalsys/__version__.py +2 -2
  3. pynamicalsys/common/recurrence_quantification_analysis.py +1 -1
  4. pynamicalsys/common/time_series_metrics.py +85 -0
  5. pynamicalsys/common/utils.py +3 -3
  6. pynamicalsys/continuous_time/chaotic_indicators.py +306 -8
  7. pynamicalsys/continuous_time/models.py +25 -0
  8. pynamicalsys/continuous_time/numerical_integrators.py +7 -7
  9. pynamicalsys/continuous_time/trajectory_analysis.py +460 -13
  10. pynamicalsys/core/continuous_dynamical_systems.py +933 -35
  11. pynamicalsys/core/discrete_dynamical_systems.py +20 -9
  12. pynamicalsys/core/hamiltonian_systems.py +1193 -0
  13. pynamicalsys/core/time_series_metrics.py +65 -0
  14. pynamicalsys/discrete_time/dynamical_indicators.py +13 -102
  15. pynamicalsys/discrete_time/models.py +2 -2
  16. pynamicalsys/discrete_time/trajectory_analysis.py +10 -10
  17. pynamicalsys/discrete_time/transport.py +1 -1
  18. pynamicalsys/hamiltonian_systems/__init__.py +16 -0
  19. pynamicalsys/hamiltonian_systems/chaotic_indicators.py +638 -0
  20. pynamicalsys/hamiltonian_systems/models.py +68 -0
  21. pynamicalsys/hamiltonian_systems/numerical_integrators.py +248 -0
  22. pynamicalsys/hamiltonian_systems/trajectory_analysis.py +293 -0
  23. pynamicalsys/hamiltonian_systems/validators.py +114 -0
  24. {pynamicalsys-1.3.0.dist-info → pynamicalsys-1.4.0.dist-info}/METADATA +37 -8
  25. pynamicalsys-1.4.0.dist-info/RECORD +36 -0
  26. pynamicalsys-1.3.0.dist-info/RECORD +0 -28
  27. {pynamicalsys-1.3.0.dist-info → pynamicalsys-1.4.0.dist-info}/WHEEL +0 -0
  28. {pynamicalsys-1.3.0.dist-info → pynamicalsys-1.4.0.dist-info}/top_level.txt +0 -0
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",
@@ -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.3.0'
32
- __version_tuple__ = version_tuple = (1, 3, 0)
31
+ __version__ = version = '1.4.0'
32
+ __version_tuple__ = version_tuple = (1, 4, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -91,7 +91,7 @@ class RTEConfig:
91
91
  raise ValueError("metric must be 'supremum', 'euclidean' or 'manhattan'")
92
92
 
93
93
 
94
- @njit(cache=True)
94
+ @njit
95
95
  def _recurrence_matrix(
96
96
  arr: NDArray[np.float64], threshold: float, metric_id: int
97
97
  ) -> NDArray[np.uint8]:
@@ -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
@@ -21,7 +21,7 @@ from numpy.typing import NDArray
21
21
  from numba import njit
22
22
 
23
23
 
24
- @njit(cache=True)
24
+ @njit
25
25
  def qr(M: NDArray[np.float64]) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
26
26
  """
27
27
  Perform numerically stable QR decomposition using modified Gram-Schmidt with reorthogonalization.
@@ -92,7 +92,7 @@ def qr(M: NDArray[np.float64]) -> Tuple[NDArray[np.float64], NDArray[np.float64]
92
92
  return Q, R
93
93
 
94
94
 
95
- @njit(cache=True)
95
+ @njit
96
96
  def householder_qr(
97
97
  M: NDArray[np.float64],
98
98
  ) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
@@ -184,7 +184,7 @@ def householder_qr(
184
184
  return Q, R
185
185
 
186
186
 
187
- @njit(cache=True)
187
+ @njit
188
188
  def finite_difference_jacobian(
189
189
  u: NDArray[np.float64],
190
190
  parameters: NDArray[np.float64],
@@ -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, Union, Tuple, Dict, List, Any, Sequence
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
- from pynamicalsys.continuous_time.trajectory_analysis import step, evolve_system
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
- # @njit(cache=True)
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))) / np.log(log_base)
128
+ exponents += np.log(np.abs(np.diag(R)))
114
129
 
115
130
  if return_history:
116
131
  result = [time]
@@ -136,7 +151,103 @@ def lyapunov_exponents(
136
151
  return [result]
137
152
 
138
153
 
139
- @njit(cache=True)
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
+
250
+ @njit
140
251
  def SALI(
141
252
  u: NDArray[np.float64],
142
253
  parameters: NDArray[np.float64],
@@ -238,7 +349,6 @@ def SALI(
238
349
  return [[time, sali]]
239
350
 
240
351
 
241
- # @njit(cache=True)
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,
@@ -23,7 +23,7 @@ from numba import njit, prange
23
23
  from pynamicalsys.continuous_time.models import variational_equations
24
24
 
25
25
 
26
- @njit(cache=True)
26
+ @njit
27
27
  def rk4_step(
28
28
  t: float,
29
29
  u: NDArray[np.float64],
@@ -43,7 +43,7 @@ def rk4_step(
43
43
  return u_next
44
44
 
45
45
 
46
- @njit(cache=True)
46
+ @njit
47
47
  def variational_rk4_step(
48
48
  t: float,
49
49
  u: NDArray[np.float64],
@@ -115,7 +115,7 @@ RK45_B4 = np.array(
115
115
  )
116
116
 
117
117
 
118
- @njit(cache=True)
118
+ @njit
119
119
  def rk45_step(t, u, parameters, equations_of_motion, time_step, atol=1e-6, rtol=1e-3):
120
120
  """Single adaptive step of RK45 (Dormand-Prince).
121
121
 
@@ -163,7 +163,7 @@ def rk45_step(t, u, parameters, equations_of_motion, time_step, atol=1e-6, rtol=
163
163
  return u5, t + time_step, time_step_new, accept
164
164
 
165
165
 
166
- @njit(cache=True)
166
+ @njit
167
167
  def variational_rk45_step(
168
168
  t,
169
169
  u,
@@ -227,7 +227,7 @@ def variational_rk45_step(
227
227
  return u5, t + time_step, time_step_new, accept
228
228
 
229
229
 
230
- @njit(cache=True)
230
+ @njit
231
231
  def rk4_step_wrapped(
232
232
  t: float,
233
233
  u: NDArray[np.float64],
@@ -265,7 +265,7 @@ def rk4_step_wrapped(
265
265
  return u_next, t_next, h_next, accept
266
266
 
267
267
 
268
- @njit(cache=True)
268
+ @njit
269
269
  def rk45_step_wrapped(
270
270
  t: float,
271
271
  u: NDArray[np.float64],
@@ -302,7 +302,7 @@ def rk45_step_wrapped(
302
302
  )
303
303
 
304
304
 
305
- @njit(cache=True)
305
+ @njit
306
306
  def estimate_initial_step(
307
307
  t0: float,
308
308
  u0: np.ndarray,