pynamicalsys 1.5.4__tar.gz → 1.6.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 (101) hide show
  1. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/CHANGELOG.md +28 -0
  2. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/PKG-INFO +1 -1
  3. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/__version__.py +3 -3
  4. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/common/basin_analysis.py +105 -0
  5. pynamicalsys-1.6.0/src/pynamicalsys/common/poincare.py +42 -0
  6. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/common/types.py +22 -13
  7. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/core/basin_metrics.py +205 -1
  8. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/core/hamiltonian_systems.py +565 -188
  9. pynamicalsys-1.6.0/src/pynamicalsys/hamiltonian_systems/__init__.py +16 -0
  10. pynamicalsys-1.6.0/src/pynamicalsys/hamiltonian_systems/clv.py +950 -0
  11. pynamicalsys-1.6.0/src/pynamicalsys/hamiltonian_systems/fixed_step.py +249 -0
  12. pynamicalsys-1.6.0/src/pynamicalsys/hamiltonian_systems/gali.py +313 -0
  13. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/hamiltonian_systems/hurst.py +30 -10
  14. pynamicalsys-1.5.4/src/pynamicalsys/hamiltonian_systems/lyapunov.py → pynamicalsys-1.6.0/src/pynamicalsys/hamiltonian_systems/ldi.py +117 -117
  15. pynamicalsys-1.6.0/src/pynamicalsys/hamiltonian_systems/lyapunov.py +506 -0
  16. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/hamiltonian_systems/models.py +41 -1
  17. pynamicalsys-1.6.0/src/pynamicalsys/hamiltonian_systems/poincare.py +368 -0
  18. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/hamiltonian_systems/rte.py +29 -9
  19. pynamicalsys-1.6.0/src/pynamicalsys/hamiltonian_systems/sali.py +255 -0
  20. pynamicalsys-1.6.0/src/pynamicalsys/hamiltonian_systems/tangent.py +438 -0
  21. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/hamiltonian_systems/trajectory.py +36 -21
  22. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys.egg-info/PKG-INFO +1 -1
  23. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys.egg-info/SOURCES.txt +3 -0
  24. pynamicalsys-1.6.0/src/pynamicalsys.egg-info/scm_file_list.json +218 -0
  25. pynamicalsys-1.6.0/src/pynamicalsys.egg-info/scm_version.json +8 -0
  26. pynamicalsys-1.5.4/src/pynamicalsys/hamiltonian_systems/__init__.py +0 -16
  27. pynamicalsys-1.5.4/src/pynamicalsys/hamiltonian_systems/clv.py +0 -452
  28. pynamicalsys-1.5.4/src/pynamicalsys/hamiltonian_systems/fixed_step.py +0 -118
  29. pynamicalsys-1.5.4/src/pynamicalsys/hamiltonian_systems/gali.py +0 -167
  30. pynamicalsys-1.5.4/src/pynamicalsys/hamiltonian_systems/ldi.py +0 -136
  31. pynamicalsys-1.5.4/src/pynamicalsys/hamiltonian_systems/poincare.py +0 -291
  32. pynamicalsys-1.5.4/src/pynamicalsys/hamiltonian_systems/sali.py +0 -135
  33. pynamicalsys-1.5.4/src/pynamicalsys/hamiltonian_systems/tangent.py +0 -249
  34. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/.gitignore +0 -0
  35. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/CITATION.cff +0 -0
  36. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/CONTRIBUTING.md +0 -0
  37. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/LICENSE +0 -0
  38. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/MANIFEST.in +0 -0
  39. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/README.md +0 -0
  40. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/pyproject.toml +0 -0
  41. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/setup.cfg +0 -0
  42. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/__init__.py +0 -0
  43. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/common/__init__.py +0 -0
  44. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/common/clv.py +0 -0
  45. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/common/differentiation.py +0 -0
  46. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/common/fitting.py +0 -0
  47. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/common/hurst.py +0 -0
  48. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/common/linalg.py +0 -0
  49. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/common/recurrence_quantification_analysis.py +0 -0
  50. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/common/validators.py +0 -0
  51. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/__init__.py +0 -0
  52. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/adaptive_step.py +0 -0
  53. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/basins.py +0 -0
  54. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/clv.py +0 -0
  55. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/fixed_step.py +0 -0
  56. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/gali.py +0 -0
  57. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/hurst.py +0 -0
  58. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/ldi.py +0 -0
  59. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/lyapunov.py +0 -0
  60. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/maxima_map.py +0 -0
  61. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/models.py +0 -0
  62. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/poincare.py +0 -0
  63. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/rte.py +0 -0
  64. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/sali.py +0 -0
  65. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/step.py +0 -0
  66. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/step_methods.py +0 -0
  67. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/stroboscopic.py +0 -0
  68. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/trajectory.py +0 -0
  69. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/validators.py +0 -0
  70. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/variational.py +0 -0
  71. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/core/__init__.py +0 -0
  72. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/core/continuous_dynamical_systems.py +0 -0
  73. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/core/discrete_dynamical_systems.py +0 -0
  74. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/core/plot_styler.py +0 -0
  75. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/core/time_series_metrics.py +0 -0
  76. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/__init__.py +0 -0
  77. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/averages.py +0 -0
  78. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/bifurcation.py +0 -0
  79. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/birkhoff.py +0 -0
  80. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/clv.py +0 -0
  81. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/escape.py +0 -0
  82. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/gali.py +0 -0
  83. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/hurst.py +0 -0
  84. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/ldi.py +0 -0
  85. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/lyapunov.py +0 -0
  86. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/manifolds.py +0 -0
  87. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/models.py +0 -0
  88. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/periodic_orbits.py +0 -0
  89. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/rotation.py +0 -0
  90. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/rte.py +0 -0
  91. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/sali.py +0 -0
  92. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/stability.py +0 -0
  93. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/symmetry.py +0 -0
  94. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/trajectory.py +0 -0
  95. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/transport.py +0 -0
  96. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/validators.py +0 -0
  97. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/hamiltonian_systems/coefficients.py +0 -0
  98. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/hamiltonian_systems/validators.py +0 -0
  99. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys.egg-info/dependency_links.txt +0 -0
  100. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys.egg-info/requires.txt +0 -0
  101. {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys.egg-info/top_level.txt +0 -0
@@ -5,6 +5,34 @@ 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.6.0] - 2026-07-01
9
+
10
+ ### Added
11
+
12
+ - Support for **general (non-separable) Hamiltonian systems** `H(q, p)`
13
+ via a new `"imp"` (implicit midpoint) integrator. Specify the system
14
+ via `eom` and `hess_H` instead of `grad_T`/`grad_V`. All analysis
15
+ methods (`lyapunov`, `CLV`, `CLV_angles`, `SALI`, `LDI`, `GALI`,
16
+ `poincare_section`, `recurrence_time_entropy`, `hurst_exponent`)
17
+ dispatch to the implicit midpoint backend automatically when `"imp"`
18
+ is selected.
19
+ - Support for **periodic/angular section coordinates** in Poincaré
20
+ sections via `periodic_section_coordinate` and `period` parameters.
21
+ Useful for action-angle systems where the section coordinate grows
22
+ unbounded rather than oscillating. Available on `poincare_section`,
23
+ `recurrence_time_entropy`, and `hurst_exponent`.
24
+ - `poincare_section` now accepts a `max_workers` parameter controlling
25
+ the number of parallel workers used when computing an ensemble of
26
+ initial conditions (default: all available CPUs).
27
+ - The implicit midpoint integrator now raises `RuntimeError` on
28
+ non-convergence instead of silently returning corrupted results.
29
+ - `BasinMetrics`: added `uncertainty_fraction_mapping` for Monte Carlo
30
+ estimation of the uncertainty fraction and uncertainty exponent of
31
+ escape basins, with parallel evaluation and reproducible random
32
+ sampling via a user-provided seed.
33
+
34
+ [v1.6.0]: https://github.com/mrolims/pynamicalsys/compare/v1.5.4...v1.6.0
35
+
8
36
  ## [v1.5.4] - 2026-05-28
9
37
 
10
38
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pynamicalsys
3
- Version: 1.5.4
3
+ Version: 1.6.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-Expression: GPL-3.0-only
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.5.4'
22
- __version_tuple__ = version_tuple = (1, 5, 4)
21
+ __version__ = version = '1.6.0'
22
+ __version_tuple__ = version_tuple = (1, 6, 0)
23
23
 
24
- __commit_id__ = commit_id = 'g83b2f0161'
24
+ __commit_id__ = commit_id = 'g4ec6e167d'
@@ -19,6 +19,7 @@ import numpy as np
19
19
  from numba import njit, prange
20
20
  from typing import Tuple
21
21
  from numpy.typing import NDArray
22
+ from joblib import delayed, Parallel
22
23
 
23
24
 
24
25
  def uncertainty_fraction(x, y, basin, epsilon_max, n_eps=100, epsilon_min=0):
@@ -168,3 +169,107 @@ def basin_entropy(
168
169
  Sbb = np.mean(S[boundary_mask]) if boundary_mask.any() else 0.0
169
170
 
170
171
  return float(Sb), float(Sbb)
172
+
173
+ def uncertainty_fraction_mapping(
174
+ X: NDArray[np.float64],
175
+ Y: NDArray[np.float64],
176
+ basin: NDArray[np.float64],
177
+ mapping: object,
178
+ parameters: object,
179
+ exits: object,
180
+ escape: str = "exiting",
181
+ n_samples: int = 120_000,
182
+ p_samples: int = 7,
183
+ threshold: float = 0.1,
184
+ n_eps: int = 100,
185
+ max_time: int = 1000,
186
+ seed: int = 13,
187
+ n_jobs: int = -1,
188
+ ) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
189
+ """
190
+ Wrapper to compute the uncertainty fraction f(epsilon) for a given basin of attraction with mapping.
191
+ """
192
+
193
+ rng = np.random.default_rng(seed)
194
+
195
+ flat_x = X.flatten()
196
+ flat_y = Y.flatten()
197
+ flat_basin = basin.flatten()
198
+
199
+ epsilons = np.logspace(np.log10(0.1), np.log10(1 / X.size), n_eps)
200
+
201
+ idx = rng.choice(len(flat_x), size=n_samples, replace=False)
202
+ xs = flat_x[idx]
203
+ ys = flat_y[idx]
204
+ labels = flat_basin[idx]
205
+
206
+ angles = np.linspace(0, 2 * np.pi, p_samples, endpoint=False)
207
+ dcos = np.cos(angles)
208
+ dsin = np.sin(angles)
209
+
210
+ needed = max(1, int(np.ceil(threshold * p_samples)))
211
+
212
+ ss = np.random.SeedSequence(seed)
213
+ child_seeds = ss.spawn(n_eps)
214
+
215
+ f_eps = Parallel(n_jobs=n_jobs)(
216
+ delayed(_process_single_epsilon)(
217
+ eps, xs, ys, labels, p_samples, dcos, dsin, needed,
218
+ mapping, max_time, parameters, exits, escape, n_samples,
219
+ child_seeds[i],
220
+ )
221
+ for i, eps in enumerate(epsilons)
222
+ )
223
+
224
+ return epsilons, f_eps
225
+
226
+
227
+ def _process_single_epsilon(
228
+ eps: float,
229
+ xs: NDArray[np.float64],
230
+ ys: NDArray[np.float64],
231
+ labels: NDArray[np.float64],
232
+ p_samples: int,
233
+ dcos: NDArray[np.float64],
234
+ dsin: NDArray[np.float64],
235
+ needed: int,
236
+ mapping: object,
237
+ max_time: int,
238
+ parameters: object,
239
+ exits: object,
240
+ escape: str,
241
+ n_samples: int,
242
+ child_seed: np.random.SeedSequence,
243
+ ) -> float:
244
+ """Worker function that computes the uncertainty fraction for a single
245
+ epsilon value.
246
+ """
247
+
248
+ rng = np.random.default_rng(child_seed)
249
+ uncertain = 0
250
+
251
+ for x, y, side_center in zip(xs, ys, labels):
252
+ u_rand = rng.random(p_samples)
253
+ r = eps * np.sqrt(u_rand)
254
+ x_eps = x + r * dcos
255
+ y_eps = y + r * dsin
256
+
257
+ different = 0
258
+ for xp, yp in zip(x_eps, y_eps):
259
+ side_p, _ = mapping.escape_analysis(
260
+ u=(xp, yp),
261
+ max_time=max_time,
262
+ parameters=parameters,
263
+ exits=exits,
264
+ escape=escape,
265
+ )
266
+ if side_p != side_center:
267
+ different += 1
268
+ if different >= needed:
269
+ break
270
+
271
+ if different >= needed:
272
+ uncertain += 1
273
+
274
+ return uncertain / n_samples
275
+
@@ -0,0 +1,42 @@
1
+ # poincare.py
2
+
3
+ # Copyright (C) 2025-2026 Matheus Rolim Sales
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ from numba import njit
19
+ import numpy as np
20
+
21
+
22
+ @njit
23
+ def wrap_period(x: np.float64, period: np.float64) -> np.float64:
24
+ """Wrap x into (-period/2, period/2)."""
25
+ half = 0.5 * period
26
+ x = x % period
27
+ if x >= half:
28
+ x -= period
29
+ return x
30
+
31
+
32
+ @njit
33
+ def detect_crossing(g_old: np.float64, g_new: np.float64, crossing: int) -> bool:
34
+ if crossing == 0:
35
+ return bool(
36
+ (g_old < 0.0 and g_new >= 0.0)
37
+ or (g_old > 0.0 and g_new <= 0.0)
38
+ or (g_old == 0.0 and g_new != 0.0)
39
+ )
40
+ if crossing == 1:
41
+ return bool((g_old < 0.0 and g_new >= 0.0) or (g_old == 0.0 and g_new > 0.0))
42
+ return bool((g_old > 0.0 and g_new <= 0.0) or (g_old == 0.0 and g_new < 0.0))
@@ -59,29 +59,38 @@ hess_t: TypeAlias = Callable[
59
59
  NDArray[np.float64],
60
60
  ]
61
61
 
62
+ system_func_t: TypeAlias = Callable[..., NDArray[np.float64]]
63
+
64
+ # Equations of motion: f(q, p, parameters) -> (dq/dt, dp/dt)
65
+ # i.e. (dH/dp, -dH/dq), supplied directly by the user (not assembled
66
+ # from grad_T/grad_V, since H need not split that way)
67
+ eom_t: TypeAlias = Callable[
68
+ [NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]],
69
+ tuple[NDArray[np.float64], NDArray[np.float64]],
70
+ ]
71
+
72
+ # Full Hessian of H w.r.t. the combined state z = (q, p), shape (2n, 2n).
73
+ # Block layout: [[H_qq, H_qp], [H_pq, H_pp]]
74
+ hess_H_t: TypeAlias = Callable[
75
+ [NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]],
76
+ NDArray[np.float64],
77
+ ]
78
+
62
79
  symplectic_step_t: TypeAlias = Callable[
63
80
  [
64
81
  NDArray[np.float64],
65
82
  NDArray[np.float64],
66
83
  np.float64,
67
- grad_t,
68
- grad_t,
84
+ system_func_t,
85
+ system_func_t,
69
86
  NDArray[np.float64],
87
+ np.float64,
88
+ int,
70
89
  ],
71
90
  tuple[NDArray[np.float64], NDArray[np.float64]],
72
91
  ]
