pynamicalsys 1.2.2__tar.gz → 1.3.0__tar.gz

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.
Files changed (66) hide show
  1. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/CHANGELOG.md +27 -4
  2. pynamicalsys-1.3.0/CONTRIBUTING.md +47 -0
  3. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/PKG-INFO +3 -3
  4. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/README.md +2 -2
  5. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys/__version__.py +16 -3
  6. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys/common/utils.py +20 -21
  7. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys/continuous_time/chaotic_indicators.py +174 -78
  8. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys/continuous_time/numerical_integrators.py +1 -1
  9. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys/continuous_time/trajectory_analysis.py +58 -22
  10. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys/core/continuous_dynamical_systems.py +184 -11
  11. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys/core/discrete_dynamical_systems.py +264 -16
  12. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys/discrete_time/dynamical_indicators.py +407 -163
  13. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys/discrete_time/transport.py +74 -112
  14. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys/discrete_time/validators.py +1 -1
  15. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys.egg-info/PKG-INFO +3 -3
  16. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys.egg-info/SOURCES.txt +5 -1
  17. pynamicalsys-1.3.0/tests/continuous_time/test_SALI_and_LDI.ipynb +259 -0
  18. pynamicalsys-1.3.0/tests/continuous_time/test_lyapunov.ipynb +438 -0
  19. pynamicalsys-1.3.0/tests/continuous_time/test_trajectory.ipynb +443 -0
  20. pynamicalsys-1.3.0/tests/discrete-time/test_FTLE.ipynb +164 -0
  21. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/tests/discrete-time/test_RTE.ipynb +2 -2
  22. pynamicalsys-1.3.0/tests/discrete-time/test_SALI.ipynb +153 -0
  23. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/tests/discrete-time/test_bif_diagram.ipynb +3 -3
  24. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/tests/discrete-time/test_chaotic_saddle.ipynb +3 -3
  25. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/tests/discrete-time/test_dig.ipynb +2 -2
  26. pynamicalsys-1.3.0/tests/discrete-time/test_ensemble_statistics.ipynb +562 -0
  27. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/tests/discrete-time/test_escape.ipynb +2 -2
  28. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/tests/discrete-time/test_fractal_dimension.ipynb +3 -3
  29. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/tests/discrete-time/test_hurst_exponent.ipynb +2 -2
  30. pynamicalsys-1.3.0/tests/discrete-time/test_lyapunov.ipynb +472 -0
  31. pynamicalsys-1.3.0/tests/discrete-time/test_lyapunov_exponents.ipynb +319 -0
  32. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/tests/discrete-time/test_manifold.ipynb +2 -2
  33. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/tests/discrete-time/test_manifolds.ipynb +3 -3
  34. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/tests/discrete-time/test_period_counter.ipynb +2 -2
  35. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/tests/discrete-time/test_phase_space.ipynb +2 -2
  36. pynamicalsys-1.3.0/tests/discrete-time/test_step.py +31 -0
  37. pynamicalsys-1.2.2/CONTRIBUTING.md +0 -47
  38. pynamicalsys-1.2.2/tests/continuous_time/test_SALI_and_LDI.ipynb +0 -237
  39. pynamicalsys-1.2.2/tests/continuous_time/test_lyapunov.ipynb +0 -332
  40. pynamicalsys-1.2.2/tests/continuous_time/test_trajectory.ipynb +0 -443
  41. pynamicalsys-1.2.2/tests/discrete-time/test_FTLE.ipynb +0 -101
  42. pynamicalsys-1.2.2/tests/discrete-time/test_lyapunov_exponents.ipynb +0 -473
  43. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/.gitignore +0 -0
  44. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/CITATION.cff +0 -0
  45. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/LICENSE +0 -0
  46. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/MANIFEST.in +0 -0
  47. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/pyproject.toml +0 -0
  48. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/setup.cfg +0 -0
  49. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys/__init__.py +0 -0
  50. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys/common/__init__.py +0 -0
  51. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys/common/basin_analysis.py +0 -0
  52. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys/common/recurrence_quantification_analysis.py +0 -0
  53. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys/continuous_time/__init__.py +0 -0
  54. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys/continuous_time/models.py +0 -0
  55. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys/continuous_time/validators.py +0 -0
  56. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys/core/__init__.py +0 -0
  57. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys/core/basin_metrics.py +0 -0
  58. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys/core/plot_styler.py +0 -0
  59. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys/core/time_series_metrics.py +0 -0
  60. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys/discrete_time/__init__.py +0 -0
  61. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys/discrete_time/models.py +0 -0
  62. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys/discrete_time/trajectory_analysis.py +0 -0
  63. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys.egg-info/dependency_links.txt +0 -0
  64. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys.egg-info/requires.txt +0 -0
  65. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/src/pynamicalsys.egg-info/top_level.txt +0 -0
  66. {pynamicalsys-1.2.2 → pynamicalsys-1.3.0}/tests/discrete-time/test_SALI.py +0 -0
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [v1.3.0] 2025-08-23
9
+
10
+ ### Added
11
+
12
+ - `DiscreteDynamicalSystem` class:
13
+ - `step` method: returns the next state of the system.
14
+ - `GALI` method: computes the generalized alignment index (GALI).
15
+
16
+ - `ContinuousDynamicalSystem` class:
17
+ - `GALI` method that computes the generalized alignment index (GALI).
18
+
19
+ ### Modified
20
+
21
+ - `DiscreteDynamicalSystem` class:
22
+ - Improved performance when checking sampling points by avoiding repeated searches in sample_times.
23
+ - Refactored the `lyapunov` method to allow computing only a subset of the Lyapunov spectrum.
24
+
25
+ - `ContinuousDynamicalSystem` class:
26
+ - Unified integration step logic (previously duplicated across methods like trajectory and lyapunov_exponents) into a single step function.
27
+ - Refactored the `lyapunov` method to allow computing only a subset of the Lyapunov spectrum.
28
+
29
+ [v1.3.0]: https://github.com/mrolims/pynamicalsys/compare/v1.2.2...v1.3.0
30
+
8
31
  ## [v1.2.2] - 2025-06-29
