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.
- pynamicalsys/__version__.py +16 -3
- pynamicalsys/common/recurrence_quantification_analysis.py +1 -1
- pynamicalsys/common/utils.py +23 -24
- pynamicalsys/continuous_time/chaotic_indicators.py +176 -80
- pynamicalsys/continuous_time/numerical_integrators.py +8 -8
- pynamicalsys/continuous_time/trajectory_analysis.py +60 -24
- pynamicalsys/core/continuous_dynamical_systems.py +184 -11
- pynamicalsys/core/discrete_dynamical_systems.py +264 -16
- pynamicalsys/discrete_time/dynamical_indicators.py +415 -171
- pynamicalsys/discrete_time/models.py +2 -2
- pynamicalsys/discrete_time/trajectory_analysis.py +10 -10
- pynamicalsys/discrete_time/transport.py +74 -112
- pynamicalsys/discrete_time/validators.py +1 -1
- {pynamicalsys-1.2.2.dist-info → pynamicalsys-1.3.1.dist-info}/METADATA +3 -3
- pynamicalsys-1.3.1.dist-info/RECORD +28 -0
- pynamicalsys-1.2.2.dist-info/RECORD +0 -28
- {pynamicalsys-1.2.2.dist-info → pynamicalsys-1.3.1.dist-info}/WHEEL +0 -0
- {pynamicalsys-1.2.2.dist-info → pynamicalsys-1.3.1.dist-info}/top_level.txt +0 -0
@@ -29,10 +29,10 @@ from pynamicalsys.discrete_time.trajectory_analysis import (
|
|
29
29
|
iterate_mapping,
|
30
30
|
generate_trajectory,
|
31
31
|
)
|
32
|
-
from pynamicalsys.common.utils import qr, householder_qr, fit_poly
|
32
|
+
from pynamicalsys.common.utils import qr, householder_qr, fit_poly, wedge_norm
|
33
33
|
|
34
34
|
|
35
|
-
@njit
|
35
|
+
@njit
|
36
36
|
def lyapunov_1D(
|
37
37
|
u: NDArray[np.float64],
|
38
38
|
parameters: NDArray[np.float64],
|
@@ -41,8 +41,9 @@ def lyapunov_1D(
|
|
41
41
|
derivative_mapping: Callable[
|
42
42
|
[NDArray[np.float64], NDArray[np.float64], Callable], NDArray[np.float64]
|
43
43
|
],
|
44
|
+
num_exponents: int, # Added just to match signature
|
45
|
+
sample_times: Union[NDArray[np.int32], NDArray[np.int64]],
|
44
46
|
return_history: bool = False,
|
45
|
-
sample_times: Optional[NDArray[np.int32]] = None,
|
46
47
|
transient_time: Optional[int] = None,
|
47
48
|
log_base: float = np.e,
|
48
49
|
) -> Union[NDArray[np.float64], float]:
|
@@ -64,10 +65,10 @@ def lyapunov_1D(
|
|
64
65
|
Function defining the system's evolution: `u_next = mapping(u, parameters)`.
|
65
66
|
derivative_mapping : Callable[[NDArray, NDArray, Callable], NDArray]
|
66
67
|
Function returning the derivative of `mapping` (Jacobian for 1D systems).
|
68
|
+
sample_times: Union[NDArray[np.int32], NDArray[np.int64]],
|
69
|
+
Specific time steps to record the exponent (if `return_history=True`).
|
67
70
|
return_history : bool, optional
|
68
71
|
If True, returns the Lyapunov exponent estimate at each step (default: False).
|
69
|
-
sample_times : Optional[NDArray[np.int32]], optional
|
70
|
-
Specific time steps to record the exponent (if `return_history=True`).
|
71
72
|
transient_time : Optional[int], optional
|
72
73
|
Number of initial iterations to discard as transient (default: None).
|
73
74
|
log_base : float, optional
|
@@ -96,33 +97,30 @@ def lyapunov_1D(
|
|
96
97
|
sample_size = total_time
|
97
98
|
|
98
99
|
# Initialize history tracking
|
99
|
-
exponent = 0.0
|
100
100
|
if return_history:
|
101
|
-
if sample_times
|
102
|
-
|
103
|
-
|
104
|
-
history = np.zeros(len(sample_times))
|
105
|
-
count = 0
|
106
|
-
else:
|
107
|
-
history = np.zeros(sample_size)
|
101
|
+
if sample_times.max() > sample_size:
|
102
|
+
raise ValueError("sample_times must be ≤ total_time - transient_time")
|
103
|
+
history = np.zeros(len(sample_times))
|
108
104
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
105
|
+
sample_idx = 0
|
106
|
+
exponent = 0.0
|
107
|
+
prev_i = 0
|
108
|
+
for st in sample_times:
|
109
|
+
steps = st - prev_i
|
110
|
+
for _ in range(steps):
|
111
|
+
u = mapping(u, parameters)
|
112
|
+
du = derivative_mapping(u, parameters, mapping)
|
113
|
+
exponent += np.log(np.abs(du[0, 0])) / np.log(log_base)
|
114
114
|
|
115
115
|
if return_history:
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
history[count] = exponent / i
|
120
|
-
count += 1
|
116
|
+
history[sample_idx] = exponent / st
|
117
|
+
sample_idx += 1
|
118
|
+
prev_i = st
|
121
119
|
|
122
120
|
return history if return_history else np.array([exponent / sample_size])
|
123
121
|
|
124
122
|
|
125
|
-
@njit
|
123
|
+
@njit
|
126
124
|
def lyapunov_er(
|
127
125
|
u: NDArray[np.float64],
|
128
126
|
parameters: NDArray[np.float64],
|
@@ -131,8 +129,9 @@ def lyapunov_er(
|
|
131
129
|
jacobian: Callable[
|
132
130
|
[NDArray[np.float64], NDArray[np.float64], Callable], NDArray[np.float64]
|
133
131
|
],
|
132
|
+
num_exponents: int, # Added just to match signature
|
133
|
+
sample_times: Union[NDArray[np.int32], NDArray[np.int64]],
|
134
134
|
return_history: bool = False,
|
135
|
-
sample_times: Optional[NDArray[np.int32]] = None,
|
136
135
|
transient_time: Optional[int] = None,
|
137
136
|
log_base: float = np.e,
|
138
137
|
) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
|
@@ -154,10 +153,10 @@ def lyapunov_er(
|
|
154
153
|
System evolution function: `u_next = mapping(u, parameters)`.
|
155
154
|
jacobian : Callable[[NDArray, NDArray, Callable], NDArray]
|
156
155
|
Function returning the Jacobian matrix (shape: `(2, 2)`).
|
156
|
+
sample_times: Union[NDArray[np.int32], NDArray[np.int64]],
|
157
|
+
Specific time steps to record exponents (if `return_history=True`).
|
157
158
|
return_history : bool, optional
|
158
159
|
If True, returns exponent convergence history (default: False).
|
159
|
-
sample_times : Optional[NDArray[np.int32]], optional
|
160
|
-
Specific time steps to record exponents (if `return_history=True`).
|
161
160
|
transient_time : Optional[int], optional
|
162
161
|
Number of initial iterations to discard as transient (default: None).
|
163
162
|
log_base : float, optional
|
@@ -202,44 +201,41 @@ def lyapunov_er(
|
|
202
201
|
|
203
202
|
# Initialize history tracking
|
204
203
|
if return_history:
|
205
|
-
if sample_times
|
206
|
-
|
207
|
-
|
208
|
-
history = np.zeros((len(sample_times), neq))
|
209
|
-
count = 0
|
210
|
-
else:
|
211
|
-
history = np.zeros((sample_size, neq))
|
204
|
+
if sample_times.max() > sample_size:
|
205
|
+
raise ValueError("sample_times must be ≤ total_time - transient_time")
|
206
|
+
history = np.zeros((len(sample_times), neq))
|
212
207
|
|
208
|
+
sample_idx = 0
|
213
209
|
eigvals = np.zeros(neq)
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
beta = np.arctan2(-J[1, 0] * cb0 + J[1, 1] * sb0, J[0, 0] * cb0 - J[0, 1] * sb0)
|
222
|
-
|
223
|
-
# Transformation matrix elements
|
224
|
-
cb, sb = np.cos(beta), np.sin(beta)
|
225
|
-
eigvals[0] = (J[0, 0] * cb - J[1, 0] * sb) * cb0 - (
|
226
|
-
J[0, 1] * cb - J[1, 1] * sb
|
227
|
-
) * sb0
|
228
|
-
eigvals[1] = (J[0, 0] * sb + J[1, 0] * cb) * sb0 + (
|
229
|
-
J[0, 1] * sb + J[1, 1] * cb
|
230
|
-
) * cb0
|
231
|
-
|
232
|
-
exponents += np.log(np.abs(eigvals)) / np.log(log_base)
|
233
|
-
|
234
|
-
# Record history if requested
|
235
|
-
if return_history:
|
236
|
-
if sample_times is None:
|
237
|
-
history[i - 1] = exponents / i
|
238
|
-
elif i in sample_times:
|
239
|
-
history[count] = exponents / i
|
240
|
-
count += 1
|
210
|
+
log_base_inv = 1.0 / np.log(log_base)
|
211
|
+
prev_i = 0
|
212
|
+
for st in sample_times:
|
213
|
+
steps = st - prev_i
|
214
|
+
for _ in range(steps):
|
215
|
+
u_contig = mapping(u_contig, parameters)
|
216
|
+
J = jacobian(u_contig, parameters, mapping)
|
241
217
|
|
242
|
-
|
218
|
+
cb0, sb0 = np.cos(beta0), np.sin(beta0)
|
219
|
+
beta = np.arctan2(
|
220
|
+
-J[1, 0] * cb0 + J[1, 1] * sb0, J[0, 0] * cb0 - J[0, 1] * sb0
|
221
|
+
)
|
222
|
+
|
223
|
+
cb, sb = np.cos(beta), np.sin(beta)
|
224
|
+
eigvals[0] = (J[0, 0] * cb - J[1, 0] * sb) * cb0 - (
|
225
|
+
J[0, 1] * cb - J[1, 1] * sb
|
226
|
+
) * sb0
|
227
|
+
eigvals[1] = (J[0, 0] * sb + J[1, 0] * cb) * sb0 + (
|
228
|
+
J[0, 1] * sb + J[1, 1] * cb
|
229
|
+
) * cb0
|
230
|
+
|
231
|
+
exponents += np.log(np.abs(eigvals)) * log_base_inv
|
232
|
+
|
233
|
+
beta0 = beta
|
234
|
+
|
235
|
+
if return_history:
|
236
|
+
history[sample_idx] = exponents / st
|
237
|
+
sample_idx += 1
|
238
|
+
prev_i = st
|
243
239
|
|
244
240
|
# Format output
|
245
241
|
if return_history:
|
@@ -250,7 +246,128 @@ def lyapunov_er(
|
|
250
246
|
return aux_exponents, u_contig
|
251
247
|
|
252
248
|
|
253
|
-
@njit
|
249
|
+
@njit
|
250
|
+
def maximum_lyapunov_er(
|
251
|
+
u: NDArray[np.float64],
|
252
|
+
parameters: NDArray[np.float64],
|
253
|
+
total_time: int,
|
254
|
+
mapping: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
255
|
+
jacobian: Callable[
|
256
|
+
[NDArray[np.float64], NDArray[np.float64], Callable], NDArray[np.float64]
|
257
|
+
],
|
258
|
+
num_exponents: int, # Added just to match signature
|
259
|
+
sample_times: Union[NDArray[np.int32], NDArray[np.int64]],
|
260
|
+
return_history: bool = False,
|
261
|
+
transient_time: Optional[int] = None,
|
262
|
+
log_base: float = np.e,
|
263
|
+
) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
|
264
|
+
"""
|
265
|
+
Compute the maximum Lyapunov exponent using the Eckmann-Ruelle (ER) method for 2D systems.
|
266
|
+
|
267
|
+
This method tracks the evolution of perturbations via continuous QR decomposition
|
268
|
+
using rotational angles, providing numerically stable exponent estimates.
|
269
|
+
|
270
|
+
Parameters
|
271
|
+
----------
|
272
|
+
u : NDArray[np.float64]
|
273
|
+
Initial state vector (shape: `(2,)` for 2D systems).
|
274
|
+
parameters : NDArray[np.float64]
|
275
|
+
System parameters passed to `mapping` and `jacobian`.
|
276
|
+
total_time : int
|
277
|
+
Total number of iterations (time steps) to compute.
|
278
|
+
mapping : Callable[[NDArray, NDArray], NDArray]
|
279
|
+
System evolution function: `u_next = mapping(u, parameters)`.
|
280
|
+
jacobian : Callable[[NDArray, NDArray, Callable], NDArray]
|
281
|
+
Function returning the Jacobian matrix (shape: `(2, 2)`).
|
282
|
+
sample_times: Union[NDArray[np.int32], NDArray[np.int64]],
|
283
|
+
Specific time steps to record exponents (if `return_history=True`).
|
284
|
+
return_history : bool, optional
|
285
|
+
If True, returns exponent convergence history (default: False).
|
286
|
+
transient_time : Optional[int], optional
|
287
|
+
Number of initial iterations to discard as transient (default: None).
|
288
|
+
log_base : float, optional
|
289
|
+
Logarithm base for exponent calculation (default: e).
|
290
|
+
|
291
|
+
Returns
|
292
|
+
-------
|
293
|
+
Tuple[NDArray[np.float64], NDArray[np.float64]]
|
294
|
+
- If `return_history=True`:
|
295
|
+
- `history`: Array of exponent estimates (shape: `(sample_size, 2)` or `(len(sample_times), 2)`)
|
296
|
+
- `final_state`: System state at termination (shape: `(2,)`)
|
297
|
+
- If `return_history=False`:
|
298
|
+
- `exponents`: Final Lyapunov exponents (shape: `(2, 1)`)
|
299
|
+
- `final_state`: System state at termination (shape: `(2,)`)
|
300
|
+
|
301
|
+
Notes
|
302
|
+
-----
|
303
|
+
- **Method**: Uses rotation angles for continuous QR decomposition [1].
|
304
|
+
- **Stability**: More robust than Gram-Schmidt for 2D systems.
|
305
|
+
- **Limitation**: Designed specifically for 2D maps (`neq=2`).
|
306
|
+
- **Numerics**: Exponents are averaged as:
|
307
|
+
λ_i = (1/N) Σ log|T_ii|, where T is the transformation matrix.
|
308
|
+
|
309
|
+
References
|
310
|
+
----------
|
311
|
+
[1] J. Eckmann & D. Ruelle, "Ergodic theory of chaos and strange attractors",
|
312
|
+
Rev. Mod. Phys. 57, 617 (1985).
|
313
|
+
"""
|
314
|
+
|
315
|
+
neq = len(u)
|
316
|
+
exponent = 0.0
|
317
|
+
beta0 = 0.0 # Initial rotation angle
|
318
|
+
u_contig = np.ascontiguousarray(u)
|
319
|
+
|
320
|
+
# Handle transient time
|
321
|
+
if transient_time is not None:
|
322
|
+
sample_size = total_time - transient_time
|
323
|
+
for _ in range(transient_time):
|
324
|
+
u_contig = mapping(u_contig, parameters)
|
325
|
+
else:
|
326
|
+
sample_size = total_time
|
327
|
+
|
328
|
+
# Initialize history tracking
|
329
|
+
if return_history:
|
330
|
+
if sample_times.max() > sample_size:
|
331
|
+
raise ValueError("sample_times must be ≤ total_time - transient_time")
|
332
|
+
history = np.zeros(len(sample_times))
|
333
|
+
|
334
|
+
sample_idx = 0
|
335
|
+
eigval = 0.0
|
336
|
+
log_base_inv = 1.0 / np.log(log_base)
|
337
|
+
prev_i = 0
|
338
|
+
for st in sample_times:
|
339
|
+
steps = st - prev_i
|
340
|
+
for _ in range(steps):
|
341
|
+
u_contig = mapping(u_contig, parameters)
|
342
|
+
J = jacobian(u_contig, parameters, mapping)
|
343
|
+
|
344
|
+
cb0, sb0 = np.cos(beta0), np.sin(beta0)
|
345
|
+
beta = np.arctan2(
|
346
|
+
-J[1, 0] * cb0 + J[1, 1] * sb0, J[0, 0] * cb0 - J[0, 1] * sb0
|
347
|
+
)
|
348
|
+
|
349
|
+
cb, sb = np.cos(beta), np.sin(beta)
|
350
|
+
eigval = (J[0, 0] * cb - J[1, 0] * sb) * cb0 - (
|
351
|
+
J[0, 1] * cb - J[1, 1] * sb
|
352
|
+
) * sb0
|
353
|
+
|
354
|
+
exponent += np.log(np.abs(eigval)) * log_base_inv
|
355
|
+
|
356
|
+
beta0 = beta
|
357
|
+
|
358
|
+
if return_history:
|
359
|
+
history[sample_idx] = exponent / st
|
360
|
+
sample_idx += 1
|
361
|
+
prev_i = st
|
362
|
+
|
363
|
+
# Format output
|
364
|
+
if return_history:
|
365
|
+
return history, u_contig
|
366
|
+
else:
|
367
|
+
return np.array([exponent / sample_size]), u_contig
|
368
|
+
|
369
|
+
|
370
|
+
@njit
|
254
371
|
def lyapunov_qr(
|
255
372
|
u: NDArray[np.float64],
|
256
373
|
parameters: NDArray[np.float64],
|
@@ -259,13 +376,15 @@ def lyapunov_qr(
|
|
259
376
|
jacobian: Callable[
|
260
377
|
[NDArray[np.float64], NDArray[np.float64], Callable], NDArray[np.float64]
|
261
378
|
],
|
379
|
+
num_exponents: int,
|
380
|
+
sample_times: Union[NDArray[np.int32], NDArray[np.int64]],
|
262
381
|
QR: Callable[
|
263
382
|
[NDArray[np.float64]], Tuple[NDArray[np.float64], NDArray[np.float64]]
|
264
383
|
] = qr,
|
265
384
|
return_history: bool = False,
|
266
|
-
sample_times: Optional[NDArray[np.int32]] = None,
|
267
385
|
transient_time: Optional[int] = None,
|
268
386
|
log_base: float = np.e,
|
387
|
+
seed: int = 13,
|
269
388
|
) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
|
270
389
|
"""
|
271
390
|
Compute Lyapunov exponents using QR decomposition (Gram-Schmidt) for N-dimensional systems.
|
@@ -320,11 +439,11 @@ def lyapunov_qr(
|
|
320
439
|
Physica D 16D, 285-317 (1985).
|
321
440
|
"""
|
322
441
|
|
442
|
+
np.random.seed(seed)
|
323
443
|
neq = len(u)
|
324
|
-
v = np.ascontiguousarray(np.random.rand(neq,
|
325
|
-
v = np.ascontiguousarray(np.eye(neq))
|
444
|
+
v = np.ascontiguousarray(np.random.rand(neq, num_exponents))
|
326
445
|
v, _ = qr(v) # Initialize orthonormal vectors
|
327
|
-
exponents = np.zeros(
|
446
|
+
exponents = np.zeros(num_exponents)
|
328
447
|
u_contig = np.ascontiguousarray(u.copy())
|
329
448
|
|
330
449
|
# Handle transient time
|
@@ -337,38 +456,34 @@ def lyapunov_qr(
|
|
337
456
|
|
338
457
|
# Initialize history tracking
|
339
458
|
if return_history:
|
340
|
-
if sample_times
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
v
|
356
|
-
|
357
|
-
exponents += np.log(np.abs(np.diag(R))) / np.log(log_base)
|
459
|
+
if sample_times.max() > sample_size:
|
460
|
+
raise ValueError("sample_times must be ≤ total_time - transient_time")
|
461
|
+
history = np.zeros((len(sample_times), num_exponents))
|
462
|
+
|
463
|
+
sample_idx = 0
|
464
|
+
log_base_inv = 1.0 / np.log(log_base)
|
465
|
+
prev_i = 0
|
466
|
+
for st in sample_times:
|
467
|
+
steps = st - prev_i
|
468
|
+
for _ in range(steps):
|
469
|
+
u_contig = mapping(u_contig, parameters)
|
470
|
+
J = np.ascontiguousarray(jacobian(u_contig, parameters, mapping))
|
471
|
+
# Evolve and orthogonalize vectors
|
472
|
+
for i in range(num_exponents):
|
473
|
+
v[:, i] = np.ascontiguousarray(J) @ np.ascontiguousarray(v[:, i])
|
474
|
+
v, R = QR(v)
|
475
|
+
exponents += np.log(np.abs(np.diag(R))) * log_base_inv
|
358
476
|
|
359
|
-
# Record history if requested
|
360
477
|
if return_history:
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
history[count] = exponents / j
|
365
|
-
count += 1
|
478
|
+
history[sample_idx] = exponents / st
|
479
|
+
sample_idx += 1
|
480
|
+
prev_i = st
|
366
481
|
|
367
482
|
# Format output
|
368
483
|
if return_history:
|
369
484
|
return history, u_contig
|
370
485
|
else:
|
371
|
-
aux_exponents = np.zeros((
|
486
|
+
aux_exponents = np.zeros((num_exponents, 1))
|
372
487
|
aux_exponents[:, 0] = exponents / sample_size
|
373
488
|
return aux_exponents, u_contig
|
374
489
|
|
@@ -382,6 +497,7 @@ def finite_time_lyapunov(
|
|
382
497
|
jacobian: Callable[
|
383
498
|
[NDArray[np.float64], NDArray[np.float64], Callable], NDArray[np.float64]
|
384
499
|
],
|
500
|
+
num_exponents: int,
|
385
501
|
method: str = "QR",
|
386
502
|
transient_time: Optional[int] = None,
|
387
503
|
log_base: float = np.e,
|
@@ -451,18 +567,43 @@ def finite_time_lyapunov(
|
|
451
567
|
|
452
568
|
neq = len(u)
|
453
569
|
num_windows = sample_size // finite_time
|
454
|
-
exponents = np.zeros((num_windows,
|
570
|
+
exponents = np.zeros((num_windows, num_exponents))
|
455
571
|
phase_space_points = np.zeros((num_windows, neq))
|
456
|
-
|
572
|
+
sample_times = np.arange(finite_time)
|
457
573
|
# Compute exponents for each window
|
458
574
|
for i in range(num_windows):
|
459
|
-
if method == "ER":
|
575
|
+
if num_exponents == 1 and method == "ER":
|
576
|
+
window_exponents, u_new = maximum_lyapunov_er(
|
577
|
+
u,
|
578
|
+
parameters,
|
579
|
+
finite_time,
|
580
|
+
mapping,
|
581
|
+
jacobian,
|
582
|
+
num_exponents,
|
583
|
+
sample_times,
|
584
|
+
log_base=log_base,
|
585
|
+
)
|
586
|
+
elif num_exponents > 1 and method == "ER":
|
460
587
|
window_exponents, u_new = lyapunov_er(
|
461
|
-
u,
|
588
|
+
u,
|
589
|
+
parameters,
|
590
|
+
finite_time,
|
591
|
+
mapping,
|
592
|
+
jacobian,
|
593
|
+
num_exponents,
|
594
|
+
sample_times,
|
595
|
+
log_base=log_base,
|
462
596
|
)
|
463
597
|
elif method == "QR":
|
464
598
|
window_exponents, u_new = lyapunov_qr(
|
465
|
-
u,
|
599
|
+
u,
|
600
|
+
parameters,
|
601
|
+
finite_time,
|
602
|
+
mapping,
|
603
|
+
jacobian,
|
604
|
+
num_exponents,
|
605
|
+
sample_times,
|
606
|
+
log_base=log_base,
|
466
607
|
)
|
467
608
|
elif method == "QR_HH":
|
468
609
|
window_exponents, u_new = lyapunov_qr(
|
@@ -471,6 +612,8 @@ def finite_time_lyapunov(
|
|
471
612
|
finite_time,
|
472
613
|
mapping,
|
473
614
|
jacobian,
|
615
|
+
num_exponents,
|
616
|
+
sample_times,
|
474
617
|
QR=householder_qr,
|
475
618
|
log_base=log_base,
|
476
619
|
)
|
@@ -558,7 +701,7 @@ def dig(
|
|
558
701
|
return -np.log10(abs(WB0 - WB1))
|
559
702
|
|
560
703
|
|
561
|
-
@njit
|
704
|
+
@njit
|
562
705
|
def SALI(
|
563
706
|
u: NDArray[np.float64],
|
564
707
|
parameters: NDArray[np.float64],
|
@@ -567,8 +710,8 @@ def SALI(
|
|
567
710
|
jacobian: Callable[
|
568
711
|
[NDArray[np.float64], NDArray[np.float64], Callable], NDArray[np.float64]
|
569
712
|
],
|
713
|
+
sample_times: Union[NDArray[np.int32], NDArray[np.int64]],
|
570
714
|
return_history: bool = False,
|
571
|
-
sample_times: Optional[NDArray[np.int32]] = None,
|
572
715
|
tol: float = 1e-16,
|
573
716
|
transient_time: Optional[int] = None,
|
574
717
|
seed: int = 13,
|
@@ -591,10 +734,10 @@ def SALI(
|
|
591
734
|
Function representing the system's time evolution: `u_next = mapping(u, parameters)`.
|
592
735
|
jacobian : Callable[[NDArray, NDArray, Callable], NDArray]
|
593
736
|
Function computing the Jacobian matrix of `mapping` at state `u`.
|
737
|
+
sample_times: Union[NDArray[np.int32], NDArray[np.int64]],
|
738
|
+
Specific time steps at which to record SALI (if `return_history=True`). Must be sorted.
|
594
739
|
return_history : bool, optional
|
595
740
|
If True, return SALI values at each time step (or `sample_times`). Default: False.
|
596
|
-
sample_times : Optional[NDArray[np.int32]], optional
|
597
|
-
Specific time steps at which to record SALI (if `return_history=True`). Must be sorted.
|
598
741
|
tol : float, optional
|
599
742
|
Tolerance for early stopping if SALI < `tol` (default: 1e-16).
|
600
743
|
transient_time : Optional[int], optional
|
@@ -618,7 +761,7 @@ def SALI(
|
|
618
761
|
- Uses QR decomposition to initialize orthonormal deviation vectors.
|
619
762
|
- Computes both Parallel (PAI) and Antiparallel (AAI) Alignment Indices.
|
620
763
|
- Early termination occurs if SALI < `tol` (indicating chaotic behavior).
|
621
|
-
- Optimized with `@njit
|
764
|
+
- Optimized with `@njit` for performance.
|
622
765
|
"""
|
623
766
|
|
624
767
|
np.random.seed(seed) # For reproducibility
|
@@ -639,48 +782,158 @@ def SALI(
|
|
639
782
|
|
640
783
|
# Initialize history tracking
|
641
784
|
if return_history:
|
642
|
-
if sample_times
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
J = np.ascontiguousarray(jacobian(u, parameters, mapping))
|
785
|
+
if sample_times.max() > sample_size:
|
786
|
+
raise ValueError("sample_times must be ≤ total_time - transient_time")
|
787
|
+
history = np.zeros(len(sample_times))
|
788
|
+
|
789
|
+
sample_idx = 0
|
790
|
+
prev_i = 0
|
791
|
+
for st in sample_times:
|
792
|
+
steps = st - prev_i
|
793
|
+
for _ in range(steps):
|
794
|
+
u = mapping(u, parameters)
|
795
|
+
J = np.ascontiguousarray(jacobian(u, parameters, mapping))
|
654
796
|
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
v[:, i] /= np.linalg.norm(v[:, i])
|
797
|
+
for i in range(2):
|
798
|
+
v[:, i] = np.ascontiguousarray(J) @ np.ascontiguousarray(v[:, i])
|
799
|
+
v[:, i] /= np.linalg.norm(v[:, i])
|
659
800
|
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
801
|
+
# Compute SALI
|
802
|
+
PAI = np.linalg.norm(v[:, 0] + v[:, 1])
|
803
|
+
AAI = np.linalg.norm(v[:, 0] - v[:, 1])
|
804
|
+
sali_val = min(PAI, AAI)
|
664
805
|
|
665
|
-
# Record history if requested
|
666
806
|
if return_history:
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
history[count] = sali_val
|
671
|
-
count += 1
|
807
|
+
history[sample_idx] = sali_val
|
808
|
+
sample_idx += 1
|
809
|
+
prev_i = st
|
672
810
|
|
673
|
-
# Early termination
|
674
811
|
if sali_val < tol:
|
675
812
|
break
|
676
813
|
|
677
814
|
return history if return_history else np.array([sali_val])
|
678
815
|
|
679
816
|
|
680
|
-
|
817
|
+
def LDI_k(
|
818
|
+
u: NDArray[np.float64],
|
819
|
+
parameters: NDArray[np.float64],
|
820
|
+
total_time: int,
|
821
|
+
mapping: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
822
|
+
jacobian: Callable[
|
823
|
+
[NDArray[np.float64], NDArray[np.float64], Callable], NDArray[np.float64]
|
824
|
+
],
|
825
|
+
k: int,
|
826
|
+
sample_times: Union[NDArray[np.int32], NDArray[np.int64]],
|
827
|
+
return_history: bool = False,
|
828
|
+
tol: float = 1e-16,
|
829
|
+
transient_time: Optional[int] = None,
|
830
|
+
seed: int = 13,
|
831
|
+
) -> Union[NDArray[np.float64], Tuple[NDArray[np.float64], NDArray[np.float64]]]:
|
832
|
+
"""
|
833
|
+
Compute the linear dependence index (LDI) for a dynamical system.
|
681
834
|
|
835
|
+
LDI is a measure of chaos in dynamical systems, calculated using the evolution
|
836
|
+
of `k` initially orthonormal deviation vectors under the system's Jacobian.
|
682
837
|
|
683
|
-
|
838
|
+
Parameters
|
839
|
+
----------
|
840
|
+
u : NDArray[np.float64]
|
841
|
+
Initial state vector of the system (shape: `(neq,)`).
|
842
|
+
parameters : NDArray[np.float64]
|
843
|
+
System parameters (shape: arbitrary, passed to `mapping` and `jacobian`).
|
844
|
+
total_time : int
|
845
|
+
Total number of iterations (time steps) to simulate.
|
846
|
+
mapping : Callable[[NDArray, NDArray], NDArray]
|
847
|
+
Function representing the system's time evolution (maps state `u` to next state).
|
848
|
+
jacobian : Callable[[NDArray, NDArray, Callable], NDArray]
|
849
|
+
Function computing the Jacobian matrix of `mapping` at state `u`.
|
850
|
+
k : int
|
851
|
+
Number of deviation vectors to track.
|
852
|
+
sample_times: Union[NDArray[np.int32], NDArray[np.int64]],
|
853
|
+
Specific time steps at which to record LDI (if `return_history=True`).
|
854
|
+
return_history : bool, optional
|
855
|
+
If True, return GALI values at each time step (or `sample_times`). Default: False.
|
856
|
+
tol : float, optional
|
857
|
+
Tolerance for early stopping if GALI drops below this value (default: 1e-16).
|
858
|
+
transient_time : Optional[int], optional
|
859
|
+
Number of initial iterations to discard as transient (default: None).
|
860
|
+
seed : int, optional
|
861
|
+
Random seed for reproducibility (default 13)
|
862
|
+
|
863
|
+
Returns
|
864
|
+
-------
|
865
|
+
Union[NDArray[np.float64], Tuple[NDArray[np.float64], NDArray[np.float64]]]
|
866
|
+
- If `return_history=False`: Final LDI value (shape: `(1,)`).
|
867
|
+
- If `return_history=True`: Array of LDI values at each sampled time.
|
868
|
+
|
869
|
+
Raises
|
870
|
+
------
|
871
|
+
ValueError
|
872
|
+
If `sample_times` contains values exceeding `total_time`.
|
873
|
+
|
874
|
+
Notes
|
875
|
+
-----
|
876
|
+
- Early termination occurs if LDI < `tol` (indicating chaotic behavior).
|
877
|
+
"""
|
878
|
+
|
879
|
+
np.random.seed(seed) # For reproducibility
|
880
|
+
|
881
|
+
neq = len(u)
|
882
|
+
|
883
|
+
# Generate random orthonormal deviation vectors
|
884
|
+
v = np.ascontiguousarray(np.random.rand(neq, k))
|
885
|
+
v, _ = qr(v)
|
886
|
+
|
887
|
+
if transient_time is not None:
|
888
|
+
# Discard transient time
|
889
|
+
sample_size = total_time - transient_time
|
890
|
+
for i in range(transient_time):
|
891
|
+
u = mapping(u, parameters)
|
892
|
+
else:
|
893
|
+
sample_size = total_time
|
894
|
+
|
895
|
+
# Initialize history tracking
|
896
|
+
if return_history:
|
897
|
+
if sample_times.max() > sample_size:
|
898
|
+
raise ValueError("sample_times must be ≤ total_time - transient_time")
|
899
|
+
history = np.zeros(len(sample_times))
|
900
|
+
|
901
|
+
sample_idx = 0
|
902
|
+
prev_j = 0
|
903
|
+
for st in sample_times:
|
904
|
+
steps = st - prev_j
|
905
|
+
for _ in range(steps):
|
906
|
+
u = mapping(u, parameters)
|
907
|
+
J = np.ascontiguousarray(jacobian(u, parameters, mapping))
|
908
|
+
|
909
|
+
# Update deviation vectors
|
910
|
+
for i in range(k):
|
911
|
+
v[:, i] = np.ascontiguousarray(J) @ np.ascontiguousarray(v[:, i])
|
912
|
+
v[:, i] = v[:, i] / np.linalg.norm(v[:, i])
|
913
|
+
|
914
|
+
# Compute LDI
|
915
|
+
S = np.linalg.svd(v, full_matrices=False, compute_uv=False)
|
916
|
+
# ldi = np.prod(S) # LDI is the product of the singular values
|
917
|
+
ldi = np.exp(np.sum(np.log(S))) # LDI is the product of all singular values
|
918
|
+
# Instead of computing prod(S) directly, which could lead to underflows
|
919
|
+
# or overflows, we compute the sum_{i=1}^k log(S_i) and then take the
|
920
|
+
# exponential of this sum.
|
921
|
+
|
922
|
+
if return_history:
|
923
|
+
history[sample_idx] = ldi
|
924
|
+
sample_idx += 1
|
925
|
+
prev_j = st
|
926
|
+
|
927
|
+
if ldi < tol:
|
928
|
+
break
|
929
|
+
|
930
|
+
if return_history:
|
931
|
+
return history
|
932
|
+
else:
|
933
|
+
return np.array([ldi])
|
934
|
+
|
935
|
+
|
936
|
+
def GALI_k(
|
684
937
|
u: NDArray[np.float64],
|
685
938
|
parameters: NDArray[np.float64],
|
686
939
|
total_time: int,
|
@@ -688,15 +941,15 @@ def LDI_k(
|
|
688
941
|
jacobian: Callable[
|
689
942
|
[NDArray[np.float64], NDArray[np.float64], Callable], NDArray[np.float64]
|
690
943
|
],
|
691
|
-
k: int
|
944
|
+
k: int,
|
945
|
+
sample_times: Union[NDArray[np.int32], NDArray[np.int64]],
|
692
946
|
return_history: bool = False,
|
693
|
-
sample_times: Optional[NDArray[np.int32]] = None,
|
694
947
|
tol: float = 1e-16,
|
695
948
|
transient_time: Optional[int] = None,
|
696
949
|
seed: int = 13,
|
697
950
|
) -> Union[NDArray[np.float64], Tuple[NDArray[np.float64], NDArray[np.float64]]]:
|
698
951
|
"""
|
699
|
-
Compute the Generalized
|
952
|
+
Compute the Generalized Aligment Index (GALI) for a dynamical system.
|
700
953
|
|
701
954
|
GALI is a measure of chaos in dynamical systems, calculated using the evolution
|
702
955
|
of `k` initially orthonormal deviation vectors under the system's Jacobian.
|
@@ -713,12 +966,12 @@ def LDI_k(
|
|
713
966
|
Function representing the system's time evolution (maps state `u` to next state).
|
714
967
|
jacobian : Callable[[NDArray, NDArray, Callable], NDArray]
|
715
968
|
Function computing the Jacobian matrix of `mapping` at state `u`.
|
716
|
-
k : int
|
717
|
-
Number of deviation vectors to track
|
969
|
+
k : int
|
970
|
+
Number of deviation vectors to track.
|
971
|
+
sample_times: Union[NDArray[np.int32], NDArray[np.int64]],
|
972
|
+
Specific time steps at which to record LDI (if `return_history=True`).
|
718
973
|
return_history : bool, optional
|
719
974
|
If True, return GALI values at each time step (or `sample_times`). Default: False.
|
720
|
-
sample_times : Optional[NDArray[np.int32]], optional
|
721
|
-
Specific time steps at which to record GALI (if `return_history=True`).
|
722
975
|
tol : float, optional
|
723
976
|
Tolerance for early stopping if GALI drops below this value (default: 1e-16).
|
724
977
|
transient_time : Optional[int], optional
|
@@ -739,9 +992,7 @@ def LDI_k(
|
|
739
992
|
|
740
993
|
Notes
|
741
994
|
-----
|
742
|
-
- The function uses QR decomposition to maintain orthonormality of deviation vectors.
|
743
995
|
- Early termination occurs if GALI < `tol` (indicating chaotic behavior).
|
744
|
-
- For performance, the function is optimized with `@njit(cache=True)`.
|
745
996
|
"""
|
746
997
|
|
747
998
|
np.random.seed(seed) # For reproducibility
|
@@ -760,39 +1011,32 @@ def LDI_k(
|
|
760
1011
|
else:
|
761
1012
|
sample_size = total_time
|
762
1013
|
|
1014
|
+
# Initialize history tracking
|
763
1015
|
if return_history:
|
764
|
-
if sample_times
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
# Update the state
|
776
|
-
u = mapping(u, parameters)
|
777
|
-
|
778
|
-
# Compute the Jacobian
|
779
|
-
J = np.ascontiguousarray(jacobian(u, parameters, mapping))
|
1016
|
+
if sample_times.max() > sample_size:
|
1017
|
+
raise ValueError("sample_times must be ≤ total_time - transient_time")
|
1018
|
+
history = np.zeros(len(sample_times))
|
1019
|
+
|
1020
|
+
sample_idx = 0
|
1021
|
+
prev_j = 0
|
1022
|
+
for st in sample_times:
|
1023
|
+
steps = st - prev_j
|
1024
|
+
for _ in range(steps):
|
1025
|
+
u = mapping(u, parameters)
|
1026
|
+
J = np.ascontiguousarray(jacobian(u, parameters, mapping))
|
780
1027
|
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
1028
|
+
# Update deviation vectors
|
1029
|
+
for i in range(k):
|
1030
|
+
v[:, i] = np.ascontiguousarray(J) @ np.ascontiguousarray(v[:, i])
|
1031
|
+
v[:, i] = v[:, i] / np.linalg.norm(v[:, i])
|
785
1032
|
|
786
|
-
|
787
|
-
|
788
|
-
gali = np.prod(S) # GALI is the product of the singular values
|
1033
|
+
# Compute GALI
|
1034
|
+
gali = wedge_norm(v)
|
789
1035
|
|
790
1036
|
if return_history:
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
history[count] = gali
|
795
|
-
count += 1
|
1037
|
+
history[sample_idx] = gali
|
1038
|
+
sample_idx += 1
|
1039
|
+
prev_j = st
|
796
1040
|
|
797
1041
|
if gali < tol:
|
798
1042
|
break
|
@@ -803,7 +1047,7 @@ def LDI_k(
|
|
803
1047
|
return np.array([gali])
|
804
1048
|
|
805
1049
|
|
806
|
-
@njit
|
1050
|
+
@njit
|
807
1051
|
def hurst_exponent(
|
808
1052
|
u: NDArray[np.float64],
|
809
1053
|
parameters: NDArray[np.float64],
|
@@ -980,13 +1224,13 @@ def finite_time_hurst_exponent(
|
|
980
1224
|
return H_values
|
981
1225
|
|
982
1226
|
|
983
|
-
@njit
|
1227
|
+
@njit
|
984
1228
|
def lyapunov_vectors():
|
985
1229
|
# ! To be implemented...
|
986
1230
|
pass
|
987
1231
|
|
988
1232
|
|
989
|
-
@njit
|
1233
|
+
@njit
|
990
1234
|
def lagrangian_descriptors(
|
991
1235
|
u: NDArray[np.float64],
|
992
1236
|
parameters: NDArray[np.float64],
|