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.
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/CHANGELOG.md +28 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/PKG-INFO +1 -1
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/__version__.py +3 -3
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/common/basin_analysis.py +105 -0
- pynamicalsys-1.6.0/src/pynamicalsys/common/poincare.py +42 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/common/types.py +22 -13
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/core/basin_metrics.py +205 -1
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/core/hamiltonian_systems.py +565 -188
- pynamicalsys-1.6.0/src/pynamicalsys/hamiltonian_systems/__init__.py +16 -0
- pynamicalsys-1.6.0/src/pynamicalsys/hamiltonian_systems/clv.py +950 -0
- pynamicalsys-1.6.0/src/pynamicalsys/hamiltonian_systems/fixed_step.py +249 -0
- pynamicalsys-1.6.0/src/pynamicalsys/hamiltonian_systems/gali.py +313 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/hamiltonian_systems/hurst.py +30 -10
- pynamicalsys-1.5.4/src/pynamicalsys/hamiltonian_systems/lyapunov.py → pynamicalsys-1.6.0/src/pynamicalsys/hamiltonian_systems/ldi.py +117 -117
- pynamicalsys-1.6.0/src/pynamicalsys/hamiltonian_systems/lyapunov.py +506 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/hamiltonian_systems/models.py +41 -1
- pynamicalsys-1.6.0/src/pynamicalsys/hamiltonian_systems/poincare.py +368 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/hamiltonian_systems/rte.py +29 -9
- pynamicalsys-1.6.0/src/pynamicalsys/hamiltonian_systems/sali.py +255 -0
- pynamicalsys-1.6.0/src/pynamicalsys/hamiltonian_systems/tangent.py +438 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/hamiltonian_systems/trajectory.py +36 -21
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys.egg-info/PKG-INFO +1 -1
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys.egg-info/SOURCES.txt +3 -0
- pynamicalsys-1.6.0/src/pynamicalsys.egg-info/scm_file_list.json +218 -0
- pynamicalsys-1.6.0/src/pynamicalsys.egg-info/scm_version.json +8 -0
- pynamicalsys-1.5.4/src/pynamicalsys/hamiltonian_systems/__init__.py +0 -16
- pynamicalsys-1.5.4/src/pynamicalsys/hamiltonian_systems/clv.py +0 -452
- pynamicalsys-1.5.4/src/pynamicalsys/hamiltonian_systems/fixed_step.py +0 -118
- pynamicalsys-1.5.4/src/pynamicalsys/hamiltonian_systems/gali.py +0 -167
- pynamicalsys-1.5.4/src/pynamicalsys/hamiltonian_systems/ldi.py +0 -136
- pynamicalsys-1.5.4/src/pynamicalsys/hamiltonian_systems/poincare.py +0 -291
- pynamicalsys-1.5.4/src/pynamicalsys/hamiltonian_systems/sali.py +0 -135
- pynamicalsys-1.5.4/src/pynamicalsys/hamiltonian_systems/tangent.py +0 -249
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/.gitignore +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/CITATION.cff +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/CONTRIBUTING.md +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/LICENSE +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/MANIFEST.in +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/README.md +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/pyproject.toml +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/setup.cfg +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/__init__.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/common/__init__.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/common/clv.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/common/differentiation.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/common/fitting.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/common/hurst.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/common/linalg.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/common/recurrence_quantification_analysis.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/common/validators.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/__init__.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/adaptive_step.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/basins.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/clv.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/fixed_step.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/gali.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/hurst.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/ldi.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/lyapunov.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/maxima_map.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/models.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/poincare.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/rte.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/sali.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/step.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/step_methods.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/stroboscopic.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/trajectory.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/validators.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/continuous_time/variational.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/core/__init__.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/core/continuous_dynamical_systems.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/core/discrete_dynamical_systems.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/core/plot_styler.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/core/time_series_metrics.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/__init__.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/averages.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/bifurcation.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/birkhoff.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/clv.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/escape.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/gali.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/hurst.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/ldi.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/lyapunov.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/manifolds.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/models.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/periodic_orbits.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/rotation.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/rte.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/sali.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/stability.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/symmetry.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/trajectory.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/transport.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/discrete_time/validators.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/hamiltonian_systems/coefficients.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys/hamiltonian_systems/validators.py +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys.egg-info/dependency_links.txt +0 -0
- {pynamicalsys-1.5.4 → pynamicalsys-1.6.0}/src/pynamicalsys.egg-info/requires.txt +0 -0
- {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
|
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (1,
|
|
21
|
+
__version__ = version = '1.6.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 6, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
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
|
-
|
|
68
|
-
|
|
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
|
+
|