pynamicalsys 1.0.1__py3-none-any.whl → 1.2.2__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.
@@ -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
- """To be implemented: Continuous Dynamical Systems (CDS) class."""
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])