73
92
 
74
93
  symplectic_tangent_step_t: TypeAlias = Callable[
75
- [
76
- NDArray[np.float64],
77
- NDArray[np.float64],
78
- NDArray[np.float64],
79
- np.float64,
80
- grad_t,
81
- grad_t,
82
- hess_t,
83
- hess_t,
84
- NDArray[np.float64],
85
- ],
94
+ ...,
86
95
  tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]],
87
96
  ]
@@ -19,7 +19,7 @@ import numpy as np
19
19
  from numbers import Integral, Real
20
20
  from typing import Optional, Tuple
21
21
  from numpy.typing import NDArray
22
- from pynamicalsys.common.basin_analysis import basin_entropy, uncertainty_fraction
22
+ from pynamicalsys.common.basin_analysis import basin_entropy, uncertainty_fraction, uncertainty_fraction_mapping
23
23
 
24
24
 
25
25
  class BasinMetrics:
@@ -204,3 +204,207 @@ class BasinMetrics:
204
204
  n_eps=n_eps,
205
205
  epsilon_min=epsilon_min,
206
206
  )
207
+
208
+ def uncertainty_fraction_mapping(
209
+ self,
210
+ X,
211
+ Y,
212
+ mapping,
213
+ parameters,
214
+ exits,
215
+ escape='exiting',
216
+ n_samples=120_000,
217
+ p_samples=7,
218
+ threshold=0.1,
219
+ n_eps = 100,
220
+ max_time=1000,
221
+ seed=13,
222
+ n_jobs=-1):
223
+ """
224
+ Estimate the uncertainty fraction of a dynamical mapping using Monte Carlo sampling.
225
+
226
+ The computation is performed through random sampling of initial conditions and
227
+ perturbations of size ε, allowing the estimation of the scaling law:
228
+
229
+ f(ε) ~ ε^{⍺},
230
+
231
+ where ⍺ is the uncertainty exponent. For a D-dimensional phase space, the
232
+ dimension d of the basin boundary is related to ⍺ by:
233
+
234
+ d = D - ⍺.
235
+
236
+ Parameters
237
+ ----------
238
+ X : NDArray[np.float64]
239
+ 2D array containing the x-coordinates of grid.
240
+ Y : NDArray[np.float64]
241
+ 2D array containing the y-coordinates of grid.
242
+ mapping : callable
243
+ Dynamical mapping function describing the discrete-time system evolution.
244
+ The function must accept the current state and the system parameters as input.
245
+ parameters : list
246
+ Parameters passed to the mapping function.
247
+ exits : list
248
+ List containing the exit regions or exit conditions of the system.
249
+ escape : str, optional
250
+ Escape criterion type (default: ``"exiting"``).
251
+
252
+ Currently, only:
253
+ - ``"exiting"`` : trajectories are classified according to the exit reached.
254
+ is implemented.
255
+ n_samples : int, optional
256
+ Number of random initial conditions sampled for the Monte Carlo estimation
257
+ (default: 120000).
258
+ p_samples : int, optional
259
+ Number of perturbed neighbors generated for each sampled point
260
+ (default: 7).
261
+ threshold : float, optional
262
+ Fraction threshold used to classify a point as uncertain
263
+ (default: 0.1).
264
+
265
+ Must satisfy:
266
+
267
+ 0 <= threshold <= 1
268
+ n_eps : int, optional
269
+ Number of epsilon values used in the uncertainty scaling analysis
270
+ (default: 100).
271
+ max_time : int, optional
272
+ Maximum number of iterations allowed for trajectory evolution
273
+ (default: 1000).
274
+ seed : int, optional
275
+ Seed for the random number generator used during sampling
276
+ (default: 13).
277
+ n_jobs : int, optional
278
+ Number of parallel jobs used during computation
279
+ (default: -1).
280
+
281
+ Common values:
282
+ - ``-1`` : use all available CPU cores
283
+
284
+ Returns
285
+ -------
286
+ Tuple[NDArray[np.float64], NDArray[np.float64]]
287
+ A tuple containing:
288
+
289
+ - epsilons : Array of epsilon values.
290
+ - uncertainty_fraction : Array containing the estimated uncertainty
291
+ fraction corresponding to each epsilon value.
292
+
293
+ Raises
294
+ ------
295
+ ValueError
296
+ If:
297
+
298
+ - ``X`` or ``Y`` are not 2-dimensional arrays.
299
+ - ``X``, ``Y``, and ``basin`` do not have the same shape.
300
+ - ``parameters`` is ``None``.
301
+ - ``escape`` is not ``"exiting"``.
302
+ - ``n_samples`` is not a positive integer.
303
+ - ``p_samples`` is not a positive integer.
304
+ - ``n_eps`` is not a positive integer.
305
+ - ``max_time`` is not a positive integer.
306
+ - ``seed`` is negative.
307
+ - ``threshold`` is not between 0 and 1.
308
+ - ``n_jobs`` is zero.
309
+
310
+ Notes
311
+ -----
312
+ - This implementation uses Monte Carlo sampling to reduce computational cost
313
+ when dealing with large phase spaces.
314
+ - Parallel execution is supported through the ``n_jobs`` parameter.
315
+
316
+ Examples
317
+ --------
318
+ >>> import numpy as np
319
+ >>> from numba import njit
320
+ >>> from joblib import Parallel, delayed
321
+ >>> from pynamicalsys import BasinMetrics, DiscreteDynamicalSystem as dds
322
+ >>>
323
+ >>> @njit
324
+ ... def henon_map(state, parameters):
325
+ ... x, y = state
326
+ ... a, b = parameters
327
+ ... return np.array([1 - a*x**2 + y, b*x])
328
+ >>>
329
+ >>> henon = dds(
330
+ ... mapping=henon_map,
331
+ ... number_of_parameters=2,
332
+ ... system_dimension=2,
333
+ ... )
334
+ >>>
335
+ >>> grid_size = 1000
336
+ >>> x = np.linspace(-2, 2, grid_size)
337
+ >>> y = np.linspace(-2, 2, grid_size)
338
+ >>> X, Y = np.meshgrid(x, y, indexing='ij')
339
+ >>> grid_points = np.column_stack((X.ravel(), Y.ravel()))
340
+ >>>
341
+ >>> exits = np.array([[-10, 10], [-10, 10]], dtype=np.float64)
342
+ >>> N = 2000
343
+ >>>
344
+ >>> escape = np.array(Parallel(n_jobs=-1)(
345
+ ... delayed(henon.escape_analysis)(
346
+ ... u, N, exits, parameters=[1.45, 0.3], escape="exiting"
347
+ ... )
348
+ ... for u in grid_points
349
+ ... ))
350
+ >>>
351
+ >>> basin = escape[:, 0].reshape(grid_size, grid_size)
352
+ >>>
353
+ >>> metrics = BasinMetrics(basin)
354
+ >>>
355
+ >>> epsilons, f = metrics.uncertainty_fraction_mapping(
356
+ ... X,
357
+ ... Y,
358
+ ... mapping=henon,
359
+ ... parameters=[1.40, 0.3],
360
+ ... exits=exits,
361
+ ... n_samples=10_000,
362
+ ... p_samples=7,
363
+ ... max_time=1000,
364
+ ... )
365
+ """
366
+
367
+ X = np.asarray(X, dtype=np.float64)
368
+ Y = np.asarray(Y, dtype=np.float64)
369
+
370
+ if X.ndim != 2 or Y.ndim != 2:
371
+ raise ValueError("X, Y, and basin must be 2-dimensional arrays")
372
+ if X.shape != Y.shape or X.shape != self.basin.shape:
373
+ raise ValueError("X, Y, and basin must have the same shape")
374
+ if parameters is None:
375
+ raise ValueError("parameters cannot be None")
376
+ if escape != "exiting":
377
+ raise ValueError("escape must be 'exiting'. Option 'entering' is not implemented yet")
378
+ if not isinstance(n_samples, int) or n_samples <= 0:
379
+ raise ValueError("n_samples must be a positive integer")
380
+ if not isinstance(p_samples, int) or p_samples <= 0:
381
+ raise ValueError("p_samples must be a positive integer")
382
+ if not isinstance(n_eps, int) or n_eps <= 0:
383
+ raise ValueError("n_eps must be a positive integer")
384
+ if not isinstance(max_time, int) or max_time <= 0:
385
+ raise ValueError("max_time must be a positive integer")
386
+ if not isinstance(seed, int) or seed < 0:
387
+ raise ValueError("seed must be a non-negative integer")
388
+ if not isinstance(threshold, (int, float)) or not (0 <= threshold <= 1):
389
+ raise ValueError("threshold must be a float between 0 and 1")
390
+ if n_jobs == 0:
391
+ raise ValueError("n_jobs cannot be zero")
392
+
393
+
394
+ return uncertainty_fraction_mapping(
395
+ X,
396
+ Y,
397
+ self.basin,
398
+ mapping,
399
+ parameters,
400
+ exits,
401
+ escape,
402
+ n_samples,
403
+ p_samples,
404
+ threshold,
405
+ n_eps,
406
+ max_time,
407
+ seed,
408
+ n_jobs
409
+ )
410
+