pynamicalsys 1.2.2__py3-none-any.whl → 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.0'
32
+ __version_tuple__ = version_tuple = (1, 3, 0)
33
+
34
+ __commit_id__ = commit_id = None
@@ -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(cache=True)
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
-
108
- # Perform the QR decomposition
109
- v, R = QR(v)
110
-
111
- # Accumulate the log
112
- exponents += np.log(np.abs(np.diag(R))) / np.log(log_base)
107
+ # Reshape the deviation vectors into a neq x neq matrix
108
+ v = uv[neq:].reshape(neq, num_exponents).copy()
113
109
 
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)
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)
122
114
 
123
- # Reshape v back to uv
124
- uv[neq:] = v.reshape(neq**2)
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)
125
123
 
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))
@@ -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()
212
+ # Reshape the deviation vectors into a neq x ndv matrix
213
+ v = uv[neq:].reshape(neq, ndv)
213
214
 
214
- # Reshape the deviation vectors into a neq x ndv matrix
215
- v = uv[neq:].reshape(neq, ndv)
215
+ # Normalize the deviation vectors
216
+ v[:, 0] /= np.linalg.norm(v[:, 0])
217
+ v[:, 1] /= np.linalg.norm(v[:, 1])
216
218
 
217
- # Normalize the deviation vectors
218
- v[:, 0] /= np.linalg.norm(v[:, 0])
219
- v[:, 1] /= np.linalg.norm(v[:, 1])
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)
220
223
 
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)
224
+ if return_history:
225
+ result = [time, sali]
226
+ history.append(result)
225
227
 
226
- if return_history:
227
- result = [time, sali]
228
- history.append(result)
228
+ # Early termination
229
+ if sali <= threshold:
230
+ break
229
231
 
230
- # Early termination
231
- if sali <= threshold:
232
- break
233
-
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
@@ -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)
319
317
 
320
- # Reshape the deviation vectors into a neq x ndv matrix
321
- v = uv[neq:].reshape(neq, ndv)
318
+ # Normalize the deviation vectors
319
+ for i in range(ndv):
320
+ v[:, i] /= np.linalg.norm(v[:, i])
322
321
 
323
- # Normalize the deviation vectors
324
- for i in range(ndv):
325
- v[:, i] /= np.linalg.norm(v[:, i])
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.
326
328
 
327
- # Calculate the singular values
328
- S = np.linalg.svd(v, full_matrices=False, compute_uv=False)
329
- ldi = np.prod(S)
329
+ if return_history:
330
+ result = [time, ldi]
331
+ history.append(result)
330
332
 
331
- if return_history:
332
- result = [time, ldi]
333
- history.append(result)
333
+ # Early termination
334
+ if ldi <= threshold:
335
+ break
334
336
 
335
- # Early termination
336
- if ldi <= threshold:
337
- break
337
+ # Reshape v back to uv
338
+ uv[neq:] = v.reshape(neq * ndv)
338
339
 
339
- # Reshape v back to uv
340
- uv[neq:] = v.reshape(neq * ndv)
340
+ if return_history:
341
+ return history
342
+ else:
343
+ return [[time, ldi]]
341
344
 
342
- time_step = time_step_new
345
+
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]:
366
+
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
370
+
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]]
@@ -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]:
@@ -15,46 +15,49 @@
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
- from typing import Optional, Callable, Union, Tuple, Dict, List, Any, Sequence
19
- from numpy.typing import NDArray
18
+ from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union
19
+
20
20
  import numpy as np
21
21
  from numba import njit, prange
22
+ from numpy.typing import NDArray
22
23
 
23
- from pynamicalsys.continuous_time.numerical_integrators import (
24
- rk4_step_wrapped,
25
- )
24
+ from pynamicalsys.continuous_time.numerical_integrators import rk4_step_wrapped
26
25
 
27
26
 
28
27
  @njit(cache=True)
29
- def evolve_system(
28
+ def step(
29
+ time: np.float64,
30
30
  u: NDArray[np.float64],
31
31
  parameters: NDArray[np.float64],
32
- total_time: float,
33
32
  equations_of_motion: Callable[
34
33
  [NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
35
34
  ],
35
+ jacobian: Optional[
36
+ Callable[
37
+ [np.float64, NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
38
+ ]
39
+ ] = None,
36
40
  time_step: float = 0.01,
37
41
  atol: float = 1e-6,
38
42
  rtol: float = 1e-3,
39
43
  integrator=rk4_step_wrapped,
44
+ number_of_deviation_vectors: Optional[int] = None,
40
45
  ) -> NDArray[np.float64]:
41
46
 
42
47
  u = u.copy()
48
+ accept = False
43
49
 
44
- # number_of_steps = round(total_time / time_step)
45
-
46
- time = 0
47
- while time < total_time:
48
- if time + time_step > total_time:
49
- time_step = total_time - time
50
+ while not accept:
50
51
  u_new, time_new, time_step_new, accept = integrator(
51
52
  time,
52
53
  u,
53
54
  parameters,
54
55
  equations_of_motion,
56
+ jacobian=jacobian,
55
57
  time_step=time_step,
56
58
  atol=atol,
57
59
  rtol=rtol,
60
+ number_of_deviation_vectors=number_of_deviation_vectors,
58
61
  )
59
62
  if accept:
60
63
  time = time_new
@@ -62,6 +65,40 @@ def evolve_system(
62
65
 
63
66
  time_step = time_step_new
64
67
 
68
+ return u_new, time_new, time_step_new
69
+
70
+
71
+ @njit(cache=True)
72
+ def evolve_system(
73
+ u: NDArray[np.float64],
74
+ parameters: NDArray[np.float64],
75
+ total_time: float,
76
+ equations_of_motion: Callable[
77
+ [NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
78
+ ],
79
+ time_step: float = 0.01,
80
+ atol: float = 1e-6,
81
+ rtol: float = 1e-3,
82
+ integrator=rk4_step_wrapped,
83
+ ) -> NDArray[np.float64]:
84
+
85
+ u = u.copy()
86
+
87
+ time = 0
88
+ while time < total_time:
89
+ u, time, time_step = step(
90
+ time,
91
+ u,
92
+ parameters,
93
+ equations_of_motion,
94
+ time_step=time_step,
95
+ atol=atol,
96
+ rtol=rtol,
97
+ integrator=integrator,
98
+ )
99
+ if time + time_step > total_time:
100
+ time_step = total_time - time
101
+
65
102
  return u
66
103
 
67
104
 
@@ -103,7 +140,8 @@ def generate_trajectory(
103
140
  while time < total_time:
104
141
  if time + time_step > total_time:
105
142
  time_step = total_time - time
106
- u_new, time_new, time_step_new, accept = integrator(
143
+
144
+ u, time, time_step = step(
107
145
  time,
108
146
  u,
109
147
  parameters,
@@ -111,15 +149,13 @@ def generate_trajectory(
111
149
  time_step=time_step,
112
150
  atol=atol,
113
151
  rtol=rtol,
152
+ integrator=integrator,
114
153
  )
115
- if accept:
116
- time = time_new
117
- u = u_new.copy()
118
- result = [time]
119
- for i in range(neq):
120
- result.append(u[i])
121
- trajectory.append(result)
122
- time_step = time_step_new
154
+
155
+ result = [time]
156
+ for i in range(neq):
157
+ result.append(u[i])
158
+ trajectory.append(result)
123
159
 
124
160
  return trajectory
125
161