9
32
 
10
33
  ### Added
@@ -16,11 +39,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
16
39
  - Lyapunov exponents calculation.
17
40
  - The smallest aligment index (SALI) and linear dependence index (LDI) for chaos detection.
18
41
 
19
- ## [v1.0.0] - 2025-06-16
42
+ [v1.2.2]: https://github.com/mrolims/pynamicalsys/compare/v1.0.0...v1.2.2
43
+
44
+ ## v1.0.0 - 2025-06-16
20
45
 
21
46
  ### Added
22
47
 
23
- - `DiscreteDynamicalSystems` class for simulating and analyzing discrete nonlinear dynamical systems:
48
+ - `DiscreteDynamicalSystem` class for simulating and analyzing discrete nonlinear dynamical systems:
24
49
  - Trajectory computation.
25
50
  - Chaotic indicators.
26
51
  - Fixed points, periodic orbits, and manifolds.
@@ -36,8 +61,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
36
61
 
37
62
  - `PlotStyler` utility class to globally configure and apply consistent styling for Matplotlib plots.
38
63
 
39
- ---
40
-
41
64
  <!-- Dummy heading to avoid ending on a transition -->
42
65
 
43
66
  ##
@@ -0,0 +1,47 @@
1
+ # Contributing to pynamicalsys
2
+
3
+ We appreciate your interest in contributing to pynamicalsys! Whether you're fixing a bug, suggesting an improvement, or adding a new feature, your help is welcome.
4
+
5
+ ## How to Contribute
6
+
7
+ 1. **Fork the Repository**
8
+ Create a personal copy of this repository.
9
+
10
+ 2. **Create a Feature Branch**
11
+ Use a descriptive branch name:
12
+
13
+ ```bash
14
+ $ git checkout -b fix-typo-in-readme
15
+ ```
16
+
17
+ 3. **Make Changes**
18
+
19
+ Follow the coding style and ensure existing functionality it not broken. Add tests if relevant.
20
+
21
+ 4. **Submit a Pull Request**
22
+
23
+ Push your branch and open a pull request on GitHub. Include a clear description of your changes.
24
+
25
+ 5. **Wait for Review**
26
+
27
+ We'll review your PR and may request changes or offer suggestions.
28
+
29
+ ## Coding Style
30
+
31
+ - Follow **PEP8** standards for Pycode code.
32
+ - Use **docstrings** (PEP257) for public classes, functions, and modules.
33
+ - Include **type hints** where appropriate.
34
+ - Keep commits clean and descriptive.
35
+
36
+ ## Bug Reports & Feature Requests
37
+
38
+ - Submit an [issue](https://github.com/mrolims/pynamicalsys/issues) to report bugs or suggest new features.
39
+ - Include steps to reproduce, error messages, or use cases when possible.
40
+
41
+ ## Code of Conduct
42
+
43
+ We strive to maintain a respectful and inclusive community. Please follow our [code of conduct](https://pynamicalsys.readthedocs.io/en/latest/) when interacting.
44
+
45
+ ## Thank You
46
+
47
+ Your contributions help improve the project and support the community — thank you!
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pynamicalsys
3
- Version: 1.2.2
3
+ Version: 1.3.0
4
4
  Summary: A Python toolkit for the analysis of dynamical systems
5
5
  Author-email: Matheus Rolim Sales <rolim.sales.m@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -733,7 +733,7 @@ $ pip install pynamicalsys
733
733
  To upgrade your current version of **pynamicalsys** to the latest stable release, run in your command line:
734
734
 
735
735
  ```bash
736
- $ pip install **pynamicalsys** --upgrade
736
+ $ pip install pynamicalsys --upgrade
737
737
  ```
738
738
 
739
739
  ### Install from source
@@ -781,7 +781,7 @@ Currently, our research paper is under review, but in the mean time, if you use
781
781
 
782
782
  ## Contributing
783
783
 
784
- We welcome contributions from the community! To get started, please see our Contributing Guidelines.
784
+ We welcome contributions from the community! To get started, please see our [Contributing Guidelines](https://pynamicalsys.readthedocs.io/en/latest/contributing.html).
785
785
 
786
786
  ## License
787
787
 
@@ -40,7 +40,7 @@ $ pip install pynamicalsys
40
40
  To upgrade your current version of **pynamicalsys** to the latest stable release, run in your command line:
41
41
 
42
42
  ```bash
43
- $ pip install **pynamicalsys** --upgrade
43
+ $ pip install pynamicalsys --upgrade
44
44
  ```
45
45
 
46
46
  ### Install from source
@@ -88,7 +88,7 @@ Currently, our research paper is under review, but in the mean time, if you use
88
88
 
89
89
  ## Contributing
90
90
 
91
- We welcome contributions from the community! To get started, please see our Contributing Guidelines.
91
+ We welcome contributions from the community! To get started, please see our [Contributing Guidelines](https://pynamicalsys.readthedocs.io/en/latest/contributing.html).
92
92
 
93
93
  ## License
94
94
 
@@ -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 = 'g138b5d22a'
@@ -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]: