pynamicalsys 1.3.1__py3-none-any.whl → 1.4.1__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 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.1'
32
- __version_tuple__ = version_tuple = (1, 3, 1)
31
+ __version__ = version = '1.4.1'
32
+ __version_tuple__ = version_tuple = (1, 4, 1)
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, 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
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,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,