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.
- pynamicalsys/__version__.py +16 -3
- pynamicalsys/common/utils.py +20 -21
- pynamicalsys/continuous_time/chaotic_indicators.py +174 -78
- pynamicalsys/continuous_time/numerical_integrators.py +1 -1
- pynamicalsys/continuous_time/trajectory_analysis.py +58 -22
- pynamicalsys/core/continuous_dynamical_systems.py +184 -11
- pynamicalsys/core/discrete_dynamical_systems.py +264 -16
- pynamicalsys/discrete_time/dynamical_indicators.py +407 -163
- pynamicalsys/discrete_time/transport.py +74 -112
- pynamicalsys/discrete_time/validators.py +1 -1
- {pynamicalsys-1.2.2.dist-info → pynamicalsys-1.3.0.dist-info}/METADATA +3 -3
- {pynamicalsys-1.2.2.dist-info → pynamicalsys-1.3.0.dist-info}/RECORD +14 -14
- {pynamicalsys-1.2.2.dist-info → pynamicalsys-1.3.0.dist-info}/WHEEL +0 -0
- {pynamicalsys-1.2.2.dist-info → pynamicalsys-1.3.0.dist-info}/top_level.txt +0 -0
@@ -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
|
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
|
-
"""
|
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 `
|
232
|
-
If `
|
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 =
|
506
|
-
>>> transient_time =
|
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
|
509
|
-
array([
|
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.
|
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
|
-
(
|
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])
|