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.
@@ -1,7 +1,14 @@
1
1
  # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
3
 
4
- __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
5
12
 
6
13
  TYPE_CHECKING = False
7
14
  if TYPE_CHECKING:
@@ -9,13 +16,19 @@ if TYPE_CHECKING:
9
16
  from typing import Union
10
17
 
11
18
  VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
12
20
  else:
13
21
  VERSION_TUPLE = object
22
+ COMMIT_ID = object
14
23
 
15
24
  version: str
16
25
  __version__: str
17
26
  __version_tuple__: VERSION_TUPLE
18
27
  version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
19
30
 
20
- __version__ = version = '1.2.2'
21
- __version_tuple__ = version_tuple = (1, 2, 2)
31
+ __version__ = version = '1.3.1'
32
+ __version_tuple__ = version_tuple = (1, 3, 1)
33
+
34
+ __commit_id__ = commit_id = None
@@ -91,7 +91,7 @@ class RTEConfig:
91
91
  raise ValueError("metric must be 'supremum', 'euclidean' or 'manhattan'")
92
92
 
93
93
 
94
- @njit(cache=True)
94
+ @njit
95
95
  def _recurrence_matrix(
96
96
  arr: NDArray[np.float64], threshold: float, metric_id: int
97
97
  ) -> NDArray[np.uint8]:
@@ -21,7 +21,7 @@ from numpy.typing import NDArray
21
21
  from numba import njit
22
22
 
23
23
 
24
- @njit(cache=True)
24
+ @njit
25
25
  def qr(M: NDArray[np.float64]) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
26
26
  """
27
27
  Perform numerically stable QR decomposition using modified Gram-Schmidt with reorthogonalization.
@@ -92,7 +92,7 @@ def qr(M: NDArray[np.float64]) -> Tuple[NDArray[np.float64], NDArray[np.float64]
92
92
  return Q, R
93
93
 
94
94
 
95
- @njit(cache=True)
95
+ @njit
96
96
  def householder_qr(
97
97
  M: NDArray[np.float64],
98
98
  ) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
@@ -184,7 +184,7 @@ def householder_qr(
184
184
  return Q, R
185
185
 
186
186
 
187
- @njit(cache=True)
187
+ @njit
188
188
  def finite_difference_jacobian(
189
189
  u: NDArray[np.float64],
190
190
  parameters: NDArray[np.float64],
@@ -257,7 +257,7 @@ def finite_difference_jacobian(
257
257
 
258
258
 
259
259
  @njit
260
- def wedge_product_norm(vectors: NDArray[np.float64]) -> float:
260
+ def wedge_norm_2(vectors: NDArray[np.float64]) -> float:
261
261
  """
262
262
  Computes the norm of the wedge product of n m-dimensional vectors using the Gram determinant.
263
263
 
@@ -295,6 +295,25 @@ def wedge_product_norm(vectors: NDArray[np.float64]) -> float:
295
295
  return norm
296
296
 
297
297
 
298
+ def wedge_norm(V: NDArray[np.float64]) -> float:
299
+ """
300
+ Computes the norm of the wedge product of k d-dimensional vectors using the Gram determinant.
301
+
302
+ Parameters:
303
+ vectors : NDArray[np.float64]
304
+ A (d, k) array where d is the dimension and k is the number of vectors.
305
+
306
+ Returns:
307
+ norm : float
308
+ The norm (magnitude) of the wedge product.
309
+ """
310
+ G = V.T @ V # Gram matrix, shape (k, k)
311
+
312
+ det = np.linalg.det(G)
313
+
314
+ return 0 if det < 0 else np.sqrt(det)
315
+
316
+
298
317
  @njit
299
318
  def _coeff_mat(x: NDArray[np.float64], deg: int) -> NDArray[np.float64]:
300
319
  mat_ = np.zeros(shape=(x.shape[0], deg + 1))
