pynamicalsys 1.0.1__py3-none-any.whl → 1.2.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 +8 -1
- pynamicalsys/__version__.py +2 -2
- pynamicalsys/continuous_time/chaotic_indicators.py +347 -0
- pynamicalsys/continuous_time/models.py +240 -0
- pynamicalsys/continuous_time/numerical_integrators.py +337 -0
- pynamicalsys/continuous_time/trajectory_analysis.py +163 -0
- pynamicalsys/continuous_time/validators.py +114 -0
- pynamicalsys/core/continuous_dynamical_systems.py +777 -1
- pynamicalsys/core/discrete_dynamical_systems.py +44 -45
- pynamicalsys/discrete_time/trajectory_analysis.py +3 -2
- {pynamicalsys-1.0.1.dist-info → pynamicalsys-1.2.1.dist-info}/METADATA +15 -3
- {pynamicalsys-1.0.1.dist-info → pynamicalsys-1.2.1.dist-info}/RECORD +14 -9
- {pynamicalsys-1.0.1.dist-info → pynamicalsys-1.2.1.dist-info}/WHEEL +0 -0
- {pynamicalsys-1.0.1.dist-info → pynamicalsys-1.2.1.dist-info}/top_level.txt +0 -0
@@ -15,4 +15,780 @@
|
|
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
|
-
|
18
|
+
from numbers import Integral, Real
|
19
|
+
from typing import Any, Callable, Dict, List, Optional, Sequence, Union
|
20
|
+
|
21
|
+
import numpy as np
|
22
|
+
from numpy.typing import NDArray
|
23
|
+
|
24
|
+
from pynamicalsys.common.utils import householder_qr, qr
|
25
|
+
from pynamicalsys.continuous_time.chaotic_indicators import (
|
26
|
+
LDI,
|
27
|
+
SALI,
|
28
|
+
lyapunov_exponents,
|
29
|
+
)
|
30
|
+
from pynamicalsys.continuous_time.models import (
|
31
|
+
henon_heiles,
|
32
|
+
henon_heiles_jacobian,
|
33
|
+
lorenz_jacobian,
|
34
|
+
lorenz_system,
|
35
|
+
rossler_system,
|
36
|
+
rossler_system_4D,
|
37
|
+
rossler_system_4D_jacobian,
|
38
|
+
rossler_system_jacobian,
|
39
|
+
)
|
40
|
+
from pynamicalsys.continuous_time.numerical_integrators import (
|
41
|
+
estimate_initial_step,
|
42
|
+
rk4_step_wrapped,
|
43
|
+
rk45_step_wrapped,
|
44
|
+
)
|
45
|
+
from pynamicalsys.continuous_time.trajectory_analysis import (
|
46
|
+
ensemble_trajectories,
|
47
|
+
evolve_system,
|
48
|
+
generate_trajectory,
|
49
|
+
)
|
50
|
+
from pynamicalsys.continuous_time.validators import (
|
51
|
+
validate_initial_conditions,
|
52
|
+
validate_non_negative,
|
53
|
+
validate_parameters,
|
54
|
+
validate_times,
|
55
|
+
)
|
56
|
+
|
57
|
+
|
58
|
+
class ContinuousDynamicalSystem:
|
59
|
+
|
60
|
+
__AVAILABLE_MODELS: Dict[str, Dict[str, Any]] = {
|
61
|
+
"lorenz system": {
|
62
|
+
"description": "3D Lorenz system",
|
63
|
+
"has_jacobian": True,
|
64
|
+
"has_variational_equations": True,
|
65
|
+
"equations_of_motion": lorenz_system,
|
66
|
+
"jacobian": lorenz_jacobian,
|
67
|
+
"dimension": 3,
|
68
|
+
"number_of_parameters": 3,
|
69
|
+
"parameters": ["sigma", "rho", "beta"],
|
70
|
+
},
|
71
|
+
"henon heiles": {
|
72
|
+
"description": "Two d.o.f. Hénon-Heiles system",
|
73
|
+
"has_jacobian": True,
|
74
|
+
"has_variational_equations": True,
|
75
|
+
"equations_of_motion": henon_heiles,
|
76
|
+
"jacobian": henon_heiles_jacobian,
|
77
|
+
"dimension": 4,
|
78
|
+
"number_of_parameters": 0,
|
79
|
+
"parameters": [],
|
80
|
+
},
|
81
|
+
"rossler system": {
|
82
|
+
"description": "3D Rössler system",
|
83
|
+
"has_jacobian": True,
|
84
|
+
"has_variational_equations": True,
|
85
|
+
"equations_of_motion": rossler_system,
|
86
|
+
"jacobian": rossler_system_jacobian,
|
87
|
+
"dimension": 3,
|
88
|
+
"number_of_parameters": 3,
|
89
|
+
"parameters": ["a", "b", "c"],
|
90
|
+
},
|
91
|
+
"4d rossler system": {
|
92
|
+
"description": "4D Rössler system",
|
93
|
+
"has_jacobian": True,
|
94
|
+
"has_variational_equations": True,
|
95
|
+
"equations_of_motion": rossler_system_4D,
|
96
|
+
"jacobian": rossler_system_4D_jacobian,
|
97
|
+
"dimension": 4,
|
98
|
+
"number_of_parameters": 4,
|
99
|
+
"parameters": ["a", "b", "c", "d"],
|
100
|
+
},
|
101
|
+
}
|
102
|
+
|
103
|
+
__AVAILABLE_INTEGRATORS: Dict[str, Dict[str, Any]] = {
|
104
|
+
"rk4": {
|
105
|
+
"description": "4th order Runge-Kutta method with fixed step size",
|
106
|
+
"integrator": rk4_step_wrapped,
|
107
|
+
"estimate_initial_step": False,
|
108
|
+
},
|
109
|
+
"rk45": {
|
110
|
+
"description": "Adaptive 4th/5th order Runge-Kutta-Fehlberg method (RK45) with embedded error estimation",
|
111
|
+
"integrator": rk45_step_wrapped,
|
112
|
+
"estimate_initial_step": True,
|
113
|
+
},
|
114
|
+
}
|
115
|
+
|
116
|
+
def __init__(
|
117
|
+
self,
|
118
|
+
model: Optional[str] = None,
|
119
|
+
equations_of_motion: Optional[Callable] = None,
|
120
|
+
jacobian: Optional[Callable] = None,
|
121
|
+
system_dimension: Optional[int] = None,
|
122
|
+
number_of_parameters: Optional[int] = None,
|
123
|
+
) -> None:
|
124
|
+
|
125
|
+
if model is not None and equations_of_motion is not None:
|
126
|
+
raise ValueError("Cannot specify both model and custom system")
|
127
|
+
|
128
|
+
if model is not None:
|
129
|
+
model = model.lower()
|
130
|
+
if model not in self.__AVAILABLE_MODELS:
|
131
|
+
available = "\n".join(
|
132
|
+
f"- {name}: {info['description']}"
|
133
|
+
for name, info in self.__AVAILABLE_MODELS.items()
|
134
|
+
)
|
135
|
+
raise ValueError(
|
136
|
+
f"Model '{model}' not implemented. Available models:\n{available}"
|
137
|
+
)
|
138
|
+
|
139
|
+
model_info = self.__AVAILABLE_MODELS[model]
|
140
|
+
self.__model = model
|
141
|
+
self.__equations_of_motion = model_info["equations_of_motion"]
|
142
|
+
self.__jacobian = model_info["jacobian"]
|
143
|
+
self.__system_dimension = model_info["dimension"]
|
144
|
+
self.__number_of_parameters = model_info["number_of_parameters"]
|
145
|
+
|
146
|
+
if jacobian is not None:
|
147
|
+
self.__jacobian = jacobian
|
148
|
+
|
149
|
+
elif (
|
150
|
+
equations_of_motion is not None
|
151
|
+
and system_dimension is not None
|
152
|
+
and number_of_parameters is not None
|
153
|
+
):
|
154
|
+
self.__equations_of_motion = equations_of_motion
|
155
|
+
self.__jacobian = jacobian
|
156
|
+
|
157
|
+
validate_non_negative(system_dimension, "system_dimension", Integral)
|
158
|
+
validate_non_negative(
|
159
|
+
number_of_parameters, "number_of_parameters", Integral
|
160
|
+
)
|
161
|
+
|
162
|
+
self.__system_dimension = system_dimension
|
163
|
+
self.__number_of_parameters = number_of_parameters
|
164
|
+
|
165
|
+
if not callable(self.__equations_of_motion):
|
166
|
+
raise TypeError("Custom mapping must be callable")
|
167
|
+
|
168
|
+
if self.__jacobian is not None and not callable(self.__jacobian):
|
169
|
+
raise TypeError("Custom Jacobian must be callable or None")
|
170
|
+
else:
|
171
|
+
raise ValueError(
|
172
|
+
"Must specify either a model name or custom system function with its dimension and number of paramters."
|
173
|
+
)
|
174
|
+
|
175
|
+
self.__integrator = "rk4"
|
176
|
+
self.__integrator_func = rk4_step_wrapped
|
177
|
+
self.__time_step = 1e-2
|
178
|
+
self.__atol = 1e-6
|
179
|
+
self.__rtol = 1e-3
|
180
|
+
|
181
|
+
@classmethod
|
182
|
+
def available_models(cls) -> List[str]:
|
183
|
+
"""Return a list of available models."""
|
184
|
+
return list(cls.__AVAILABLE_MODELS.keys())
|
185
|
+
|
186
|
+
@classmethod
|
187
|
+
def available_integrators(cls) -> List[str]:
|
188
|
+
"""Return a list of available integrators."""
|
189
|
+
return list(cls.__AVAILABLE_INTEGRATORS.keys())
|
190
|
+
|
191
|
+
@property
|
192
|
+
def info(self) -> Dict[str, Any]:
|
193
|
+
"""Return a dictionary with information about the current model."""
|
194
|
+
|
195
|
+
if self.__model is None:
|
196
|
+
raise ValueError(
|
197
|
+
"The 'info' property is only available when a model is provided."
|
198
|
+
)
|
199
|
+
|
200
|
+
model = self.__model.lower()
|
201
|
+
|
202
|
+
return self.__AVAILABLE_MODELS[model]
|
203
|
+
|
204
|
+
@property
|
205
|
+
def integrator_info(self):
|
206
|
+
"""Return the information about the current integrator"""
|
207
|
+
integrator = self.__integrator.lower()
|
208
|
+
|
209
|
+
return self.__AVAILABLE_INTEGRATORS[integrator]
|
210
|
+
|
211
|
+
def integrator(self, integrator, time_step=1e-2, atol=1e-6, rtol=1e-3):
|
212
|
+
"""Set the integrator to use in the simulation.
|
213
|
+
|
214
|
+
Parameters
|
215
|
+
----------
|
216
|
+
integrator : str
|
217
|
+
The integrator name. Available options are 'rk4' and 'rk45'
|
218
|
+
time_step : float, optional
|
219
|
+
The integration time step when `integrator='rk4'`, by default 1e-2
|
220
|
+
atol : float, optional
|
221
|
+
The absolute tolerance used when `integrator='rk45'`, by default 1e-6
|
222
|
+
rtol : float, optional
|
223
|
+
The relative tolerance used when `integrator='rk45'`, by default 1e-3
|
224
|
+
|
225
|
+
Raises
|
226
|
+
------
|
227
|
+
ValueError
|
228
|
+
If `time_step`, `atol`, or `rtol` are negative.
|
229
|
+
If `integrator` is not available.
|
230
|
+
TypeError
|
231
|
+
If `time_step`, `atol`, or `rtol` are not valid numbers.
|
232
|
+
If `integrator` is not a string.
|
233
|
+
|
234
|
+
Examples
|
235
|
+
--------
|
236
|
+
>>> from pynamicalsys import ContinuousDynamicalSystem as cds
|
237
|
+
>>> cds.available_integrators()
|
238
|
+
['rk4', 'rk45']
|
239
|
+
>>> ds = cds(model="lorenz system")
|
240
|
+
>>> ds.integrator("rk4", time_step=0.001) # To use the RK4 integrator
|
241
|
+
>>> ds.integrator("rk45", atol=1e-10, rtol=1e-8) # To use the RK45 integrator
|
242
|
+
"""
|
243
|
+
validate_non_negative(time_step, "time_step", type_=Real)
|
244
|
+
validate_non_negative(atol, "atol", type_=Real)
|
245
|
+
validate_non_negative(rtol, "rtol", type_=Real)
|
246
|
+
|
247
|
+
if integrator in self.__AVAILABLE_INTEGRATORS:
|
248
|
+
self.__integrator = integrator.lower()
|
249
|
+
integrator_info = self.__AVAILABLE_INTEGRATORS[self.__integrator]
|
250
|
+
self.__integrator_func = integrator_info["integrator"]
|
251
|
+
self.__time_step = time_step
|
252
|
+
self.__atol = atol
|
253
|
+
self.__rtol = rtol
|
254
|
+
|
255
|
+
else:
|
256
|
+
integrator = integrator.lower()
|
257
|
+
if integrator not in self.__AVAILABLE_INTEGRATORS:
|
258
|
+
available = "\n".join(
|
259
|
+
f"- {name}: {info['description']}"
|
260
|
+
for name, info in self.__AVAILABLE_INTEGRATORS.items()
|
261
|
+
)
|
262
|
+
raise ValueError(
|
263
|
+
f"Integrator '{integrator}' not implemented. Available integrators:\n{available}"
|
264
|
+
)
|
265
|
+
|
266
|
+
def __get_initial_time_step(self, u, parameters):
|
267
|
+
if self.integrator_info["estimate_initial_step"]:
|
268
|
+
time_step = estimate_initial_step(
|
269
|
+
0.0,
|
270
|
+
u,
|
271
|
+
parameters,
|
272
|
+
self.__equations_of_motion,
|
273
|
+
atol=self.__atol,
|
274
|
+
rtol=self.__rtol,
|
275
|
+
)
|
276
|
+
else:
|
277
|
+
time_step = self.__time_step
|
278
|
+
|
279
|
+
return time_step
|
280
|
+
|
281
|
+
def evolve_system(
|
282
|
+
self,
|
283
|
+
u: NDArray[np.float64],
|
284
|
+
total_time: float,
|
285
|
+
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
286
|
+
) -> NDArray[np.float64]:
|
287
|
+
"""
|
288
|
+
Evolve the dynamical system from the given initial conditions over a specified time period.
|
289
|
+
|
290
|
+
Parameters
|
291
|
+
----------
|
292
|
+
u : NDArray[np.float64]
|
293
|
+
Initial conditions of the system. Must match the system's dimension.
|
294
|
+
total_time : float
|
295
|
+
Total time over which to evolve the system.
|
296
|
+
parameters : Union[None, Sequence[float], NDArray[np.float64]], optional
|
297
|
+
Parameters of the system, by default None. Can be a scalar, a sequence of floats or a numpy array.
|
298
|
+
|
299
|
+
Returns
|
300
|
+
-------
|
301
|
+
result : NDArray[np.float64]
|
302
|
+
The state of the system at time = total_time.
|
303
|
+
|
304
|
+
Raises
|
305
|
+
------
|
306
|
+
ValueError
|
307
|
+
- If the initial condition is not valid, i.e., if the dimensions do not match.
|
308
|
+
- If the number of parameters does not match.
|
309
|
+
- If `parameters` is not a scalar, 1D list, or 1D array.
|
310
|
+
TypeError
|
311
|
+
- If `total_time` is not a valid number.
|
312
|
+
|
313
|
+
Examples
|
314
|
+
--------
|
315
|
+
>>> from pynamicalsys import ContinuousDynamicalSystem as cds
|
316
|
+
>>> ds = cds(model="lorenz system")
|
317
|
+
>>> ds.integrator("rk4", time_step=0.01)
|
318
|
+
>>> parameters = [10, 28, 8/3]
|
319
|
+
>>> u = [1.0, 1.0, 1.0]
|
320
|
+
>>> total_time = 10
|
321
|
+
>>> ds.evolve_system(u, total_time, parameters=parameters)
|
322
|
+
>>> ds.integrator("rk45", atol=1e-8, rtol=1e-6)
|
323
|
+
>>> ds.evolve_system(u, total_time, parameters=parameters)
|
324
|
+
"""
|
325
|
+
|
326
|
+
u = validate_initial_conditions(u, self.__system_dimension)
|
327
|
+
u = u.copy()
|
328
|
+
|
329
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
330
|
+
|
331
|
+
_, total_time = validate_times(1, total_time)
|
332
|
+
|
333
|
+
time_step = self.__get_initial_time_step(u, parameters)
|
334
|
+
|
335
|
+
total_time += time_step
|
336
|
+
|
337
|
+
return evolve_system(
|
338
|
+
u,
|
339
|
+
parameters,
|
340
|
+
total_time,
|
341
|
+
self.__equations_of_motion,
|
342
|
+
time_step=time_step,
|
343
|
+
atol=self.__atol,
|
344
|
+
rtol=self.__rtol,
|
345
|
+
integrator=self.__integrator_func,
|
346
|
+
)
|
347
|
+
|
348
|
+
def trajectory(
|
349
|
+
self,
|
350
|
+
u: NDArray[np.float64],
|
351
|
+
total_time: float,
|
352
|
+
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
353
|
+
transient_time: Optional[float] = None,
|
354
|
+
) -> NDArray[np.float64]:
|
355
|
+
"""
|
356
|
+
Compute the trajectory of the dynamical system over a specified time period.
|
357
|
+
|
358
|
+
Parameters
|
359
|
+
----------
|
360
|
+
u : NDArray[np.float64]
|
361
|
+
Initial conditions of the system. Must match the system's dimension.
|
362
|
+
total_time : float
|
363
|
+
Total time over which to evolve the system (including transient).
|
364
|
+
parameters : Union[None, Sequence[float], NDArray[np.float64]], optional
|
365
|
+
Parameters of the system, by default None. Can be a scalar, a sequence of floats or a numpy array.
|
366
|
+
transient_time : float
|
367
|
+
Initial time to discard.
|
368
|
+
|
369
|
+
Returns
|
370
|
+
-------
|
371
|
+
result : NDArray[np.float64]
|
372
|
+
The trajectory of the system.
|
373
|
+
|
374
|
+
- For a single initial condition (u.ndim = 1), return a 2D array of shape (number_of_steps, neq + 1), where the first column is the time samples and the remaining columns are the coordinates of the system
|
375
|
+
- For multiple initial conditions (u.ndim = 2), return a 3D array of shape (num_ic, number_of_steps, neq + 1).
|
376
|
+
|
377
|
+
Raises
|
378
|
+
------
|
379
|
+
ValueError
|
380
|
+
- If the initial condition is not valid, i.e., if the dimensions do not match.
|
381
|
+
- If the number of parameters does not match.
|
382
|
+
- If `parameters` is not a scalar, 1D list, or 1D array.
|
383
|
+
TypeError
|
384
|
+
- If `total_time` or `transient_time` are not valid numbers.
|
385
|
+
|
386
|
+
Examples
|
387
|
+
--------
|
388
|
+
>>> from pynamicalsys import ContinuousDynamicalSystem as cds
|
389
|
+
>>> ds = cds(model="lorenz system")
|
390
|
+
>>> u = [0.1, 0.1, 0.1] # Initial condition
|
391
|
+
>>> parameters = [10, 28, 8/3]
|
392
|
+
>>> total_time = 700
|
393
|
+
>>> transient_time = 500
|
394
|
+
>>> trajectory = ds.trajectory(u, total_time, parameters=parameters, transient_time=transient_time)
|
395
|
+
(11000, 4)
|
396
|
+
>>> u = [[0.1, 0.1, 0.1],
|
397
|
+
... [0.2, 0.2, 0.2],
|
398
|
+
... [0.3, 0.3, 0.3]] # Three initial conditions
|
399
|
+
>>> trajectories = ds.trajectory(u, total_time, parameters=parameters, transient_time=transient_time)
|
400
|
+
(3, 20000, 4)
|
401
|
+
"""
|
402
|
+
|
403
|
+
u = validate_initial_conditions(u, self.__system_dimension)
|
404
|
+
u = u.copy()
|
405
|
+
|
406
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
407
|
+
|
408
|
+
transient_time, total_time = validate_times(transient_time, total_time)
|
409
|
+
|
410
|
+
time_step = self.__get_initial_time_step(u, parameters)
|
411
|
+
|
412
|
+
if u.ndim == 1:
|
413
|
+
result = generate_trajectory(
|
414
|
+
u,
|
415
|
+
parameters,
|
416
|
+
total_time,
|
417
|
+
self.__equations_of_motion,
|
418
|
+
transient_time=transient_time,
|
419
|
+
time_step=time_step,
|
420
|
+
atol=self.__atol,
|
421
|
+
rtol=self.__rtol,
|
422
|
+
integrator=self.__integrator_func,
|
423
|
+
)
|
424
|
+
return np.array(result)
|
425
|
+
else:
|
426
|
+
return ensemble_trajectories(
|
427
|
+
u,
|
428
|
+
parameters,
|
429
|
+
total_time,
|
430
|
+
self.__equations_of_motion,
|
431
|
+
transient_time=transient_time,
|
432
|
+
time_step=time_step,
|
433
|
+
atol=self.__atol,
|
434
|
+
rtol=self.__rtol,
|
435
|
+
integrator=self.__integrator_func,
|
436
|
+
)
|
437
|
+
|
438
|
+
def lyapunov(
|
439
|
+
self,
|
440
|
+
u: NDArray[np.float64],
|
441
|
+
total_time: float,
|
442
|
+
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
443
|
+
transient_time: Optional[float] = None,
|
444
|
+
return_history: bool = False,
|
445
|
+
seed: int = 13,
|
446
|
+
log_base: float = np.e,
|
447
|
+
method: str = "QR",
|
448
|
+
endpoint: bool = True,
|
449
|
+
) -> NDArray[np.float64]:
|
450
|
+
"""Calculate the Lyapunov exponents of a given dynamical system.
|
451
|
+
|
452
|
+
The Lyapunov exponent is a key concept in the study of dynamical systems. It measures the average rate at which nearby trajectories in the system diverge (or converge) over time. In simple terms, it quantifies how sensitive a system is to initial conditions.
|
453
|
+
|
454
|
+
Parameters
|
455
|
+
----------
|
456
|
+
u : NDArray[np.float64]
|
457
|
+
Initial conditions of the system. Must match the system's dimension.
|
458
|
+
total_time : float
|
459
|
+
Total time over which to evolve the system (including transient).
|
460
|
+
parameters : Union[None, Sequence[float], NDArray[np.float64]], optional
|
461
|
+
Parameters of the system, by default None. Can be a scalar, a sequence of floats or a numpy array.
|
462
|
+
transient_time : Optional[float], optional
|
463
|
+
Transient time, i.e., the time to discard before calculating the Lyapunov exponents, by default None.
|
464
|
+
return_history : bool, optional
|
465
|
+
Whether to return or not the Lyapunov exponents history in time, by default False.
|
466
|
+
seed : int, optional
|
467
|
+
The seed to randomly generate the deviation vectors, by default 13.
|
468
|
+
log_base : int, optional
|
469
|
+
The base of the logarithm function, by default np.e, i.e., natural log.
|
470
|
+
method : str, optional
|
471
|
+
The method used to calculate the QR decomposition, by default "QR". Set to "QR_HH" to use Householder reflections.
|
472
|
+
endpoint : bool, optional
|
473
|
+
Whether to include the endpoint time = total_time in the calculation, by default True.
|
474
|
+
|
475
|
+
Returns
|
476
|
+
-------
|
477
|
+
NDArray[np.float64]
|
478
|
+
The Lyapunov exponents.
|
479
|
+
|
480
|
+
- If `return_history = False`, return the Lyapunov exponents' final value.
|
481
|
+
- If `return_history = True`, return the time series of each exponent together with the time samples.
|
482
|
+
- If `sample_times` is provided, return the Lyapunov exponents at the specified times.
|
483
|
+
|
484
|
+
Raises
|
485
|
+
------
|
486
|
+
ValueError
|
487
|
+
- If the Jacobian function is not provided.
|
488
|
+
- If the initial condition is not valid, i.e., if the dimensions do not match.
|
489
|
+
- If the number of parameters does not match.
|
490
|
+
- If `parameters` is not a scalar, 1D list, or 1D array.
|
491
|
+
TypeError
|
492
|
+
- If `method` is not a string.
|
493
|
+
- If `total_time`, `transient_time`, or `log_base` are not valid numbers.
|
494
|
+
- If `seed` is not an integer.
|
495
|
+
|
496
|
+
Notes
|
497
|
+
-----
|
498
|
+
- By default, the method uses the modified Gram-Schimdt algorithm to perform the QR decomposition. If your problem requires a higher numerical stability (e.g. large-scale problem), you can set `method=QR_HH` to use Householder reflections instead.
|
499
|
+
|
500
|
+
Examples
|
501
|
+
--------
|
502
|
+
>>> from pynamicalsys import ContinuousDynamicalSystem as cds
|
503
|
+
>>> ds = cds(model="lorenz system")
|
504
|
+
>>> u = [0.1, 0.1, 0.1]
|
505
|
+
>>> total_time = 1000
|
506
|
+
>>> transient_time = 500
|
507
|
+
>>> 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])
|
510
|
+
>>> 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])
|
512
|
+
"""
|
513
|
+
|
514
|
+
if self.__jacobian is None:
|
515
|
+
raise ValueError(
|
516
|
+
"Jacobian function is required to compute Lyapunov exponents"
|
517
|
+
)
|
518
|
+
|
519
|
+
u = validate_initial_conditions(
|
520
|
+
u, self.__system_dimension, allow_ensemble=False
|
521
|
+
)
|
522
|
+
u = u.copy()
|
523
|
+
|
524
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
525
|
+
|
526
|
+
transient_time, total_time = validate_times(transient_time, total_time)
|
527
|
+
|
528
|
+
time_step = self.__get_initial_time_step(u, parameters)
|
529
|
+
|
530
|
+
if endpoint:
|
531
|
+
total_time += time_step
|
532
|
+
|
533
|
+
if not isinstance(method, str):
|
534
|
+
raise TypeError("method must be a string")
|
535
|
+
|
536
|
+
method = method.upper()
|
537
|
+
if method == "QR":
|
538
|
+
qr_func = qr
|
539
|
+
elif method == "QR_HH":
|
540
|
+
qr_func = householder_qr
|
541
|
+
else:
|
542
|
+
raise ValueError("method must be QR or QR_HH")
|
543
|
+
|
544
|
+
validate_non_negative(log_base, "log_base", Real)
|
545
|
+
if log_base == 1:
|
546
|
+
raise ValueError("The logarithm function is not defined with base 1")
|
547
|
+
|
548
|
+
result = lyapunov_exponents(
|
549
|
+
u,
|
550
|
+
parameters,
|
551
|
+
total_time,
|
552
|
+
self.__equations_of_motion,
|
553
|
+
self.__jacobian,
|
554
|
+
transient_time=transient_time,
|
555
|
+
time_step=time_step,
|
556
|
+
atol=self.__atol,
|
557
|
+
rtol=self.__rtol,
|
558
|
+
integrator=self.__integrator_func,
|
559
|
+
return_history=return_history,
|
560
|
+
seed=seed,
|
561
|
+
log_base=log_base,
|
562
|
+
QR=qr_func,
|
563
|
+
)
|
564
|
+
if return_history:
|
565
|
+
return np.array(result)
|
566
|
+
else:
|
567
|
+
return np.array(result[0])
|
568
|
+
|
569
|
+
def SALI(
|
570
|
+
self,
|
571
|
+
u: NDArray[np.float64],
|
572
|
+
total_time: float,
|
573
|
+
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
574
|
+
transient_time: Optional[float] = None,
|
575
|
+
return_history: bool = False,
|
576
|
+
seed: int = 13,
|
577
|
+
threshold: float = 1e-16,
|
578
|
+
endpoint: bool = True,
|
579
|
+
) -> NDArray[np.float64]:
|
580
|
+
"""Calculate the smallest aligment index (SALI) for a given dynamical system.
|
581
|
+
|
582
|
+
Parameters
|
583
|
+
----------
|
584
|
+
u : NDArray[np.float64]
|
585
|
+
Initial conditions of the system. Must match the system's dimension.
|
586
|
+
total_time : float
|
587
|
+
Total time over which to evolve the system (including transient).
|
588
|
+
parameters : Union[None, Sequence[float], NDArray[np.float64]], optional
|
589
|
+
Parameters of the system, by default None. Can be a scalar, a sequence of floats or a numpy array.
|
590
|
+
transient_time : Optional[float], optional
|
591
|
+
Transient time, i.e., the time to discard before calculating the Lyapunov exponents, by default None.
|
592
|
+
return_history : bool, optional
|
593
|
+
Whether to return or not the Lyapunov exponents history in time, by default False.
|
594
|
+
seed : int, optional
|
595
|
+
The seed to randomly generate the deviation vectors, by default 13.
|
596
|
+
threshold : float, optional
|
597
|
+
The threhshold for early termination, by default 1e-16. When SALI becomes less than `threshold`, stops the execution.
|
598
|
+
endpoint : bool, optional
|
599
|
+
Whether to include the endpoint time = total_time in the calculation, by default True.
|
600
|
+
|
601
|
+
Returns
|
602
|
+
-------
|
603
|
+
NDArray[np.float64]
|
604
|
+
The SALI value
|
605
|
+
|
606
|
+
- If `return_history = False`, return time and SALI, where time is the time at the end of the execution. time < total_time if SALI becomes less than `threshold` before `total_time`.
|
607
|
+
- If `return_history = True`, return the sampled times and the SALI values.
|
608
|
+
- If `sample_times` is provided, return the SALI at the specified times.
|
609
|
+
|
610
|
+
Raises
|
611
|
+
------
|
612
|
+
ValueError
|
613
|
+
- If the Jacobian function is not provided.
|
614
|
+
- If the initial condition is not valid, i.e., if the dimensions do not match.
|
615
|
+
- If the number of parameters does not match.
|
616
|
+
- If `parameters` is not a scalar, 1D list, or 1D array.
|
617
|
+
- If `total_time`, `transient_time`, or `threshold` are negative.
|
618
|
+
TypeError
|
619
|
+
- If `total_time`, `transient_time`, or `threshold` are not valid numbers.
|
620
|
+
- If `seed` is not an integer.
|
621
|
+
|
622
|
+
Examples
|
623
|
+
--------
|
624
|
+
>>> from pynamicalsys import ContinuousDynamicalSystem as cds
|
625
|
+
>>> ds = cds(model="lorenz system")
|
626
|
+
>>> u = [0.1, 0.1, 0.1]
|
627
|
+
>>> total_time = 1000
|
628
|
+
>>> transient_time = 500
|
629
|
+
>>> parameters = [16.0, 45.92, 4.0]
|
630
|
+
>>> ds.SALI(u, total_time, parameters=parameters, transient_time=transient_time)
|
631
|
+
(521.8899999999801, 7.850462293418876e-17)
|
632
|
+
>>> # Returning the history
|
633
|
+
>>> sali = ds.SALI(u, total_time, parameters=parameters, transient_time=transient_time, return_history=True)
|
634
|
+
>>> sali.shape
|
635
|
+
(2189, 2)
|
636
|
+
"""
|
637
|
+
|
638
|
+
if self.__jacobian is None:
|
639
|
+
raise ValueError(
|
640
|
+
"Jacobian function is required to compute Lyapunov exponents"
|
641
|
+
)
|
642
|
+
|
643
|
+
u = validate_initial_conditions(
|
644
|
+
u, self.__system_dimension, allow_ensemble=False
|
645
|
+
)
|
646
|
+
u = u.copy()
|
647
|
+
|
648
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
649
|
+
|
650
|
+
transient_time, total_time = validate_times(transient_time, total_time)
|
651
|
+
|
652
|
+
time_step = self.__get_initial_time_step(u, parameters)
|
653
|
+
|
654
|
+
validate_non_negative(threshold, "threshold", type_=Real)
|
655
|
+
|
656
|
+
if endpoint:
|
657
|
+
total_time += time_step
|
658
|
+
|
659
|
+
result = SALI(
|
660
|
+
u,
|
661
|
+
parameters,
|
662
|
+
total_time,
|
663
|
+
self.__equations_of_motion,
|
664
|
+
self.__jacobian,
|
665
|
+
transient_time=transient_time,
|
666
|
+
time_step=time_step,
|
667
|
+
atol=self.__atol,
|
668
|
+
rtol=self.__rtol,
|
669
|
+
integrator=self.__integrator_func,
|
670
|
+
return_history=return_history,
|
671
|
+
seed=seed,
|
672
|
+
threshold=threshold,
|
673
|
+
)
|
674
|
+
|
675
|
+
if return_history:
|
676
|
+
return np.array(result)
|
677
|
+
else:
|
678
|
+
return np.array(result[0])
|
679
|
+
|
680
|
+
def LDI(
|
681
|
+
self,
|
682
|
+
u: NDArray[np.float64],
|
683
|
+
total_time: float,
|
684
|
+
k: int,
|
685
|
+
parameters: Union[None, Sequence[float], NDArray[np.float64]] = None,
|
686
|
+
transient_time: Optional[float] = None,
|
687
|
+
return_history: bool = False,
|
688
|
+
seed: int = 13,
|
689
|
+
threshold: float = 1e-16,
|
690
|
+
endpoint: bool = True,
|
691
|
+
) -> NDArray[np.float64]:
|
692
|
+
"""Calculate the linear dependence index (LDI) for a given dynamical system.
|
693
|
+
|
694
|
+
Parameters
|
695
|
+
----------
|
696
|
+
u : NDArray[np.float64]
|
697
|
+
Initial conditions of the system. Must match the system's dimension.
|
698
|
+
total_time : float
|
699
|
+
Total time over which to evolve the system (including transient).
|
700
|
+
parameters : Union[None, Sequence[float], NDArray[np.float64]], optional
|
701
|
+
Parameters of the system, by default None. Can be a scalar, a sequence of floats or a numpy array.
|
702
|
+
transient_time : Optional[float], optional
|
703
|
+
Transient time, i.e., the time to discard before calculating the Lyapunov exponents, by default None.
|
704
|
+
return_history : bool, optional
|
705
|
+
Whether to return or not the Lyapunov exponents history in time, by default False.
|
706
|
+
seed : int, optional
|
707
|
+
The seed to randomly generate the deviation vectors, by default 13.
|
708
|
+
threshold : float, optional
|
709
|
+
The threhshold for early termination, by default 1e-16. When SALI becomes less than `threshold`, stops the execution.
|
710
|
+
endpoint : bool, optional
|
711
|
+
Whether to include the endpoint time = total_time in the calculation, by default True.
|
712
|
+
|
713
|
+
Returns
|
714
|
+
-------
|
715
|
+
NDArray[np.float64]
|
716
|
+
The LDI value
|
717
|
+
|
718
|
+
- 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
|
+
- 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
|
+
|
722
|
+
Raises
|
723
|
+
------
|
724
|
+
ValueError
|
725
|
+
- If the Jacobian function is not provided.
|
726
|
+
- If the initial condition is not valid, i.e., if the dimensions do not match.
|
727
|
+
- If the number of parameters does not match.
|
728
|
+
- If `parameters` is not a scalar, 1D list, or 1D array.
|
729
|
+
- If `total_time`, `transient_time`, or `threshold` are negative.
|
730
|
+
- If `k` < 2.
|
731
|
+
TypeError
|
732
|
+
- If `total_time`, `transient_time`, or `threshold` are not valid numbers.
|
733
|
+
- If `seed` is not an integer.
|
734
|
+
|
735
|
+
Examples
|
736
|
+
--------
|
737
|
+
>>> from pynamicalsys import ContinuousDynamicalSystem as cds
|
738
|
+
>>> ds = cds(model="lorenz system")
|
739
|
+
>>> u = [0.1, 0.1, 0.1]
|
740
|
+
>>> total_time = 1000
|
741
|
+
>>> transient_time = 500
|
742
|
+
>>> parameters = [16.0, 45.92, 4.0]
|
743
|
+
>>> ds.LDI(u, total_time, 2, parameters=parameters, transient_time=transient_time)
|
744
|
+
(521.8099999999802, 7.328757804386809e-17)
|
745
|
+
>>> ds.LDI(u, total_time, 3, parameters=parameters, transient_time=transient_time)
|
746
|
+
(501.26999999999884, 9.984145370766051e-17)
|
747
|
+
>>> # Returning the history
|
748
|
+
>>> ldi = ds.LDI(u, total_time, 2, parameters=parameters, transient_time=transient_time)
|
749
|
+
>>> ldi.shape
|
750
|
+
(2181, 2)
|
751
|
+
"""
|
752
|
+
|
753
|
+
if self.__jacobian is None:
|
754
|
+
raise ValueError(
|
755
|
+
"Jacobian function is required to compute Lyapunov exponents"
|
756
|
+
)
|
757
|
+
|
758
|
+
u = validate_initial_conditions(
|
759
|
+
u, self.__system_dimension, allow_ensemble=False
|
760
|
+
)
|
761
|
+
u = u.copy()
|
762
|
+
|
763
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
764
|
+
|
765
|
+
transient_time, total_time = validate_times(transient_time, total_time)
|
766
|
+
|
767
|
+
time_step = self.__get_initial_time_step(u, parameters)
|
768
|
+
|
769
|
+
validate_non_negative(threshold, "threshold", type_=Real)
|
770
|
+
|
771
|
+
if endpoint:
|
772
|
+
total_time += time_step
|
773
|
+
|
774
|
+
result = LDI(
|
775
|
+
u,
|
776
|
+
parameters,
|
777
|
+
total_time,
|
778
|
+
self.__equations_of_motion,
|
779
|
+
self.__jacobian,
|
780
|
+
k,
|
781
|
+
transient_time=transient_time,
|
782
|
+
time_step=time_step,
|
783
|
+
atol=self.__atol,
|
784
|
+
rtol=self.__rtol,
|
785
|
+
integrator=self.__integrator_func,
|
786
|
+
return_history=return_history,
|
787
|
+
seed=seed,
|
788
|
+
threshold=threshold,
|
789
|
+
)
|
790
|
+
|
791
|
+
if return_history:
|
792
|
+
return np.array(result)
|
793
|
+
else:
|
794
|
+
return np.array(result[0])
|