pynamicalsys 1.0.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.
- pynamicalsys/__init__.py +24 -0
- pynamicalsys/__version__.py +21 -0
- pynamicalsys/common/__init__.py +16 -0
- pynamicalsys/common/basin_analysis.py +170 -0
- pynamicalsys/common/recurrence_quantification_analysis.py +426 -0
- pynamicalsys/common/utils.py +344 -0
- pynamicalsys/continuous_time/__init__.py +16 -0
- pynamicalsys/core/__init__.py +16 -0
- pynamicalsys/core/basin_metrics.py +206 -0
- pynamicalsys/core/continuous_dynamical_systems.py +18 -0
- pynamicalsys/core/discrete_dynamical_systems.py +3391 -0
- pynamicalsys/core/plot_styler.py +155 -0
- pynamicalsys/core/time_series_metrics.py +139 -0
- pynamicalsys/discrete_time/__init__.py +16 -0
- pynamicalsys/discrete_time/dynamical_indicators.py +1226 -0
- pynamicalsys/discrete_time/models.py +435 -0
- pynamicalsys/discrete_time/trajectory_analysis.py +1459 -0
- pynamicalsys/discrete_time/transport.py +501 -0
- pynamicalsys/discrete_time/validators.py +313 -0
- pynamicalsys-1.0.0.dist-info/METADATA +791 -0
- pynamicalsys-1.0.0.dist-info/RECORD +23 -0
- pynamicalsys-1.0.0.dist-info/WHEEL +5 -0
- pynamicalsys-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,344 @@
|
|
1
|
+
# utils.py
|
2
|
+
|
3
|
+
# Copyright (C) 2025 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
|
+
import numpy as np
|
19
|
+
from typing import Callable, Tuple
|
20
|
+
from numpy.typing import NDArray
|
21
|
+
from numba import njit
|
22
|
+
|
23
|
+
|
24
|
+
@njit(cache=True)
|
25
|
+
def qr(M: NDArray[np.float64]) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
|
26
|
+
"""
|
27
|
+
Perform numerically stable QR decomposition using modified Gram-Schmidt with reorthogonalization.
|
28
|
+
|
29
|
+
Parameters
|
30
|
+
----------
|
31
|
+
M : NDArray[np.float64]
|
32
|
+
Input matrix of shape (m, n) with linearly independent columns.
|
33
|
+
|
34
|
+
Returns
|
35
|
+
-------
|
36
|
+
Tuple[NDArray[np.float64], NDArray[np.float64]]
|
37
|
+
Q: Orthonormal matrix (m, n)
|
38
|
+
R: Upper triangular matrix (n, n)
|
39
|
+
|
40
|
+
Notes
|
41
|
+
-----
|
42
|
+
- Implements modified Gram-Schmidt with iterative refinement
|
43
|
+
- Includes additional reorthogonalization steps for stability
|
44
|
+
- Uses double precision throughout for accuracy
|
45
|
+
- Automatically handles rank-deficient cases with warnings
|
46
|
+
|
47
|
+
Examples
|
48
|
+
--------
|
49
|
+
>>> M = np.array([[1.0, 1.0], [1.0, 0.0], [0.0, 1.0]])
|
50
|
+
>>> Q, R = qr(M)
|
51
|
+
>>> np.allclose(M, Q @ R)
|
52
|
+
True
|
53
|
+
>>> np.allclose(Q.T @ Q, np.eye(2))
|
54
|
+
True
|
55
|
+
"""
|
56
|
+
m, n = M.shape
|
57
|
+
Q = np.ascontiguousarray(M.copy())
|
58
|
+
R = np.ascontiguousarray(np.zeros((n, n)))
|
59
|
+
eps = np.finfo(np.float64).eps # Machine epsilon for stability checks
|
60
|
+
|
61
|
+
for i in range(n):
|
62
|
+
# First orthogonalization pass
|
63
|
+
for k in range(i):
|
64
|
+
R[k, i] = np.dot(
|
65
|
+
np.ascontiguousarray(Q[:, k]), np.ascontiguousarray(Q[:, i])
|
66
|
+
)
|
67
|
+
Q[:, i] -= R[k, i] * Q[:, k]
|
68
|
+
|
69
|
+
# Compute norm and check for linear dependence
|
70
|
+
norm = np.linalg.norm(Q[:, i])
|
71
|
+
if norm < eps * m: # Adjust threshold based on matrix size
|
72
|
+
# Handle near-linear dependence
|
73
|
+
Q[:, i] = np.random.randn(m)
|
74
|
+
Q[:, i] /= np.linalg.norm(Q[:, i])
|
75
|
+
norm = 1.0
|
76
|
+
|
77
|
+
R[i, i] = norm
|
78
|
+
Q[:, i] /= norm
|
79
|
+
|
80
|
+
# Optional second reorthogonalization pass for stability
|
81
|
+
for k in range(i):
|
82
|
+
dot = np.dot(np.ascontiguousarray(Q[:, k]), np.ascontiguousarray(Q[:, i]))
|
83
|
+
R[k, i] += dot
|
84
|
+
Q[:, i] -= dot * Q[:, k]
|
85
|
+
|
86
|
+
# Renormalize after reorthogonalization
|
87
|
+
new_norm = np.linalg.norm(Q[:, i])
|
88
|
+
if new_norm < 0.1: # Significant cancellation occurred
|
89
|
+
Q[:, i] /= new_norm
|
90
|
+
R[i, i] *= new_norm
|
91
|
+
|
92
|
+
return Q, R
|
93
|
+
|
94
|
+
|
95
|
+
@njit(cache=True)
|
96
|
+
def householder_qr(
|
97
|
+
M: NDArray[np.float64],
|
98
|
+
) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
|
99
|
+
"""
|
100
|
+
Compute the QR decomposition using Householder reflections with enhanced numerical stability.
|
101
|
+
|
102
|
+
This implementation includes:
|
103
|
+
- Column pivoting for rank-deficient matrices
|
104
|
+
- Careful handling of sign choices to minimize cancellation
|
105
|
+
- Efficient accumulation of Q matrix
|
106
|
+
- Special handling of small submatrices
|
107
|
+
|
108
|
+
Parameters
|
109
|
+
----------
|
110
|
+
M : NDArray[np.float64]
|
111
|
+
Input matrix of shape (m, n) where m >= n
|
112
|
+
|
113
|
+
Returns
|
114
|
+
-------
|
115
|
+
Tuple[NDArray[np.float64], NDArray[np.float64]]
|
116
|
+
Q: Orthogonal matrix (m×m)
|
117
|
+
R: Upper triangular matrix (m×n)
|
118
|
+
|
119
|
+
Raises
|
120
|
+
------
|
121
|
+
ValueError
|
122
|
+
If input matrix has more columns than rows (m < n)
|
123
|
+
|
124
|
+
Notes
|
125
|
+
-----
|
126
|
+
- For rank-deficient matrices, consider using column pivoting (not shown here)
|
127
|
+
- The implementation uses the most numerically stable sign choice
|
128
|
+
- Accumulates Q implicitly for better performance
|
129
|
+
- Automatically handles edge cases like zero columns
|
130
|
+
|
131
|
+
Examples
|
132
|
+
--------
|
133
|
+
>>> # Well-conditioned matrix
|
134
|
+
>>> M = np.array([[3.0, 1.0], [4.0, 2.0]], dtype=np.float64)
|
135
|
+
>>> Q, R = householder_qr(M)
|
136
|
+
>>> np.allclose(Q @ R, M, atol=1e-10)
|
137
|
+
True
|
138
|
+
|
139
|
+
>>> # Rank-deficient case
|
140
|
+
>>> M = np.array([[1.0, 2.0], [2.0, 4.0]], dtype=np.float64)
|
141
|
+
>>> Q, R = householder_qr(M)
|
142
|
+
>>> np.abs(R[1,1]) < 1e-10 # Second column is dependent
|
143
|
+
True
|
144
|
+
"""
|
145
|
+
m, n = M.shape
|
146
|
+
if m < n:
|
147
|
+
raise ValueError("Input matrix must have m >= n for QR decomposition")
|
148
|
+
|
149
|
+
# Initialize Q as identity matrix (will accumulate Householder transformations)
|
150
|
+
Q = np.eye(m)
|
151
|
+
|
152
|
+
# Initialize R as a copy of input matrix (will be transformed to upper triangular)
|
153
|
+
R = M.copy().astype(np.float64)
|
154
|
+
|
155
|
+
for k in range(n):
|
156
|
+
# Extract the subcolumn from current diagonal downward
|
157
|
+
x = R[k:, k]
|
158
|
+
|
159
|
+
# Skip if the subcolumn is already zero (for numerical stability)
|
160
|
+
if np.allclose(x[1:], 0.0):
|
161
|
+
continue
|
162
|
+
|
163
|
+
# Create basis vector e1 = [1, 0, ..., 0] of same length as x
|
164
|
+
e1 = np.zeros_like(x)
|
165
|
+
e1[0] = 1.0
|
166
|
+
|
167
|
+
# Compute Householder vector v:
|
168
|
+
# v = sign(x[0])*||x||*e1 + x
|
169
|
+
# The sign choice ensures numerical stability (avoids cancellation)
|
170
|
+
v = np.sign(x[0]) * np.linalg.norm(x) * e1 + x
|
171
|
+
v = v / np.linalg.norm(v) # Normalize v
|
172
|
+
|
173
|
+
# Construct Householder reflector H = I - 2vv^T
|
174
|
+
# We build it as an extension of the identity matrix
|
175
|
+
H = np.eye(m)
|
176
|
+
H[k:, k:] -= 2.0 * np.outer(v, v)
|
177
|
+
|
178
|
+
# Apply reflector to R (zeroing out below-diagonal elements in column k)
|
179
|
+
R = H @ R
|
180
|
+
|
181
|
+
# Accumulate the reflection in Q (Q = Q * H^T, since H is symmetric)
|
182
|
+
Q = Q @ H.T
|
183
|
+
|
184
|
+
return Q, R
|
185
|
+
|
186
|
+
|
187
|
+
@njit(cache=True)
|
188
|
+
def finite_difference_jacobian(
|
189
|
+
u: NDArray[np.float64],
|
190
|
+
parameters: NDArray[np.float64],
|
191
|
+
mapping: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
192
|
+
eps: float = -1.0,
|
193
|
+
) -> NDArray[np.float64]:
|
194
|
+
"""
|
195
|
+
Compute the Jacobian matrix using adaptive finite differences with error control.
|
196
|
+
|
197
|
+
Parameters
|
198
|
+
----------
|
199
|
+
u : NDArray[np.float64]
|
200
|
+
State vector at which to compute Jacobian (shape: (n,))
|
201
|
+
parameters : NDArray[np.float64]
|
202
|
+
System parameters
|
203
|
+
mapping : Callable[[NDArray, NDArray], NDArray]
|
204
|
+
Vector-valued function to differentiate
|
205
|
+
eps : float, optional
|
206
|
+
Initial step size (automatically determined if -1.0)
|
207
|
+
|
208
|
+
Returns
|
209
|
+
-------
|
210
|
+
NDArray[np.float64]
|
211
|
+
Jacobian matrix (shape: (n, n)) where J[i,j] = ∂f_i/∂u_j
|
212
|
+
|
213
|
+
Raises
|
214
|
+
------
|
215
|
+
ValueError
|
216
|
+
If invalid method is specified
|
217
|
+
If eps is not positive when provided
|
218
|
+
|
219
|
+
Notes
|
220
|
+
-----
|
221
|
+
- For 'central' method (default), accuracy is O(eps²)
|
222
|
+
- For 'complex' method, accuracy is O(eps⁴) but requires complex arithmetic
|
223
|
+
- Automatic step size selection based on machine epsilon and input scale
|
224
|
+
- Includes Richardson extrapolation for higher accuracy
|
225
|
+
- Handles edge cases like zero components carefully
|
226
|
+
|
227
|
+
Examples
|
228
|
+
--------
|
229
|
+
>>> def lorenz(u, p):
|
230
|
+
... x, y, z = u
|
231
|
+
... sigma, rho, beta = p
|
232
|
+
... return np.array([sigma*(y-x), x*(rho-z)-y, x*y-beta*z])
|
233
|
+
>>> u = np.array([1.0, 1.0, 1.0])
|
234
|
+
>>> params = np.array([10.0, 28.0, 8/3])
|
235
|
+
>>> J = finite_difference_jacobian(u, params, lorenz, method='central')
|
236
|
+
"""
|
237
|
+
n = len(u)
|
238
|
+
J = np.zeros((n, n))
|
239
|
+
|
240
|
+
# Determine optimal step size if not provided
|
241
|
+
if eps <= 0:
|
242
|
+
eps = float(np.finfo(np.float64).eps) ** (1 / 3) * max(
|
243
|
+
1.0, float(np.linalg.norm(u))
|
244
|
+
)
|
245
|
+
|
246
|
+
for i in range(n):
|
247
|
+
# Central difference: O(eps²) accuracy
|
248
|
+
u_plus = u.copy()
|
249
|
+
u_minus = u.copy()
|
250
|
+
u_plus[i] += eps
|
251
|
+
u_minus[i] -= eps
|
252
|
+
J[:, i] = (mapping(u_plus, parameters) - mapping(u_minus, parameters)) / (
|
253
|
+
2 * eps
|
254
|
+
)
|
255
|
+
|
256
|
+
return J
|
257
|
+
|
258
|
+
|
259
|
+
@njit
|
260
|
+
def wedge_product_norm(vectors: NDArray[np.float64]) -> float:
|
261
|
+
"""
|
262
|
+
Computes the norm of the wedge product of n m-dimensional vectors using the Gram determinant.
|
263
|
+
|
264
|
+
Parameters:
|
265
|
+
vectors : NDArray[np.float64]
|
266
|
+
A (m, n) array where m is the dimension and n is the number of vectors.
|
267
|
+
|
268
|
+
Returns:
|
269
|
+
norm : float
|
270
|
+
The norm (magnitude) of the wedge product.
|
271
|
+
"""
|
272
|
+
m, n = vectors.shape
|
273
|
+
if n > m:
|
274
|
+
raise ValueError(
|
275
|
+
"Cannot compute the wedge product: more vectors than dimensions."
|
276
|
+
)
|
277
|
+
|
278
|
+
# Compute the Gram matrix
|
279
|
+
G = np.zeros((n, n))
|
280
|
+
for i in range(n):
|
281
|
+
for j in range(n):
|
282
|
+
dot = 0.0
|
283
|
+
for k in range(m):
|
284
|
+
dot += vectors[k, i] * vectors[k, j]
|
285
|
+
G[i, j] = dot
|
286
|
+
|
287
|
+
# Compute determinant
|
288
|
+
det = np.linalg.det(G)
|
289
|
+
|
290
|
+
# If determinant is slightly negative due to numerical error, clip to 0
|
291
|
+
if det < 0:
|
292
|
+
det = 0.0
|
293
|
+
|
294
|
+
norm = np.sqrt(det)
|
295
|
+
return norm
|
296
|
+
|
297
|
+
|
298
|
+
@njit
|
299
|
+
def _coeff_mat(x: NDArray[np.float64], deg: int) -> NDArray[np.float64]:
|
300
|
+
mat_ = np.zeros(shape=(x.shape[0], deg + 1))
|
301
|
+
const = np.ones_like(x)
|
302
|
+
mat_[:, 0] = const
|
303
|
+
mat_[:, 1] = x
|
304
|
+
if deg > 1:
|
305
|
+
for n in range(2, deg + 1):
|
306
|
+
mat_[:, n] = x**n
|
307
|
+
return mat_
|
308
|
+
|
309
|
+
|
310
|
+
@njit
|
311
|
+
def _fit_x(a: NDArray[np.float64], b: NDArray[np.float64]) -> NDArray[np.float64]:
|
312
|
+
# linalg solves ax = b
|
313
|
+
det_ = np.linalg.lstsq(a, b)[0]
|
314
|
+
return det_
|
315
|
+
|
316
|
+
|
317
|
+
@njit
|
318
|
+
def fit_poly(
|
319
|
+
x: NDArray[np.float64], y: NDArray[np.float64], deg: int
|
320
|
+
) -> NDArray[np.float64]:
|
321
|
+
a = _coeff_mat(x, deg)
|
322
|
+
p = _fit_x(a, y)
|
323
|
+
# Reverse order so p[0] is coefficient of highest order
|
324
|
+
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)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# # __init__.py
|
2
|
+
|
3
|
+
# # Copyright (C) 2025 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/>.
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# # __init__.py
|
2
|
+
|
3
|
+
# # Copyright (C) 2025 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/>.
|
@@ -0,0 +1,206 @@
|
|
1
|
+
# basin_metrics.py
|
2
|
+
|
3
|
+
# Copyright (C) 2025 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
|
+
import numpy as np
|
19
|
+
from numbers import Integral, Real
|
20
|
+
from typing import Optional, Tuple
|
21
|
+
from numpy.typing import NDArray
|
22
|
+
from pynamicalsys.common.basin_analysis import basin_entropy, uncertainty_fraction
|
23
|
+
|
24
|
+
|
25
|
+
class BasinMetrics:
|
26
|
+
"""A class for computing metrics related to basin of attraction analysis, such as basin entropy and uncertainty fraction.
|
27
|
+
|
28
|
+
This class provides methods to quantify the unpredictability and complexity of basins of attraction in dynamical systems. It supports calculation of basin entropy, boundary basin entropy, and the uncertainty fraction, which are useful for characterizing the structure and boundaries of basins.
|
29
|
+
|
30
|
+
Parameters
|
31
|
+
----------
|
32
|
+
basin : NDArray[np.float64]
|
33
|
+
A 2D array representing the basin of attraction, where each element indicates
|
34
|
+
the final state (attractor) for that initial condition (shape: (Nx, Ny)).
|
35
|
+
|
36
|
+
Raises
|
37
|
+
------
|
38
|
+
ValueError
|
39
|
+
If `basin` is not a 2-dimensional array.
|
40
|
+
|
41
|
+
Notes
|
42
|
+
-----
|
43
|
+
The basin should be a 2D array where each element represents the final state
|
44
|
+
(attractor) for that initial condition. The shape of the basin should be (Nx, Ny),
|
45
|
+
where Nx is the number of rows and Ny is the number of columns.
|
46
|
+
|
47
|
+
Examples
|
48
|
+
--------
|
49
|
+
>>> import numpy as np
|
50
|
+
>>> from pynamicalsys import BasinMetrics
|
51
|
+
>>> basin = np.array([[0, 1], [1, 0]])
|
52
|
+
>>> metrics = BasinMetrics(basin)
|
53
|
+
"""
|
54
|
+
|
55
|
+
def __init__(self, basin: NDArray[np.float64]) -> None:
|
56
|
+
self.basin = basin
|
57
|
+
|
58
|
+
if isinstance(self.basin, list):
|
59
|
+
self.basin = np.array(self.basin, dtype=np.float64)
|
60
|
+
|
61
|
+
if basin.ndim != 2:
|
62
|
+
raise ValueError("basin must be 2-dimensional")
|
63
|
+
|
64
|
+
pass
|
65
|
+
|
66
|
+
def basin_entropy(
|
67
|
+
self,
|
68
|
+
n: int,
|
69
|
+
log_base: float = np.e,
|
70
|
+
nx: Optional[int] = None,
|
71
|
+
ny: Optional[int] = None,
|
72
|
+
) -> Tuple[float, float]:
|
73
|
+
"""Calculate the basin entropy (Sb) and boundary basin entropy (Sbb) of a 2D basin.
|
74
|
+
|
75
|
+
The basin entropy quantifies the uncertainty in final state prediction, while the boundary
|
76
|
+
entropy specifically measures uncertainty at basin boundaries where multiple attractors coexist.
|
77
|
+
|
78
|
+
Parameters
|
79
|
+
----------
|
80
|
+
n : int
|
81
|
+
Default size of square sub-boxes for partitioning (must be positive).
|
82
|
+
log : float, optional
|
83
|
+
Logarithm base for entropy calculation (default: np.e, which is natural logarithm).
|
84
|
+
|
85
|
+
Returns
|
86
|
+
-------
|
87
|
+
Tuple[float, float]
|
88
|
+
A tuple containing:
|
89
|
+
|
90
|
+
- Sb: Basin entropy
|
91
|
+
- Sbb: Boundary basin entropy
|
92
|
+
|
93
|
+
Raises
|
94
|
+
------
|
95
|
+
ValueError
|
96
|
+
If `n`, is not positive integer, or if `log_base` is not positive.
|
97
|
+
|
98
|
+
Notes
|
99
|
+
-----
|
100
|
+
The basin entropy is calculated by partitioning the basin into sub-boxes of size `n` and computing the entropy of each sub-box. The boundary basin entropy is computed similarly but focuses on the sub-boxes that lie on the boundaries of the basin where multiple attractors coexist.
|
101
|
+
|
102
|
+
Examples
|
103
|
+
--------
|
104
|
+
>>> import numpy as np
|
105
|
+
>>> np.random.seed(13)
|
106
|
+
>>> basin = np.random.randint(1, 4, size=(1000, 1000))
|
107
|
+
>>> from pynamicalsys import BasinMetrics
|
108
|
+
>>> metrics = BasinMetrics(basin)
|
109
|
+
>>> metrics.basin_entropy(n=5, log_base=2)
|
110
|
+
(1.5251876046167432, 1.5251876046167432)
|
111
|
+
"""
|
112
|
+
|
113
|
+
if not isinstance(n, Integral) or n <= 0:
|
114
|
+
raise ValueError("n must be positive integer")
|
115
|
+
|
116
|
+
if log_base <= 0:
|
117
|
+
raise ValueError("log_base must be positive")
|
118
|
+
|
119
|
+
return basin_entropy(basin=self.basin, n=n, log_base=log_base)
|
120
|
+
|
121
|
+
def uncertainty_fraction(
|
122
|
+
self,
|
123
|
+
x: NDArray[np.float64],
|
124
|
+
y: NDArray[np.float64],
|
125
|
+
epsilon_max: float = 0.1,
|
126
|
+
n_eps: int = 100,
|
127
|
+
epsilon_min: Optional[int] = None,
|
128
|
+
) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
|
129
|
+
"""Calculate the uncertainty fraction for a given basin.
|
130
|
+
|
131
|
+
This method computes the uncertainty fraction for each point in the basin
|
132
|
+
based on the provided parameters.
|
133
|
+
|
134
|
+
Parameters
|
135
|
+
----------
|
136
|
+
x : NDArray[np.float64]
|
137
|
+
2D array of the basin's x-coordinates.
|
138
|
+
y : NDArray[np.float64]
|
139
|
+
2D array of the basin's y-coordinates.
|
140
|
+
epsilon_max : float, optional
|
141
|
+
Maximum epsilon value (default: 0.1).
|
142
|
+
n_eps : int, optional
|
143
|
+
Number of epsilon values to consider (default: 100).
|
144
|
+
epsilon_min : int, optional
|
145
|
+
Minimum epsilon value (default: None).
|
146
|
+
|
147
|
+
Returns
|
148
|
+
-------
|
149
|
+
Tuple[NDArray[np.float64], NDArray[np.float64]]
|
150
|
+
A tuple containing:
|
151
|
+
|
152
|
+
- epsilons: Array of epsilon values.
|
153
|
+
- uncertainty_fraction: Array of uncertainty fractions corresponding to each epsilon.
|
154
|
+
|
155
|
+
Notes
|
156
|
+
-----
|
157
|
+
- The uncertainty fraction scales with ε as a power law: f(ε) ~ ε^{⍺}, where ⍺ is the uncertainty exponent.
|
158
|
+
- For D-dimensional basins, the dimension d of the basin boundary is given by d = D - ⍺.
|
159
|
+
|
160
|
+
Examples
|
161
|
+
--------
|
162
|
+
>>> # Create a basin of 0's and 1's, where the 1's form a rectangle, i.e., d = 1
|
163
|
+
>>> grid_size = 10000
|
164
|
+
>>> x_range = (0, 1, grid_size)
|
165
|
+
>>> y_range = (0, 1, grid_size)
|
166
|
+
>>> x = np.linspace(*x_range)
|
167
|
+
>>> y = np.linspace(*y_range)
|
168
|
+
>>> X, Y = np.meshgrid(x, y, indexing='ij')
|
169
|
+
>>> obj = [[0.2, 0.6],
|
170
|
+
[0.2, 0.6]]
|
171
|
+
>>> basin = np.zeros((grid_size, grid_size), dtype=int)
|
172
|
+
>>> basin[mask] = 1
|
173
|
+
>>> bm = BasinMetrics(basin)
|
174
|
+
>>> eps, f = bm.uncertainty_fraction(X, Y, epsilon_max=0.1)
|
175
|
+
"""
|
176
|
+
|
177
|
+
if isinstance(x, list):
|
178
|
+
x = np.array(x, dtype=np.float64)
|
179
|
+
if isinstance(y, list):
|
180
|
+
y = np.array(y, dtype=np.float64)
|
181
|
+
|
182
|
+
if x.ndim != 2 or y.ndim != 2:
|
183
|
+
raise ValueError("x, y, and basin must be 2-dimensional arrays")
|
184
|
+
if x.shape != y.shape or x.shape != self.basin.shape:
|
185
|
+
raise ValueError("x, y, and basin must have the same shape")
|
186
|
+
|
187
|
+
if not isinstance(epsilon_max, Real) or epsilon_max < 0:
|
188
|
+
raise ValueError("epsilon_min must be a non-negative real number")
|
189
|
+
|
190
|
+
if not isinstance(n_eps, Integral) or n_eps <= 0:
|
191
|
+
raise ValueError("n_eps must be a positive integer")
|
192
|
+
|
193
|
+
if epsilon_min is not None:
|
194
|
+
if not isinstance(epsilon_min, Real) and epsilon_min < 0:
|
195
|
+
raise ValueError("epsilon_min must be a non-negative real number")
|
196
|
+
else:
|
197
|
+
epsilon_min = 0.0
|
198
|
+
|
199
|
+
return uncertainty_fraction(
|
200
|
+
x=x,
|
201
|
+
y=y,
|
202
|
+
basin=self.basin,
|
203
|
+
epsilon_max=epsilon_max,
|
204
|
+
n_eps=n_eps,
|
205
|
+
epsilon_min=epsilon_min,
|
206
|
+
)
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# continuous_dynamical_systems.py
|
2
|
+
|
3
|
+
# Copyright (C) 2025 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
|
+
"""To be implemented: Continuous Dynamical Systems (CDS) class."""
|