@@ -322,23 +341,3 @@ def fit_poly(
322
341
  p = _fit_x(a, y)
323
342
  # Reverse order so p[0] is coefficient of highest order
324
343
  return p[::-1]
325
-
326
-
327
- if __name__ == "__main__":
328
-
329
- v = np.random.rand(2, 2)
330
- w = v.copy()
331
-
332
- q, r = qr(v)
333
- print("Q:\n", q)
334
- print("R:\n", r)
335
- print("QR Product:\n", np.dot(q, r))
336
- print("Original Matrix:\n", v)
337
-
338
- print()
339
-
340
- q, r = householder_qr(v)
341
- print("Q:\n", q)
342
- print("R:\n", r)
343
- print("QR Product:\n", np.dot(q, r))
344
- print("Original Matrix:\n", v)
@@ -18,14 +18,14 @@
18
18
  from typing import Optional, Callable, Union, Tuple, Dict, List, Any, Sequence
19
19
  from numpy.typing import NDArray
20
20
  import numpy as np
21
- from numba import njit, prange
21
+ from numba import njit
22
22
 
23
- from pynamicalsys.common.utils import qr
24
- from pynamicalsys.continuous_time.trajectory_analysis import evolve_system
23
+ from pynamicalsys.common.utils import qr, wedge_norm
24
+ from pynamicalsys.continuous_time.trajectory_analysis import step, evolve_system
25
25
  from pynamicalsys.continuous_time.numerical_integrators import rk4_step_wrapped
26
26
 
27
27
 
28
- @njit(cache=True)
28
+ # @njit
29
29
  def lyapunov_exponents(
30
30
  u: NDArray[np.float64],
31
31
  parameters: NDArray[np.float64],
@@ -36,6 +36,7 @@ def lyapunov_exponents(
36
36
  jacobian: Callable[
37
37
  [np.float64, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
38
38
  ],
39
+ num_exponents: int,
39
40
  transient_time: Optional[float] = None,
40
41
  time_step: float = 0.01,
41
42
  atol: float = 1e-6,
@@ -50,7 +51,7 @@ def lyapunov_exponents(
50
51
  ) -> NDArray[np.float64]:
51
52
 
52
53
  neq = len(u) # Number of equations of the system
53
- nt = neq + neq**2 # Total number of equations including variational equations
54
+ nt = neq + neq * num_exponents # system + variational equations
54
55
 
55
56
  u = u.copy()
56
57
 
@@ -79,57 +80,55 @@ def lyapunov_exponents(
79
80
  # Randomly define the deviation vectors and orthonormalize them
80
81
  np.random.seed(seed)
81
82
  uv[neq:] = -1 + 2 * np.random.rand(nt - neq)
82
- v = uv[neq:].reshape(neq, neq)
83
+ v = uv[neq:].reshape(neq, num_exponents)
83
84
  v, _ = QR(v)
84
- uv[neq:] = v.reshape(neq**2)
85
+ uv[neq:] = v.reshape(neq * num_exponents)
85
86
 
86
- exponents = np.zeros(neq, dtype=np.float64)
87
+ exponents = np.zeros(num_exponents, dtype=np.float64)
87
88
  history = []
88
89
 
89
90
  while time < total_time:
90
91
  if time + time_step > total_time:
91
92
  time_step = total_time - time
92
93
 
93
- uv_new, time_new, time_step_new, accept = integrator(
94
+ uv, time, time_step = step(
94
95
  time,
95
96
  uv,
96
97
  parameters,
97
98
  equations_of_motion,
98
99
  jacobian=jacobian,
99
100
  time_step=time_step,
101
+ atol=atol,
102
+ rtol=rtol,
103
+ integrator=integrator,
104
+ number_of_deviation_vectors=num_exponents,
100
105
  )
101
106
 
102
- if accept:
103
- time = time_new
104
- uv = uv_new.copy()
105
- # Reshape the deviation vectors into a neq x neq matrix
106
- v = uv[neq:].reshape(neq, neq).copy()
107
+ # Reshape the deviation vectors into a neq x neq matrix
108
+ v = uv[neq:].reshape(neq, num_exponents).copy()
107
109
 
108
- # Perform the QR decomposition
109
- v, R = QR(v)
110
+ # Perform the QR decomposition
111
+ v, R = QR(v)
112
+ # Accumulate the log
113
+ exponents += np.log(np.abs(np.diag(R))) / np.log(log_base)
110
114
 
111
- # Accumulate the log
112
- exponents += np.log(np.abs(np.diag(R))) / np.log(log_base)
115
+ if return_history:
116
+ result = [time]
117
+ for i in range(num_exponents):
118
+ result.append(
119
+ exponents[i]
120
+ / (time - (transient_time if transient_time is not None else 0))
121
+ )
122
+ history.append(result)
113
123
 
114
- if return_history:
115
- result = [time]
116
- for i in range(neq):
117
- result.append(
118
- exponents[i]
119
- / (time - (transient_time if transient_time is not None else 0))
120
- )
121
- history.append(result)
122
-
123
- # Reshape v back to uv
124
- uv[neq:] = v.reshape(neq**2)
125
-
126
- time_step = time_step_new
124
+ # Reshape v back to uv
125
+ uv[neq:] = v.reshape(neq * num_exponents)
127
126
 
128
127
  if return_history:
129
128
  return history
130
129
  else:
131
130
  result = []
132
- for i in range(neq):
131
+ for i in range(num_exponents):
133
132
  result.append(
134
133
  exponents[i]
135
134
  / (time - (transient_time if transient_time is not None else 0))
@@ -137,7 +136,7 @@ def lyapunov_exponents(
137
136
  return [result]
138
137
 
139
138
 
140
- @njit(cache=True)
139
+ @njit
141
140
  def SALI(
142
141
  u: NDArray[np.float64],
143
142
  parameters: NDArray[np.float64],
@@ -197,44 +196,41 @@ def SALI(
197
196
  if time + time_step > total_time:
198
197
  time_step = total_time - time
199
198
 
200
- uv_new, time_new, time_step_new, accept = integrator(
199
+ uv, time, time_step = step(
201
200
  time,
202
201
  uv,
203
202
  parameters,
204
203
  equations_of_motion,
205
204
  jacobian=jacobian,
206
205
  time_step=time_step,
206
+ atol=atol,
207
+ rtol=rtol,
208
+ integrator=integrator,
207
209
  number_of_deviation_vectors=ndv,
208
210
  )
209
211
 
210
- if accept:
211
- time = time_new
212
- uv = uv_new.copy()
213
-
214
- # Reshape the deviation vectors into a neq x ndv matrix
215
- v = uv[neq:].reshape(neq, ndv)
212
+ # Reshape the deviation vectors into a neq x ndv matrix
213
+ v = uv[neq:].reshape(neq, ndv)
216
214
 
217
- # Normalize the deviation vectors
218
- v[:, 0] /= np.linalg.norm(v[:, 0])
219
- v[:, 1] /= np.linalg.norm(v[:, 1])
215
+ # Normalize the deviation vectors
216
+ v[:, 0] /= np.linalg.norm(v[:, 0])
217
+ v[:, 1] /= np.linalg.norm(v[:, 1])
220
218
 
221
- # Calculate the aligment indexes and SALI
222
- PAI = np.linalg.norm(v[:, 0] + v[:, 1])
223
- AAI = np.linalg.norm(v[:, 0] - v[:, 1])
224
- sali = min(PAI, AAI)
219
+ # Calculate the aligment indexes and SALI
220
+ PAI = np.linalg.norm(v[:, 0] + v[:, 1])
221
+ AAI = np.linalg.norm(v[:, 0] - v[:, 1])
222
+ sali = min(PAI, AAI)
225
223
 
226
- if return_history:
227
- result = [time, sali]
228
- history.append(result)
224
+ if return_history:
225
+ result = [time, sali]
226
+ history.append(result)
229
227
 
230
- # Early termination
231
- if sali <= threshold:
232
- break
228
+ # Early termination
229
+ if sali <= threshold:
230
+ break
233
231
 
234
- # Reshape v back to uv
235
- uv[neq:] = v.reshape(neq * ndv)
236
-
237
- time_step = time_step_new
232
+ # Reshape v back to uv
233
+ uv[neq:] = v.reshape(neq * ndv)
238
234
 
239
235
  if return_history:
240
236
  return history
@@ -242,7 +238,7 @@ def SALI(
242
238
  return [[time, sali]]
243
239
 
244
240
 
245
- # @njit(cache=True)
241
+ # @njit
246
242
  def LDI(
247
243
  u: NDArray[np.float64],
248
244
  parameters: NDArray[np.float64],
@@ -303,45 +299,145 @@ def LDI(
303
299
  if time + time_step > total_time:
304
300
  time_step = total_time - time
305
301
 
306
- uv_new, time_new, time_step_new, accept = integrator(
302
+ uv, time, time_step = step(
307
303
  time,
308
304
  uv,
309
305
  parameters,
310
306
  equations_of_motion,
311
307
  jacobian=jacobian,
312
308
  time_step=time_step,
309
+ atol=atol,
310
+ rtol=rtol,
311
+ integrator=integrator,
313
312
  number_of_deviation_vectors=ndv,
314
313
  )
315
314
 
316
- if accept:
317
- time = time_new
318
- uv = uv_new.copy()
315
+ # Reshape the deviation vectors into a neq x ndv matrix
316
+ v = uv[neq:].reshape(neq, ndv)
317
+
318
+ # Normalize the deviation vectors
319
+ for i in range(ndv):
320
+ v[:, i] /= np.linalg.norm(v[:, i])
321
+
322
+ # Calculate the singular values
323
+ S = np.linalg.svd(v, full_matrices=False, compute_uv=False)
324
+ ldi = np.exp(np.sum(np.log(S))) # LDI is the product of all singular values
325
+ # Instead of computing prod(S) directly, which could lead to underflows
326
+ # or overflows, we compute the sum_{i=1}^k log(S_i) and then take the
327
+ # exponential of this sum.
319
328
 
320
- # Reshape the deviation vectors into a neq x ndv matrix
321
- v = uv[neq:].reshape(neq, ndv)
329
+ if return_history:
330
+ result = [time, ldi]
331
+ history.append(result)
322
332
 
323
- # Normalize the deviation vectors
324
- for i in range(ndv):
325
- v[:, i] /= np.linalg.norm(v[:, i])
333
+ # Early termination
334
+ if ldi <= threshold:
335
+ break
326
336
 
327
- # Calculate the singular values
328
- S = np.linalg.svd(v, full_matrices=False, compute_uv=False)
329
- ldi = np.prod(S)
337
+ # Reshape v back to uv
338
+ uv[neq:] = v.reshape(neq * ndv)
339
+
340
+ if return_history:
341
+ return history
342
+ else:
343
+ return [[time, ldi]]
330
344
 
331
- if return_history:
332
- result = [time, ldi]
333
- history.append(result)
334
345
 
335
- # Early termination
336
- if ldi <= threshold:
337
- break
346
+ def GALI(
347
+ u: NDArray[np.float64],
348
+ parameters: NDArray[np.float64],
349
+ total_time: float,
350
+ equations_of_motion: Callable[
351
+ [np.float64, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
352
+ ],
353
+ jacobian: Callable[
354
+ [np.float64, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
355
+ ],
356
+ number_deviation_vectors: int,
357
+ transient_time: Optional[float] = None,
358
+ time_step: float = 0.01,
359
+ atol: float = 1e-6,
360
+ rtol: float = 1e-3,
361
+ integrator=rk4_step_wrapped,
362
+ return_history: bool = False,
363
+ seed: int = 13,
364
+ threshold: float = 1e-16,
365
+ ) -> NDArray[np.float64]:
338
366
 
339
- # Reshape v back to uv
340
- uv[neq:] = v.reshape(neq * ndv)
367
+ neq = len(u) # Number of equations of the system
368
+ ndv = number_deviation_vectors # Number of deviation vectors
369
+ nt = neq + neq * ndv # Total number of equations including variational equations
341
370
 
342
- time_step = time_step_new
371
+ u = u.copy()
372
+
373
+ # Handle transient time
374
+ if transient_time is not None:
375
+ u = evolve_system(
376
+ u,
377
+ parameters,
378
+ transient_time,
379
+ equations_of_motion,
380
+ time_step=time_step,
381
+ atol=atol,
382
+ rtol=rtol,
383
+ integrator=integrator,
384
+ )
385
+ time = transient_time
386
+ else:
387
+ time = 0
388
+
389
+ # State + deviation vectors
390
+ uv = np.zeros(nt)
391
+ uv[:neq] = u.copy()
392
+
393
+ # Randomly define the deviation vectors and orthonormalize them
394
+ np.random.seed(seed)
395
+ uv[neq:] = -1 + 2 * np.random.rand(nt - neq)
396
+ v = uv[neq:].reshape(neq, ndv)
397
+ v, _ = qr(v)
398
+ uv[neq:] = v.reshape(neq * ndv)
399
+
400
+ history = []
401
+
402
+ while time < total_time:
403
+ if time + time_step > total_time:
404
+ time_step = total_time - time
405
+
406
+ uv, time, time_step = step(
407
+ time,
408
+ uv,
409
+ parameters,
410
+ equations_of_motion,
411
+ jacobian=jacobian,
412
+ time_step=time_step,
413
+ atol=atol,
414
+ rtol=rtol,
415
+ integrator=integrator,
416
+ number_of_deviation_vectors=ndv,
417
+ )
418
+
419
+ # Reshape the deviation vectors into a neq x ndv matrix
420
+ v = uv[neq:].reshape(neq, ndv)
421
+
422
+ # Normalize the deviation vectors
423
+ for i in range(ndv):
424
+ v[:, i] /= np.linalg.norm(v[:, i])
425
+
426
+ # Calculate GALI
427
+ gali = wedge_norm(v)
428
+
429
+ if return_history:
430
+ result = [time, gali]
431
+ history.append(result)
432
+
433
+ # Early termination
434
+ if gali <= threshold:
435
+ break
436
+
437
+ # Reshape v back to uv
438
+ uv[neq:] = v.reshape(neq * ndv)
343
439
 
344
440
  if return_history:
345
441
  return history
346
442
  else:
347
- return [[time, ldi]]
443
+ return [[time, gali]]
@@ -23,7 +23,7 @@ from numba import njit, prange
23
23
  from pynamicalsys.continuous_time.models import variational_equations
24
24
 
25
25
 
26
- @njit(cache=True)
26
+ @njit
27
27
  def rk4_step(
28
28
  t: float,
29
29
  u: NDArray[np.float64],
@@ -43,7 +43,7 @@ def rk4_step(
43
43
  return u_next
44
44
 
45
45
 
46
- @njit(cache=True)
46
+ @njit
47
47
  def variational_rk4_step(
48
48
  t: float,
49
49
  u: NDArray[np.float64],
@@ -115,7 +115,7 @@ RK45_B4 = np.array(
115
115
  )
116
116
 
117
117
 
118
- @njit(cache=True)
118
+ @njit
119
119
  def rk45_step(t, u, parameters, equations_of_motion, time_step, atol=1e-6, rtol=1e-3):
120
120
  """Single adaptive step of RK45 (Dormand-Prince).
121
121
 
@@ -163,7 +163,7 @@ def rk45_step(t, u, parameters, equations_of_motion, time_step, atol=1e-6, rtol=
163
163
  return u5, t + time_step, time_step_new, accept
164
164
 
165
165
 
166
- @njit(cache=True)
166
+ @njit
167
167
  def variational_rk45_step(
168
168
  t,
169
169
  u,
@@ -227,7 +227,7 @@ def variational_rk45_step(
227
227
  return u5, t + time_step, time_step_new, accept
228
228
 
229
229
 
230
- @njit(cache=True)
230
+ @njit
231
231
  def rk4_step_wrapped(
232
232
  t: float,
233
233
  u: NDArray[np.float64],
@@ -237,7 +237,7 @@ def rk4_step_wrapped(
237
237
  ],
238
238
  jacobian=None,
239
239
  time_step: float = 0.01,
240
- number_of_deviation_vectors=None,
240
+ number_of_deviation_vectors: Optional[int] = None,
241
241
  atol: float = 1e-6, # unused, just to match signature
242
242
  rtol: float = 1e-3, # unused, just to match signature
243
243
  ) -> tuple[NDArray[np.float64], float, float, bool]:
@@ -265,7 +265,7 @@ def rk4_step_wrapped(
265
265
  return u_next, t_next, h_next, accept
266
266
 
267
267
 
268
- @njit(cache=True)
268
+ @njit
269
269
  def rk45_step_wrapped(
270
270
  t: float,
271
271
  u: NDArray[np.float64],
@@ -302,7 +302,7 @@ def rk45_step_wrapped(
302
302
  )
303
303
 
304
304
 
305
- @njit(cache=True)
305
+ @njit
306
306
  def estimate_initial_step(
307
307
  t0: float,
308
308
  u0: np.ndarray,