pynamicalsys 1.2.2__py3-none-any.whl → 1.3.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.
@@ -21,6 +21,9 @@ from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union
21
21
 
22
22
  import numpy as np
23
23
  from numpy.typing import NDArray
24
+ import warnings
25
+ from numba.core.errors import NumbaExperimentalFeatureWarning
26
+
24
27
 
25
28
  from pynamicalsys.common.recurrence_quantification_analysis import RTEConfig
26
29
  from pynamicalsys.common.utils import finite_difference_jacobian, householder_qr
@@ -28,6 +31,7 @@ from pynamicalsys.discrete_time.dynamical_indicators import (
28
31
  RTE,
29
32
  SALI,
30
33
  LDI_k,
34
+ GALI_k,
31
35
  dig,
32
36
  finite_time_hurst_exponent,
33
37
  finite_time_lyapunov,
@@ -36,6 +40,7 @@ from pynamicalsys.discrete_time.dynamical_indicators import (
36
40
  lagrangian_descriptors,
37
41
  lyapunov_1D,
38
42
  lyapunov_er,
43
+ maximum_lyapunov_er,
39
44
  lyapunov_qr,
40
45
  )
41
46
  from pynamicalsys.discrete_time.models import (
@@ -322,6 +327,8 @@ class DiscreteDynamicalSystem:
322
327
  >>> system = DynamicalSystem(mapping=my_map, jacobian=my_jacobian, system_dimension=dim)
323
328
  """
324
329
 
330
+ warnings.filterwarnings("ignore", category=NumbaExperimentalFeatureWarning)
331
+
325
332
  if model is not None and mapping is not None:
326
333
  raise ValueError("Cannot specify both model and custom mapping")
327
334
 
@@ -404,6 +411,70 @@ class DiscreteDynamicalSystem:
404
411
 
405
412
  return self.__AVAILABLE_MODELS[model]
406
413
 
414
+ def step(
415
+ self,
416
+ u: Union[NDArray[np.float64], Sequence[float], float],
417
+ parameters: Union[None, float, Sequence[float], NDArray[np.float64]] = None,
418
+ ) -> NDArray[np.float64]:
419
+ """Perform one step in the mapping evolution
420
+
421
+ Parameters
422
+ ----------
423
+ u : Union[NDArray[np.float64], Sequence[float], float]
424
+ Initial condition(s):
425
+ - Single IC: 1D array of shape (d,) where d is the system dimension
426
+ - Ensemble: 2D array of shape (n, d) for n initial conditions
427
+ - Also accepts sequence types that will be converted to numpy arrays
428
+ - Scalar
429
+ parameters : Union[NDArray[np.float64], Sequence[float], float], optional
430
+ Parameters of the dynamical system, shape (p,) where p is the number of parameters
431
+
432
+ Returns
433
+ -------
434
+ NDArray[np.float64]
435
+ The next step of the given initial condition with the same shape as `u`.
436
+
437
+ Raises
438
+ ------
439
+ ValueError
440
+ - If `u` is not a scalar, 1D, or 2D array, or if its shape does not match the expected system dimension.
441
+ - If `u` is a 1D array but its length does not match the system dimension, or if `u` is a 2D array but does not match the expected shape for an ensemble.
442
+ - If `parameters` is not None and does not match the expected number of parameters.
443
+ - If `parameters` is None but the system expects parameters.
444
+ - If `parameters` is a scalar or array-like but not 1D.
445
+
446
+ TypeError
447
+ - If `u` is not a scalar or array-like type.
448
+ - If `parameters` is not a scalar or array-like type.
449
+
450
+ Examples
451
+ --------
452
+ >>> from pynamicalsys import DiscreteDynamicalSystem as dds
453
+ >>> ds = dds(model="standard map")
454
+ >>> # Single initial condition
455
+ >>> u = [0.2, 0.5]
456
+ >>> ds.step(u, parameters=1.5)
457
+ [[0.92704802 0.72704802]]
458
+ >>> # Multiple initial conditions
459
+ >>> u = np.array([[0.2, 0.5], [0.2, 0.3], [0.2, 0.6]])
460
+ >>> ds.step(u, paramters=1.5)
461
+ array([[0.92704802, 0.72704802],
462
+ [0.72704802, 0.52704802],
463
+ [0.02704802, 0.82704802]])
464
+ """
465
+ u = validate_initial_conditions(u, self.__system_dimension, allow_ensemble=True)
466
+
467
+ parameters = validate_parameters(parameters, self.__number_of_parameters)
468
+
469
+ if u.ndim == 1:
470
+ u_next = self.__mapping(u, parameters)
471
+ else:
472
+ u_next = np.zeros_like(u)
473
+ for i in range(u_next.shape[0]):
474
+ u_next[i] = self.__mapping(u[i], parameters)
475
+
476
+ return u_next
477
+
407
478
  def trajectory(
408
479
  self,
409
480
  u: Union[NDArray[np.float64], Sequence[float], float],
@@ -2269,6 +2340,7 @@ class DiscreteDynamicalSystem:
2269
2340
  return_history: bool = False,
2270
2341
  sample_times: Optional[Union[NDArray[np.int32], Sequence[int]]] = None,
2271
2342
  transient_time: Optional[int] = None,
2343
+ num_exponents: Optional[int] = None,
2272
2344
  log_base: float = np.e,
2273
2345
  ) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
2274
2346
  """Compute Lyapunov exponents using specified numerical method.
@@ -2290,7 +2362,9 @@ class DiscreteDynamicalSystem:
2290
2362
  sample_times : Optional[Union[NDArray[np.float64], Sequence[int]]], optional
2291
2363
  Specific times to sample when return_history=True
2292
2364
  transient_time : Optional[int], optional
2293
- Initial iterations to discard (default None → total_time//10)
2365
+ Initial iterations to discard
2366
+ num_exponents : Optional[int], optional
2367
+ Number of Lyapunov exponents to compute, by default None. If None, compute the whole spectrum.
2294
2368
  log_base : float, optional (default np.e)
2295
2369
  Logarithm base for exponents (e.g. e, 2, or 10)
2296
2370
 
@@ -2314,13 +2388,15 @@ class DiscreteDynamicalSystem:
2314
2388
  - If `transient_time` is greater than or equal to total_time.
2315
2389
  - If `method` is not "QR" or "QR_HH".
2316
2390
  - If `sample_times` is not a 1D array of integers.
2317
- - If `log_base` is not positive
2391
+ - If `log_base` is not positive.
2392
+ - If `num_exponents` is larger then the system's dimension.
2318
2393
  TypeError
2319
2394
  - If `u` is not a scalar or array-like type.
2320
2395
  - If `parameters` is not a scalar or array-like type.
2321
2396
  - If `total_time` is not int.
2322
2397
  - If `transient_time` is not int.
2323
2398
  - If `log_base` is not float.
2399
+ - If `num_exponents` is not an positive integer.
2324
2400
  - If sample_times cannot be converted to a 1D array of integers.
2325
2401
  - If `method` is not a string.
2326
2402
 
@@ -2376,7 +2452,19 @@ class DiscreteDynamicalSystem:
2376
2452
  if method == "QR" and self.__system_dimension == 2:
2377
2453
  method = "ER" # Fallback to QR for higher dimensions
2378
2454
 
2379
- sample_times = validate_sample_times(sample_times, total_time)
2455
+ if return_history and sample_times is not None:
2456
+ sample_times = validate_sample_times(sample_times, total_time)
2457
+ else:
2458
+ sample_times = np.arange(
2459
+ 1, total_time - (transient_time or 0) + 1, dtype=np.int64
2460
+ )
2461
+
2462
+ if num_exponents is None:
2463
+ num_exponents = self.__system_dimension
2464
+ elif num_exponents > self.__system_dimension:
2465
+ raise ValueError("num_exponents must be <= system_dimension")
2466
+ else:
2467
+ validate_non_negative(num_exponents, "num_exponents", Integral)
2380
2468
 
2381
2469
  validate_non_negative(log_base, "log_base", Real)
2382
2470
  if log_base == 1:
@@ -2387,30 +2475,38 @@ class DiscreteDynamicalSystem:
2387
2475
  compute_func = lyapunov_1D
2388
2476
  else:
2389
2477
  if method == "ER":
2390
- compute_func = lyapunov_er
2478
+ if num_exponents == 1:
2479
+ compute_func = maximum_lyapunov_er
2480
+ else:
2481
+ compute_func = lyapunov_er
2391
2482
  elif method == "QR":
2392
2483
  compute_func = lyapunov_qr
2393
2484
  else: # QR_HH
2394
2485
  compute_func = lambda *args, **kwargs: lyapunov_qr(
2395
2486
  *args, QR=householder_qr, **kwargs
2396
2487
  )
2397
-
2398
2488
  result = compute_func(
2399
2489
  u,
2400
2490
  parameters,
2401
2491
  total_time,
2402
2492
  self.__mapping,
2403
2493
  self.__jacobian,
2494
+ num_exponents,
2495
+ sample_times,
2404
2496
  return_history=return_history,
2405
- sample_times=sample_times,
2406
2497
  transient_time=transient_time,
2407
2498
  log_base=log_base,
2408
2499
  )
2409
2500
 
2410
2501
  if return_history:
2411
- return result[0]
2502
+ return result if self.__system_dimension == 1 else result[0]
2412
2503
  else:
2413
- return result[0][:, 0] if self.__system_dimension > 1 else result[0]
2504
+ if self.__system_dimension == 1:
2505
+ return result[0]
2506
+ elif self.__system_dimension > 1 and num_exponents > 1:
2507
+ return result[0][:, 0]
2508
+ else:
2509
+ return result[0][0]
2414
2510
 
2415
2511
  def finite_time_lyapunov(
2416
2512
  self,
@@ -2420,6 +2516,7 @@ class DiscreteDynamicalSystem:
2420
2516
  parameters: Union[
2421
2517
  None, float, Sequence[np.float64], NDArray[np.float64]
2422
2518
  ] = None,
2519
+ num_exponents: Optional[int] = None,
2423
2520
  method: str = "QR",
2424
2521
  transient_time: Optional[int] = None,
2425
2522
  log_base: float = np.e,
@@ -2519,6 +2616,11 @@ class DiscreteDynamicalSystem:
2519
2616
  if method not in ("QR", "QR_HH"):
2520
2617
  raise ValueError("method must be 'QR' or 'QR_HH'")
2521
2618
 
2619
+ if num_exponents is None:
2620
+ num_exponents = self.__system_dimension
2621
+ elif num_exponents > self.__system_dimension:
2622
+ raise ValueError("num_exponents must be <= system_dimension")
2623
+
2522
2624
  # Validate method for system dimension
2523
2625
  if method == "QR" and self.__system_dimension == 2:
2524
2626
  method = "ER" # Fallback to QR for higher dimensions
@@ -2537,6 +2639,7 @@ class DiscreteDynamicalSystem:
2537
2639
  finite_time,
2538
2640
  self.__mapping,
2539
2641
  self.__jacobian,
2642
+ num_exponents,
2540
2643
  method=method,
2541
2644
  transient_time=transient_time,
2542
2645
  log_base=log_base,
@@ -2819,7 +2922,12 @@ class DiscreteDynamicalSystem:
2819
2922
  validate_non_negative(total_time, "total_time", Integral)
2820
2923
  validate_transient_time(transient_time, total_time, Integral)
2821
2924
 
2822
- sample_times = validate_sample_times(sample_times, total_time)
2925
+ if return_history and sample_times is not None:
2926
+ sample_times = validate_sample_times(sample_times, total_time)
2927
+ else:
2928
+ sample_times = np.arange(
2929
+ 1, total_time - (transient_time or 0) + 1, dtype=np.int64
2930
+ )
2823
2931
 
2824
2932
  validate_non_negative(tol, "tol", Real)
2825
2933
 
@@ -2832,8 +2940,8 @@ class DiscreteDynamicalSystem:
2832
2940
  total_time,
2833
2941
  self.__mapping,
2834
2942
  self.__jacobian,
2943
+ sample_times,
2835
2944
  return_history=return_history,
2836
- sample_times=sample_times,
2837
2945
  transient_time=transient_time,
2838
2946
  tol=tol,
2839
2947
  seed=seed,
@@ -2855,7 +2963,7 @@ class DiscreteDynamicalSystem:
2855
2963
  transient_time: Optional[int] = None,
2856
2964
  seed: int = 13,
2857
2965
  ) -> Union[NDArray[np.float64], Tuple[NDArray[np.float64], NDArray[np.float64]]]:
2858
- """Compute Linear Dependence Index(LDI_k) for chaos detection.
2966
+ """Compute the Linear Dependence Index (LDI_k) for chaos detection.
2859
2967
 
2860
2968
  Parameters
2861
2969
  ----------
@@ -2882,7 +2990,7 @@ class DiscreteDynamicalSystem:
2882
2990
  -------
2883
2991
  Union[NDArray[np.float64], Tuple[NDArray[np.float64], NDArray[np.float64]]]
2884
2992
  - If return_history = False: Final LDI_k value(scalar)
2885
- - If return_history = True: Tuple of(LDI_history, final_state) where LDI_history is 1D array of values
2993
+ - If return_history = True: Tuple of (LDI_history, final_state) where LDI_history is 1D array of values
2886
2994
 
2887
2995
  Raises
2888
2996
  ------
@@ -2912,7 +3020,7 @@ class DiscreteDynamicalSystem:
2912
3020
  - LDI_k behavior:
2913
3021
  - → 0 exponentially for chaotic orbits(rate depends on k)
2914
3022
  - → positive constant for regular orbits
2915
- - LDI_2 ~ SALI(same convergence rate)
3023
+ - LDI_2 ~ SALI (same convergence rate)
2916
3024
  - Higher k indices decay faster for chaotic orbits
2917
3025
  - For Hamiltonian systems, k should be ≤ d/2
2918
3026
  - Early termination when LDI_k < tol
@@ -2947,7 +3055,12 @@ class DiscreteDynamicalSystem:
2947
3055
  if k < 2 or k > self.__system_dimension:
2948
3056
  raise ValueError(f"k must be in range [2, {self.__system_dimension}]")
2949
3057
 
2950
- sample_times = validate_sample_times(sample_times, total_time)
3058
+ if return_history and sample_times is not None:
3059
+ sample_times = validate_sample_times(sample_times, total_time)
3060
+ else:
3061
+ sample_times = np.arange(
3062
+ 1, total_time - (transient_time or 0) + 1, dtype=np.int64
3063
+ )
2951
3064
 
2952
3065
  validate_non_negative(tol, "tol", Real)
2953
3066
 
@@ -2961,9 +3074,144 @@ class DiscreteDynamicalSystem:
2961
3074
  total_time,
2962
3075
  self.__mapping,
2963
3076
  self.__jacobian,
2964
- k=k,
3077
+ k,
3078
+ sample_times,
3079
+ return_history=return_history,
3080
+ transient_time=transient_time,
3081
+ tol=tol,
3082
+ seed=seed,
3083
+ )
3084
+
3085
+ return result if return_history else result[0]
3086
+
3087
+ def GALI(
3088
+ self,
3089
+ u: Union[NDArray[np.float64], Sequence[float]],
3090
+ total_time: int,
3091
+ k: int,
3092
+ parameters: Union[
3093
+ None, float, Sequence[np.float64], NDArray[np.float64]
3094
+ ] = None,
3095
+ return_history: bool = False,
3096
+ sample_times: Optional[Union[NDArray[np.int32], Sequence[int]]] = None,
3097
+ tol: float = 1e-16,
3098
+ transient_time: Optional[int] = None,
3099
+ seed: int = 13,
3100
+ ) -> Union[NDArray[np.float64], Tuple[NDArray[np.float64], NDArray[np.float64]]]:
3101
+ """Compute the Generalized Aligment Index (GALI_k) for chaos detection.
3102
+
3103
+ Parameters
3104
+ ----------
3105
+ u: Union[NDArray[np.float64], Sequence[float]]
3106
+ Initial condition of shape(d,) where d is system dimension
3107
+ total_time: int
3108
+ Maximum number of iterations(must be ≥ 1)
3109
+ k: int
3110
+ Number of deviation vectors to use(2 ≤ k ≤ d, default 2)
3111
+ parameters: Union[None, float, Sequence[np.float64], NDArray[np.float64]], optional
3112
+ System parameters of shape(p,) passed to mapping function
3113
+ return_history: bool, optional
3114
+ If True, returns full evolution(default False)
3115
+ sample_times: Optional[Union[NDArray[np.float64], Sequence[int]]], optional
3116
+ Specific times to sample(must be sorted, default None)
3117
+ tol: float, optional
3118
+ Early termination threshold(default 1e-16)
3119
+ transient_time: Optional[int], optional
3120
+ Initial iterations to discard(default None → total_time//10)
3121
+ seed: int, optional
3122
+ Random seed for reproducibility(default 13)
3123
+
3124
+ Returns
3125
+ -------
3126
+ Union[NDArray[np.float64], Tuple[NDArray[np.float64], NDArray[np.float64]]]
3127
+ - If return_history = False: Final GALI_k value(scalar)
3128
+ - If return_history = True: Tuple of (GALI_history, final_state) where GALI_history is 1D array of values
3129
+
3130
+ Raises
3131
+ ------
3132
+ ValueError
3133
+ - If `u` is not an 1D array, or if its shape does not match the expected system dimension.
3134
+ - If `parameters` is not None and does not match the expected number of parameters.
3135
+ - If `parameters` is None but the system expects parameters.
3136
+ - If `parameters` is a scalar or array-like but not 1D.
3137
+ - If `total_time` is negative.
3138
+ - If `trasient_time` is negative.
3139
+ - If `transient_time` is greater than or equal to total_time.
3140
+ - If `sample_times` is not a 1D array of integers.
3141
+ - If `k` is less than 2 or greater than system dimension.
3142
+
3143
+ TypeError
3144
+ - If `u` is not a scalar or array-like type.
3145
+ - If `parameters` is not a scalar or array-like type.
3146
+ - If `total_time` is not int.
3147
+ - If `transient_time` is not int.
3148
+ - If sample_times cannot be converted to a 1D array of integers.
3149
+ - If `tol` is not a positive float.
3150
+ - If `seed` is not an integer.
3151
+ - If `k` is not a positive integer.
3152
+
3153
+ Notes
3154
+ -----
3155
+ - GALI_k behavior:
3156
+ - → 0 exponentially for chaotic orbits(rate depends on k)
3157
+ - → positive constant for regular orbits
3158
+ - GALI_2 ~ SALI (same convergence rate)
3159
+ - Higher k indices decay faster for chaotic orbits
3160
+ - For Hamiltonian systems, k should be ≤ d/2
3161
+ - Early termination when GALI_k < tol
3162
+
3163
+ Examples
3164
+ --------
3165
+ >>> # Basic usage (LDI_2 final value)
3166
+ >>> u0 = np.array([0.1, 0.2, 0.0, 0.0])
3167
+ >>> params = np.array([0.5, 1.0])
3168
+ >>> LDI = system.LDI(u0, params, 10000, k=2)
3169
+
3170
+ >>> # LDI_3 with full history
3171
+ >>> LDI_hist, final = system.LDI(
3172
+ ... u0, params, 10000, k=3, return_history=True)
3173
+
3174
+ >>> # With custom sampling
3175
+ >>> times = np.array([100, 1000, 5000])
3176
+ >>> LDI_samples, _ = system.LDI(
3177
+ ... u0, params, 10000, k=2, sample_times=times, return_history=True)
3178
+ """
3179
+
3180
+ u = validate_initial_conditions(
3181
+ u, self.__system_dimension, allow_ensemble=False
3182
+ )
3183
+
3184
+ parameters = validate_parameters(parameters, self.__number_of_parameters)
3185
+
3186
+ validate_non_negative(total_time, "total_time", Integral)
3187
+ validate_transient_time(transient_time, total_time, Integral)
3188
+
3189
+ validate_positive(k, "k", Integral)
3190
+ if k < 2 or k > self.__system_dimension:
3191
+ raise ValueError(f"k must be in range [2, {self.__system_dimension}]")
3192
+
3193
+ if return_history and sample_times is not None:
3194
+ sample_times = validate_sample_times(sample_times, total_time)
3195
+ else:
3196
+ sample_times = np.arange(
3197
+ 1, total_time - (transient_time or 0) + 1, dtype=np.int64
3198
+ )
3199
+
3200
+ validate_non_negative(tol, "tol", Real)
3201
+
3202
+ if not isinstance(seed, Integral):
3203
+ raise TypeError("seed must be an integer")
3204
+
3205
+ # Call underlying implementation
3206
+ result = GALI_k(
3207
+ u,
3208
+ parameters,
3209
+ total_time,
3210
+ self.__mapping,
3211
+ self.__jacobian,
3212
+ k,
3213
+ sample_times,
2965
3214
  return_history=return_history,
2966
- sample_times=sample_times,
2967
3215
  transient_time=transient_time,
2968
3216
  tol=tol,
2969
3217
  seed=seed,