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.
@@ -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."""