pynamicalsys 1.2.2__py3-none-any.whl → 1.3.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.
@@ -25,6 +25,7 @@ from pynamicalsys.common.utils import householder_qr, qr
25
25
  from pynamicalsys.continuous_time.chaotic_indicators import (
26
26
  LDI,
27
27
  SALI,
28
+ GALI,
28
29
  lyapunov_exponents,
29
30
  )
30
31
  from pynamicalsys.continuous_time.models import (
@@ -56,6 +57,47 @@ from pynamicalsys.continuous_time.validators import (
56
57
 
57
58
 
58
59
  class ContinuousDynamicalSystem:
60
+ """Class representing a continuous-time dynamical system with various models and methods for analysis.
61
+
62
+ This class allows users to work with predefined dynamical models or with user-provided equations of motion, compute trajectories, Lyapunov exponents and more dynamical analyses.
63
+
64
+ Parameters
65
+ ----------
66
+ model : str, optional
67
+ Name of the predefined model to use (e.g. "lorenz system").
68
+ equations_of_motion : callable, optional
69
+ Custom function that describes the equations of motion with signature f(time, state, parameters) -> array_like. If provided, `model` must be None.
70
+ jacobian : callable, optional
71
+ Custom function that describes the Jacobian matrix of the system with signature J(time, state, parameters) -> array_like
72
+ system_dimension : int, optional
73
+ Dimension of the system (number of equations). Required if using custom equations of motion and not a predefined model.
74
+ number_of_parameters : int, optional
75
+ Number of parameters of the system. Required if using custom equations of motion and not a predefined model.
76
+
77
+ Raises
78
+ ------
79
+ ValueError
80
+ - If neither model nor equations_of_motion is provided, or if provided model is not implemented.
81
+ - If `system_dimension` is negative.
82
+ - If `number_of_parameters` is negative.
83
+
84
+ TypeError
85
+ - If `equations_of_motion` or `jacobian` are not callable.
86
+ - If `system_dimension` or `number_of_parameters` are not valid integers.
87
+
88
+ Notes
89
+ -----
90
+ - When providing custom functions, either provide both `equations_of_motion` and `jacobian`, or just the `equations_of_motion`.
91
+ - When providing custom functions, the equations of motion functions signature should be f(time, u, parameters) -> NDArray[np.float64].
92
+ - The class supports various predefined models, such as the Lorenz and Rössler system.
93
+ - The available models can be queried using the 'available_models' class method.
94
+
95
+ Examples
96
+ --------
97
+ >>> from pynamicalsys import ContinuousDynamicalSystem as cds
98
+ >>> # Using predefined model
99
+ >>> ds = cds(model="lorenz system")
100
+ """
59
101
 
60
102
  __AVAILABLE_MODELS: Dict[str, Dict[str, Any]] = {
61
103
  "lorenz system": {
@@ -203,13 +245,13 @@ class ContinuousDynamicalSystem:
203
245
 
204
246
  @property
205
247
  def integrator_info(self):
206
- """Return the information about the current integrator"""
248
+ """Return a dictionary with information about the current integrator."""
207
249
  integrator = self.__integrator.lower()
208
250
 
209
251
  return self.__AVAILABLE_INTEGRATORS[integrator]
210
252
 
211
253
  def integrator(self, integrator, time_step=1e-2, atol=1e-6, rtol=1e-3):
212
- """Set the integrator to use in the simulation.
254
+ """Define the integrator.
213
255
 
214
256
  Parameters
215
257
  ----------
@@ -228,8 +270,8 @@ class ContinuousDynamicalSystem:
228
270
  If `time_step`, `atol`, or `rtol` are negative.
229
271
  If `integrator` is not available.
230
272
  TypeError
231
- If `time_step`, `atol`, or `rtol` are not valid numbers.
232
- If `integrator` is not a string.
273
+ - If `integrator` is not a string.
274
+ - If `time_step`, `atol`, or `rtol` are not valid numbers
233
275
 
234
276
  Examples
235
277
  --------
@@ -240,6 +282,9 @@ class ContinuousDynamicalSystem:
240
282
  >>> ds.integrator("rk4", time_step=0.001) # To use the RK4 integrator
241
283
  >>> ds.integrator("rk45", atol=1e-10, rtol=1e-8) # To use the RK45 integrator
242
284
  """
285
+
286
+ if not isinstance(integrator, str):
287
+ raise ValueError("integrator must be a string.")
243
288
  validate_non_negative(time_step, "time_step", type_=Real)
244
289
  validate_non_negative(atol, "atol", type_=Real)
245
290
  validate_non_negative(rtol, "rtol", type_=Real)
@@ -441,6 +486,7 @@ class ContinuousDynamicalSystem:
441
486
  total_time: float,
442
487
  parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
443
488
  transient_time: Optional[float] = None,
489
+ num_exponents: Optional[int] = None,
444
490
  return_history: bool = False,
445
491
  seed: int = 13,
446
492
  log_base: float = np.e,
@@ -461,6 +507,8 @@ class ContinuousDynamicalSystem:
461
507
  Parameters of the system, by default None. Can be a scalar, a sequence of floats or a numpy array.
462
508
  transient_time : Optional[float], optional
463
509
  Transient time, i.e., the time to discard before calculating the Lyapunov exponents, by default None.
510
+ num_exponents : Optional[int], optional
511
+ The number of Lyapunov exponents to be calculated, by default None. If None, the method calculates the whole spectrum.
464
512
  return_history : bool, optional
465
513
  Whether to return or not the Lyapunov exponents history in time, by default False.
466
514
  seed : int, optional
@@ -491,6 +539,7 @@ class ContinuousDynamicalSystem:
491
539
  TypeError
492
540
  - If `method` is not a string.
493
541
  - If `total_time`, `transient_time`, or `log_base` are not valid numbers.
542
+ - If `num_exponents` is not an positive integer.
494
543
  - If `seed` is not an integer.
495
544
 
496
545
  Notes
@@ -502,13 +551,15 @@ class ContinuousDynamicalSystem:
502
551
  >>> from pynamicalsys import ContinuousDynamicalSystem as cds
503
552
  >>> ds = cds(model="lorenz system")
504
553
  >>> u = [0.1, 0.1, 0.1]
505
- >>> total_time = 1000
506
- >>> transient_time = 500
554
+ >>> total_time = 10000
555
+ >>> transient_time = 5000
507
556
  >>> parameters = [16.0, 45.92, 4.0]
508
- >>> ds.lyapunov(u, total_time, parameters=parameters, transient_time=transient_time, log_base=2)
509
- array([ 2.15920769e+00, -4.61882314e-03, -3.24498622e+01])
557
+ >>> ds.lyapunov(u, total_time, parameters=parameters, transient_time=transient_time)
558
+ array([ 1.49885208e+00, -1.65186396e-04, -2.24977688e+01])
559
+ >>> ds.lyapunov(u, total_time, parameters=parameters, transient_time=transient_time, num_exponents=2)
560
+ array([1.49873694e+00, 1.31950729e-04])
510
561
  >>> ds.lyapunov(u, total_time, parameters=parameters, transient_time=transient_time, log_base=2, method="QR_HH")
511
- array([ 2.15920769e+00, -4.61882314e-03, -3.24498622e+01])
562
+ array([ 2.16664847e+00, -6.80920729e-04, -3.24625604e+01])
512
563
  """
513
564
 
514
565
  if self.__jacobian is None:
@@ -527,6 +578,13 @@ class ContinuousDynamicalSystem:
527
578
 
528
579
  time_step = self.__get_initial_time_step(u, parameters)
529
580
 
581
+ if num_exponents is None:
582
+ num_exponents = self.__system_dimension
583
+ elif num_exponents > self.__system_dimension:
584
+ raise ValueError("num_exponents must be <= system_dimension")
585
+ else:
586
+ validate_non_negative(num_exponents, "num_exponents", Integral)
587
+
530
588
  if endpoint:
531
589
  total_time += time_step
532
590
 
@@ -551,6 +609,7 @@ class ContinuousDynamicalSystem:
551
609
  total_time,
552
610
  self.__equations_of_motion,
553
611
  self.__jacobian,
612
+ num_exponents,
554
613
  transient_time=transient_time,
555
614
  time_step=time_step,
556
615
  atol=self.__atol,
@@ -717,7 +776,6 @@ class ContinuousDynamicalSystem:
717
776
 
718
777
  - If `return_history = False`, return time and LDI, where time is the time at the end of the execution. time < total_time if LDI becomes less than `threshold` before `total_time`.
719
778
  - If `return_history = True`, return the sampled times and the LDI values.
720
- - If `sample_times` is provided, return the LDI at the specified times.
721
779
 
722
780
  Raises
723
781
  ------
@@ -741,7 +799,7 @@ class ContinuousDynamicalSystem:
741
799
  >>> transient_time = 500
742
800
  >>> parameters = [16.0, 45.92, 4.0]
743
801
  >>> ds.LDI(u, total_time, 2, parameters=parameters, transient_time=transient_time)
744
- (521.8099999999802, 7.328757804386809e-17)
802
+ array([5.23170000e+02, 6.93495605e-17])
745
803
  >>> ds.LDI(u, total_time, 3, parameters=parameters, transient_time=transient_time)
746
804
  (501.26999999999884, 9.984145370766051e-17)
747
805
  >>> # Returning the history
@@ -792,3 +850,118 @@ class ContinuousDynamicalSystem:
792
850
  return np.array(result)
793
851
  else:
794
852
  return np.array(result[0])
853
+
854
+ def GALI(
855
+ self,
856
+ u: NDArray[np.float64],
857
+ total_time: float,
858
+ k: int,
859
+ parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
860
+ transient_time: Optional[float] = None,
861
+ return_history: bool = False,
862
+ seed: int = 13,
863
+ threshold: float = 1e-16,
864
+ endpoint: bool = True,
865
+ ) -> NDArray[np.float64]:
866
+ """Calculate the Generalized Aligment Index (GALI) for a given dynamical system.
867
+
868
+ Parameters
869
+ ----------
870
+ u : NDArray[np.float64]
871
+ Initial conditions of the system. Must match the system's dimension.
872
+ total_time : float
873
+ Total time over which to evolve the system (including transient).
874
+ parameters : Union[None, Sequence[float], NDArray[np.float64]], optional
875
+ Parameters of the system, by default None. Can be a scalar, a sequence of floats or a numpy array.
876
+ transient_time : Optional[float], optional
877
+ Transient time, i.e., the time to discard before calculating the Lyapunov exponents, by default None.
878
+ return_history : bool, optional
879
+ Whether to return or not the Lyapunov exponents history in time, by default False.
880
+ seed : int, optional
881
+ The seed to randomly generate the deviation vectors, by default 13.
882
+ threshold : float, optional
883
+ The threhshold for early termination, by default 1e-16. When SALI becomes less than `threshold`, stops the execution.
884
+ endpoint : bool, optional
885
+ Whether to include the endpoint time = total_time in the calculation, by default True.
886
+
887
+ Returns
888
+ -------
889
+ NDArray[np.float64]
890
+ The GALI value
891
+
892
+ - If `return_history = False`, return time and GALI, where time is the time at the end of the execution. time < total_time if GALI becomes less than `threshold` before `total_time`.
893
+ - If `return_history = True`, return the sampled times and the GALI values.
894
+
895
+ Raises
896
+ ------
897
+ ValueError
898
+ - If the Jacobian function is not provided.
899
+ - If the initial condition is not valid, i.e., if the dimensions do not match.
900
+ - If the number of parameters does not match.
901
+ - If `parameters` is not a scalar, 1D list, or 1D array.
902
+ - If `total_time`, `transient_time`, or `threshold` are negative.
903
+ - If `k` < 2.
904
+ TypeError
905
+ - If `total_time`, `transient_time`, or `threshold` are not valid numbers.
906
+ - If `seed` is not an integer.
907
+
908
+ Examples
909
+ --------
910
+ >>> from pynamicalsys import ContinuousDynamicalSystem as cds
911
+ >>> ds = cds(model="lorenz system")
912
+ >>> u = [0.1, 0.1, 0.1]
913
+ >>> total_time = 1000
914
+ >>> transient_time = 500
915
+ >>> parameters = [16.0, 45.92, 4.0]
916
+ >>> ds.GALI(u, total_time, 2, parameters=parameters, transient_time=transient_time)
917
+ (521.8099999999802, 7.328757804386809e-17)
918
+ >>> ds.GALI(u, total_time, 3, parameters=parameters, transient_time=transient_time)
919
+ (501.26999999999884, 9.984145370766051e-17)
920
+ >>> # Returning the history
921
+ >>> gali = ds.GALI(u, total_time, 2, parameters=parameters, transient_time=transient_time)
922
+ >>> gali.shape
923
+ (2181, 2)
924
+ """
925
+
926
+ if self.__jacobian is None:
927
+ raise ValueError(
928
+ "Jacobian function is required to compute Lyapunov exponents"
929
+ )
930
+
931
+ u = validate_initial_conditions(
932
+ u, self.__system_dimension, allow_ensemble=False
933
+ )
934
+ u = u.copy()
935
+
936
+ parameters = validate_parameters(parameters, self.__number_of_parameters)
937
+
938
+ transient_time, total_time = validate_times(transient_time, total_time)
939
+
940
+ time_step = self.__get_initial_time_step(u, parameters)
941
+
942
+ validate_non_negative(threshold, "threshold", type_=Real)
943
+
944
+ if endpoint:
945
+ total_time += time_step
946
+
947
+ result = GALI(
948
+ u,
949
+ parameters,
950
+ total_time,
951
+ self.__equations_of_motion,
952
+ self.__jacobian,
953
+ k,
954
+ transient_time=transient_time,
955
+ time_step=time_step,
956
+ atol=self.__atol,
957
+ rtol=self.__rtol,
958
+ integrator=self.__integrator_func,
959
+ return_history=return_history,
960
+ seed=seed,
961
+ threshold=threshold,
962
+ )
963
+
964
+ if return_history:
965
+ return np.array(result)
966
+ else:
967
+ return np.array(result[0])