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
pynamicalsys/__init__.py
ADDED
@@ -0,0 +1,24 @@
|
|
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/>.
|
17
|
+
|
18
|
+
from pynamicalsys.core.discrete_dynamical_systems import DiscreteDynamicalSystem
|
19
|
+
from pynamicalsys.core.basin_metrics import BasinMetrics
|
20
|
+
from pynamicalsys.core.plot_styler import PlotStyler
|
21
|
+
from pynamicalsys.core.time_series_metrics import TimeSeriesMetrics
|
22
|
+
from .__version__ import __version__
|
23
|
+
|
24
|
+
__all__ = ["DiscreteDynamicalSystem", "PlotStyler", "TimeSeriesMetrics", "BasinMetrics"]
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# file generated by setuptools-scm
|
2
|
+
# don't change, don't track in version control
|
3
|
+
|
4
|
+
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
5
|
+
|
6
|
+
TYPE_CHECKING = False
|
7
|
+
if TYPE_CHECKING:
|
8
|
+
from typing import Tuple
|
9
|
+
from typing import Union
|
10
|
+
|
11
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
12
|
+
else:
|
13
|
+
VERSION_TUPLE = object
|
14
|
+
|
15
|
+
version: str
|
16
|
+
__version__: str
|
17
|
+
__version_tuple__: VERSION_TUPLE
|
18
|
+
version_tuple: VERSION_TUPLE
|
19
|
+
|
20
|
+
__version__ = version = '1.0.0'
|
21
|
+
__version_tuple__ = version_tuple = (1, 0, 0)
|
@@ -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,170 @@
|
|
1
|
+
# basin_analysis.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 numba import njit, prange
|
20
|
+
from typing import Optional, Tuple
|
21
|
+
from numpy.typing import NDArray
|
22
|
+
|
23
|
+
|
24
|
+
def uncertainty_fraction(x, y, basin, epsilon_max, n_eps=100, epsilon_min=0):
|
25
|
+
"""
|
26
|
+
Wrapper to compute uncertainty fractions using a njit-compatible core function.
|
27
|
+
"""
|
28
|
+
|
29
|
+
# Estimate dx and dy (uniform spacing assumed)
|
30
|
+
dx = np.mean(np.abs(x[1:, :] - x[:-1, :]))
|
31
|
+
dy = np.mean(np.abs(y[:, 1:] - y[:, :-1]))
|
32
|
+
if epsilon_min != 0:
|
33
|
+
min_epsilon = epsilon_min
|
34
|
+
else:
|
35
|
+
min_epsilon = max(dx, dy)
|
36
|
+
|
37
|
+
# Log-spaced epsilon values (outside JIT)
|
38
|
+
epsilons = np.logspace(np.log10(min_epsilon), np.log10(epsilon_max), n_eps)
|
39
|
+
|
40
|
+
return epsilons, _uncertainty_fraction_core(basin, dx, dy, epsilons)
|
41
|
+
|
42
|
+
|
43
|
+
@njit(cache=True, parallel=True)
|
44
|
+
def _uncertainty_fraction_core(basin, dx, dy, epsilons):
|
45
|
+
"""
|
46
|
+
Numba-compatible core function for computing uncertainty fractions.
|
47
|
+
"""
|
48
|
+
nx, ny = basin.shape
|
49
|
+
f_epsilons = np.zeros(len(epsilons))
|
50
|
+
|
51
|
+
for k in prange(len(epsilons)):
|
52
|
+
eps = epsilons[k]
|
53
|
+
dx_pix = int(round(eps / dx))
|
54
|
+
dy_pix = int(round(eps / dy))
|
55
|
+
|
56
|
+
if dx_pix < 1 or dy_pix < 1:
|
57
|
+
f_epsilons[k] = 0.0
|
58
|
+
continue
|
59
|
+
|
60
|
+
uncertain = 0
|
61
|
+
total = 0
|
62
|
+
|
63
|
+
for i in range(dx_pix, nx - dx_pix):
|
64
|
+
for j in range(dy_pix, ny - dy_pix):
|
65
|
+
center = basin[i, j]
|
66
|
+
neighbors = (
|
67
|
+
basin[i + dx_pix, j],
|
68
|
+
basin[i - dx_pix, j],
|
69
|
+
basin[i, j + dy_pix],
|
70
|
+
basin[i, j - dy_pix],
|
71
|
+
)
|
72
|
+
for nb in neighbors:
|
73
|
+
if nb != center:
|
74
|
+
uncertain += 1
|
75
|
+
break # once marked uncertain, no need to check other neighbors
|
76
|
+
total += 1
|
77
|
+
|
78
|
+
if total > 0:
|
79
|
+
f_epsilons[k] = uncertain / total
|
80
|
+
|
81
|
+
return f_epsilons
|
82
|
+
|
83
|
+
|
84
|
+
def basin_entropy(
|
85
|
+
basin: NDArray[np.float64], n: int, log_base: float = np.e
|
86
|
+
) -> Tuple[float, float]:
|
87
|
+
"""
|
88
|
+
Calculate the basin entropy (Sb) and boundary basin entropy (Sbb) of a 2D attraction basin.
|
89
|
+
|
90
|
+
The basin entropy quantifies the uncertainty in final state prediction, while the boundary
|
91
|
+
entropy specifically measures uncertainty at basin boundaries where multiple attractors coexist.
|
92
|
+
|
93
|
+
Parameters
|
94
|
+
----------
|
95
|
+
basin : NDArray[np.float64]
|
96
|
+
2D array representing the basin of attraction, where each element indicates
|
97
|
+
the final state (attractor) for that initial condition (shape: (Nx, Ny)).
|
98
|
+
n : int
|
99
|
+
Default size of square sub-boxes for partitioning (must be positive).
|
100
|
+
log : {'2', 'e', '10'} or Callable, optional
|
101
|
+
Logarithm base for entropy calculation:
|
102
|
+
- '2' : bits (default)
|
103
|
+
- 'e' : nats
|
104
|
+
- '10' : hartleys
|
105
|
+
Alternatively, a custom log function can be provided.
|
106
|
+
|
107
|
+
Returns
|
108
|
+
-------
|
109
|
+
Sb : float
|
110
|
+
Average entropy across all sub-boxes (basin entropy).
|
111
|
+
Sbb : float
|
112
|
+
Average entropy across boundary sub-boxes (boundary basin entropy).
|
113
|
+
|
114
|
+
Raises
|
115
|
+
------
|
116
|
+
ValueError
|
117
|
+
- If `basin` is not 2D
|
118
|
+
- If `n` ≤ 0
|
119
|
+
- If invalid `log` specification
|
120
|
+
|
121
|
+
Notes
|
122
|
+
-----
|
123
|
+
- **Entropy Calculation**:
|
124
|
+
For each sub-box: S = -Σ(p_i * log(p_i)), where p_i is the probability of state i.
|
125
|
+
- **Boundary Detection**:
|
126
|
+
Sub-boxes with >1 unique state are considered boundaries.
|
127
|
+
- **Performance**:
|
128
|
+
Uses vectorized operations where possible for efficiency.
|
129
|
+
|
130
|
+
Examples
|
131
|
+
--------
|
132
|
+
>>> basin = np.random.randint(0, 2, (100, 100))
|
133
|
+
>>> Sb, Sbb = boundary_entropy(basin, n=10, log='2')
|
134
|
+
>>> print(f"Basin entropy: {Sb:.3f}, Boundary entropy: {Sbb:.3f}")
|
135
|
+
"""
|
136
|
+
|
137
|
+
Nx, Ny = basin.shape
|
138
|
+
|
139
|
+
if Nx % n != 0 or Ny % n != 0:
|
140
|
+
raise ValueError(
|
141
|
+
f"Sub-box sizes ({n}, {n}) must divide basin dimensions ({Nx}, {Ny})"
|
142
|
+
)
|
143
|
+
|
144
|
+
# Initialize
|
145
|
+
Mx = Nx // n
|
146
|
+
My = Ny // n
|
147
|
+
S = np.zeros((Mx, My))
|
148
|
+
boundary_mask = np.zeros((Mx, My), dtype=bool)
|
149
|
+
|
150
|
+
# Process each sub-box
|
151
|
+
for i in range(Mx):
|
152
|
+
for j in range(My):
|
153
|
+
# Extract sub-box
|
154
|
+
box = basin[i * n : (i + 1) * n, j * n : (j + 1) * n]
|
155
|
+
unique_states, counts = np.unique(box, return_counts=True)
|
156
|
+
num_unique = len(unique_states)
|
157
|
+
|
158
|
+
# Mark boundary boxes
|
159
|
+
if num_unique > 1:
|
160
|
+
boundary_mask[i, j] = True
|
161
|
+
|
162
|
+
# Calculate entropy
|
163
|
+
probs = counts / counts.sum()
|
164
|
+
S[i, j] = -np.sum(probs * np.log(probs) / np.log(log_base))
|
165
|
+
|
166
|
+
# Compute averages
|
167
|
+
Sb = np.mean(S)
|
168
|
+
Sbb = np.mean(S[boundary_mask]) if boundary_mask.any() else 0.0
|
169
|
+
|
170
|
+
return float(Sb), float(Sbb)
|
@@ -0,0 +1,426 @@
|
|
1
|
+
# recurrence_quantification_analysis.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 numba import njit
|
20
|
+
from dataclasses import dataclass
|
21
|
+
from typing import Literal
|
22
|
+
from numpy.typing import NDArray
|
23
|
+
|
24
|
+
|
25
|
+
@dataclass
|
26
|
+
class RTEConfig:
|
27
|
+
"""
|
28
|
+
Configuration class for Recurrence Time Entropy (RTE) analysis.
|
29
|
+
|
30
|
+
Attributes
|
31
|
+
----------
|
32
|
+
metric : {'supremum', 'euclidean', 'manhattan'}, default='supremum'
|
33
|
+
Distance metric used for phase space reconstruction.
|
34
|
+
std_metric : {'supremum', 'euclidean', 'manhattan'}, default='supremum'
|
35
|
+
Distance metric used for standard deviation calculation.
|
36
|
+
lmin : int, default=1
|
37
|
+
Minimum line length to consider in recurrence quantification.
|
38
|
+
threshold : float, default=0.1
|
39
|
+
Recurrence threshold (relative to data range).
|
40
|
+
threshold_std : bool, default=True
|
41
|
+
Whether to scale threshold by data standard deviation.
|
42
|
+
return_final_state : bool, default=False
|
43
|
+
Whether to return the final system state in results.
|
44
|
+
return_recmat : bool, default=False
|
45
|
+
Whether to return the recurrence matrix.
|
46
|
+
return_p : bool, default=False
|
47
|
+
Whether to return white vertical line length distribution.
|
48
|
+
|
49
|
+
Notes
|
50
|
+
-----
|
51
|
+
- The 'supremum' metric (default) is computationally efficient and often sufficient for RTE.
|
52
|
+
- Typical threshold values range from 0.05 to 0.3 depending on data noise levels.
|
53
|
+
- Set lmin=2 to exclude single-point recurrences from analysis.
|
54
|
+
"""
|
55
|
+
|
56
|
+
metric: Literal["supremum", "euclidean", "manhattan"] = "supremum"
|
57
|
+
std_metric: Literal["supremum", "euclidean", "manhattan"] = "supremum"
|
58
|
+
lmin: int = 1
|
59
|
+
threshold: float = 0.1
|
60
|
+
threshold_std: bool = True
|
61
|
+
return_final_state: bool = False
|
62
|
+
return_recmat: bool = False
|
63
|
+
return_p: bool = False
|
64
|
+
|
65
|
+
def __post_init__(self):
|
66
|
+
"""Validate configuration parameters."""
|
67
|
+
if self.lmin < 1:
|
68
|
+
raise ValueError("lmin must be ≥ 1")
|
69
|
+
|
70
|
+
if not isinstance(self.lmin, int):
|
71
|
+
raise TypeError("lmin must be an integer")
|
72
|
+
|
73
|
+
if not isinstance(self.threshold, float):
|
74
|
+
raise TypeError("threshold must be a float")
|
75
|
+
|
76
|
+
if not 0 < self.threshold < 1:
|
77
|
+
raise ValueError("threshold must be in (0, 1)")
|
78
|
+
|
79
|
+
if not isinstance(self.std_metric, str):
|
80
|
+
raise TypeError("std_metric must be a string")
|
81
|
+
|
82
|
+
if not isinstance(self.metric, str):
|
83
|
+
raise TypeError("metric must be a string")
|
84
|
+
|
85
|
+
if self.std_metric not in {"supremum", "euclidean", "manhattan"}:
|
86
|
+
raise ValueError(
|
87
|
+
"std_metric must be 'supremum', 'euclidean' or 'manhattan'"
|
88
|
+
)
|
89
|
+
|
90
|
+
if self.metric not in {"supremum", "euclidean", "manhattan"}:
|
91
|
+
raise ValueError("metric must be 'supremum', 'euclidean' or 'manhattan'")
|
92
|
+
|
93
|
+
|
94
|
+
@njit(cache=True)
|
95
|
+
def _recurrence_matrix(
|
96
|
+
arr: NDArray[np.float64], threshold: float, metric_id: int
|
97
|
+
) -> NDArray[np.uint8]:
|
98
|
+
"""
|
99
|
+
Compute the binary recurrence matrix of a time series using a specified norm.
|
100
|
+
|
101
|
+
Parameters
|
102
|
+
----------
|
103
|
+
arr : NDarray of shape (N, d)
|
104
|
+
The input time series or phase-space trajectory, where N is the number of time points
|
105
|
+
and d is the embedding dimension (or feature dimension).
|
106
|
+
|
107
|
+
threshold : float
|
108
|
+
Distance threshold for determining recurrence. A recurrence is detected
|
109
|
+
when the distance between two points is less than this threshold.
|
110
|
+
|
111
|
+
metric_id : int
|
112
|
+
Identifier for the norm to be used:
|
113
|
+
- 0: Supremum (infinity) norm
|
114
|
+
- 1: Euclidean (L2) norm
|
115
|
+
- 2: Manhattan (L1) norm
|
116
|
+
|
117
|
+
Returns
|
118
|
+
-------
|
119
|
+
recmat : NDarray of shape (N, N), dtype=np.uint8
|
120
|
+
Binary recurrence matrix where 1 indicates recurrence and 0 indicates no recurrence.
|
121
|
+
"""
|
122
|
+
N, d = arr.shape
|
123
|
+
recmat = np.zeros((N, N), dtype=np.uint8)
|
124
|
+
|
125
|
+
for i in range(N):
|
126
|
+
for j in range(i, N):
|
127
|
+
if metric_id == 0: # Supremum norm
|
128
|
+
max_diff = 0.0
|
129
|
+
for k in range(d):
|
130
|
+
diff = abs(arr[i, k] - arr[j, k])
|
131
|
+
if diff > max_diff:
|
132
|
+
max_diff = diff
|
133
|
+
dist = max_diff
|
134
|
+
elif metric_id == 1: # Manhattan norm
|
135
|
+
sum_abs = 0.0
|
136
|
+
for k in range(d):
|
137
|
+
sum_abs += abs(arr[i, k] - arr[j, k])
|
138
|
+
dist = sum_abs
|
139
|
+
elif metric_id == 2: # Euclidean norm
|
140
|
+
sq_sum = 0.0
|
141
|
+
for k in range(d):
|
142
|
+
diff = arr[i, k] - arr[j, k]
|
143
|
+
sq_sum += diff * diff
|
144
|
+
dist = np.sqrt(sq_sum)
|
145
|
+
else:
|
146
|
+
# Fallback: shouldn't happen
|
147
|
+
dist = 0.0
|
148
|
+
|
149
|
+
if dist < threshold:
|
150
|
+
recmat[i, j] = 1
|
151
|
+
recmat[j, i] = 1 # enforce symmetry
|
152
|
+
|
153
|
+
return recmat
|
154
|
+
|
155
|
+
|
156
|
+
def recurrence_matrix(
|
157
|
+
arr: NDArray[np.float64], threshold: float, metric: str = "supremum"
|
158
|
+
) -> NDArray[np.uint8]:
|
159
|
+
"""
|
160
|
+
Compute the recurrence matrix of a univariate or multivariate time series.
|
161
|
+
|
162
|
+
Parameters
|
163
|
+
----------
|
164
|
+
u : NDArray
|
165
|
+
Time series data. Can be 1D (shape: (N,)) or 2D (shape: (N, d)).
|
166
|
+
If 1D, the array is reshaped to (N, 1) automatically.
|
167
|
+
|
168
|
+
threshold : float
|
169
|
+
Distance threshold for recurrence. A recurrence is detected when the
|
170
|
+
distance between two points is less than this threshold.
|
171
|
+
|
172
|
+
metric : str, optional, default="supremum"
|
173
|
+
Distance metric to use. Supported values are:
|
174
|
+
- "supremum" : infinity norm (L-infinity)
|
175
|
+
- "euclidean" : L2 norm
|
176
|
+
- "manhattan" : L1 norm
|
177
|
+
|
178
|
+
Returns
|
179
|
+
-------
|
180
|
+
recmat : NDArray of shape (N, N), dtype=np.uint8
|
181
|
+
Binary recurrence matrix indicating whether each pair of points
|
182
|
+
are within the threshold distance.
|
183
|
+
|
184
|
+
Raises
|
185
|
+
------
|
186
|
+
ValueError
|
187
|
+
If the specified metric is invalid.
|
188
|
+
"""
|
189
|
+
metrics = {"supremum": 0, "euclidean": 1, "manhattan": 2}
|
190
|
+
if metric not in metrics:
|
191
|
+
raise ValueError("Metric must be 'supremum', 'euclidean', or 'manhattan'")
|
192
|
+
metric_id = metrics[metric]
|
193
|
+
|
194
|
+
if threshold <= 0:
|
195
|
+
print(threshold)
|
196
|
+
raise ValueError("Threshold must be positive")
|
197
|
+
|
198
|
+
if not isinstance(arr, np.ndarray):
|
199
|
+
raise TypeError("Input 'arr' must be a NumPy array")
|
200
|
+
if arr.ndim not in (1, 2):
|
201
|
+
raise ValueError("Input 'arr' must be 1D or 2D array")
|
202
|
+
|
203
|
+
arr = np.atleast_2d(arr).astype(np.float64)
|
204
|
+
if arr.shape[0] == 1:
|
205
|
+
arr = arr.T
|
206
|
+
|
207
|
+
return _recurrence_matrix(arr, threshold, metric_id)
|
208
|
+
|
209
|
+
|
210
|
+
@njit
|
211
|
+
def white_vertline_distr(recmat: NDArray[np.uint8]) -> NDArray[np.float64]:
|
212
|
+
"""
|
213
|
+
Calculate the distribution of white vertical line lengths in a binary recurrence matrix.
|
214
|
+
|
215
|
+
This function counts occurrences of consecutive vertical white (0) pixels, excluding
|
216
|
+
lines touching the matrix borders, as defined in recurrence quantification analysis.
|
217
|
+
|
218
|
+
Parameters
|
219
|
+
----------
|
220
|
+
recmat : NDArray[np.uint8]
|
221
|
+
A 2D binary matrix (0s and 1s) representing a recurrence matrix.
|
222
|
+
Expected shape: (N, N) where N is the matrix dimension.
|
223
|
+
|
224
|
+
Returns
|
225
|
+
-------
|
226
|
+
NDArray[np.float64]
|
227
|
+
Array where index represents line length and value represents count.
|
228
|
+
(Note: Index 0 is unused since minimum line length is 1)
|
229
|
+
|
230
|
+
Raises
|
231
|
+
------
|
232
|
+
ValueError
|
233
|
+
If input is not 2D or not square.
|
234
|
+
|
235
|
+
Notes
|
236
|
+
-----
|
237
|
+
- Border lines (touching matrix edges) are excluded from counts [1]
|
238
|
+
- Complexity: O(N^2) for N x N matrix
|
239
|
+
- Optimized with Numba's @njit decorator for performance
|
240
|
+
|
241
|
+
References
|
242
|
+
----------
|
243
|
+
[1] K. H. Kraemer & N. Marwan, "Border effect corrections for diagonal line based
|
244
|
+
recurrence quantification analysis measures", Physics Letters A 383, 125977 (2019)
|
245
|
+
"""
|
246
|
+
# Input validation
|
247
|
+
if recmat.ndim != 2 or recmat.shape[0] != recmat.shape[1]:
|
248
|
+
raise ValueError("Input must be a square 2D array")
|
249
|
+
|
250
|
+
N = recmat.shape[0]
|
251
|
+
P = np.zeros(N + 1) # Index 0 unused, max possible length is N
|
252
|
+
|
253
|
+
for i in range(N):
|
254
|
+
current_length = 0
|
255
|
+
border_flag = False # Tracks if we're in a border region
|
256
|
+
|
257
|
+
for j in range(N):
|
258
|
+
if recmat[i, j] == 0:
|
259
|
+
if border_flag: # Only count after first black pixel
|
260
|
+
current_length += 1
|
261
|
+
else:
|
262
|
+
border_flag = True # Mark that we've passed the border
|
263
|
+
if current_length > 0:
|
264
|
+
P[current_length] += 1
|
265
|
+
current_length = 0
|
266
|
+
|
267
|
+
# Handle line continuing to matrix edge
|
268
|
+
if current_length > 0 and border_flag:
|
269
|
+
P[current_length] += 1
|
270
|
+
|
271
|
+
P = P[1:] # Exclude unused 0 index
|
272
|
+
|
273
|
+
return P
|
274
|
+
|
275
|
+
|
276
|
+
# def RTE(
|
277
|
+
# u: NDArray[np.float64],
|
278
|
+
# parameters: NDArray[np.float64],
|
279
|
+
# total_time: int,
|
280
|
+
# mapping: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
281
|
+
# transient_time: Optional[int] = None,
|
282
|
+
# **kwargs
|
283
|
+
# ) -> Union[float, Tuple]:
|
284
|
+
# """
|
285
|
+
# Calculate Recurrence Time Entropy (RTE) for a dynamical system.
|
286
|
+
|
287
|
+
# RTE quantifies the complexity of a system by analyzing the distribution
|
288
|
+
# of white vertical lines, i.e., the gap between two diagonal lines.
|
289
|
+
# Higher entropy indicates more complex dynamics.
|
290
|
+
|
291
|
+
# Parameters
|
292
|
+
# ----------
|
293
|
+
# u : NDArray[np.float64]
|
294
|
+
# Initial state vector (shape: (neq,))
|
295
|
+
# parameters : NDArray[np.float64]
|
296
|
+
# System parameters passed to mapping function
|
297
|
+
# total_time : int
|
298
|
+
# Number of iterations to simulate
|
299
|
+
# mapping : Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]]
|
300
|
+
# System evolution function: u_next = mapping(u, parameters)
|
301
|
+
# transient_time : Optional[int], default=None
|
302
|
+
# Time to wait before starting RTE calculation.
|
303
|
+
# **kwargs
|
304
|
+
# Configuration parameters (see RTEConfig)
|
305
|
+
|
306
|
+
# Returns
|
307
|
+
# -------
|
308
|
+
# Union[float, Tuple]
|
309
|
+
# - Base case: RTE value (float)
|
310
|
+
# - With optional returns: List containing [RTE, *requested_additional_data]
|
311
|
+
|
312
|
+
# Raises
|
313
|
+
# ------
|
314
|
+
# ValueError
|
315
|
+
# - If invalid metric specified
|
316
|
+
# - If trajectory generation fails
|
317
|
+
|
318
|
+
# Notes
|
319
|
+
# -----
|
320
|
+
# - Implements the method described in [1]
|
321
|
+
# - For optimal results:
|
322
|
+
# - Use total_time > 1000 for reliable statistics
|
323
|
+
# - Typical threshold values: 0.05-0.3
|
324
|
+
# - Set lmin=1 to include single-point recurrences
|
325
|
+
|
326
|
+
# References
|
327
|
+
# ----------
|
328
|
+
# [1] M. R. Sales, M. Mugnaine, J. Szezech, José D., R. L. Viana, I. L. Caldas, N. Marwan, and J. Kurths, Stickiness and recurrence plots: An entropy-based approach, Chaos: An Interdisciplinary Journal of Nonlinear Science 33, 033140 (2023)
|
329
|
+
# """
|
330
|
+
|
331
|
+
# u = u.copy()
|
332
|
+
|
333
|
+
# # Configuration handling
|
334
|
+
# config = RTEConfig(**kwargs)
|
335
|
+
|
336
|
+
# # Metric setup
|
337
|
+
# metric_map = {
|
338
|
+
# "supremum": np.inf,
|
339
|
+
# "euclidean": 2,
|
340
|
+
# "manhattan": 1
|
341
|
+
# }
|
342
|
+
|
343
|
+
# try:
|
344
|
+
# ord = metric_map[config.std_metric.lower()]
|
345
|
+
# except KeyError:
|
346
|
+
# raise ValueError(
|
347
|
+
# f"Invalid std_metric: {config.std_metric}. Must be {list(metric_map.keys())}")
|
348
|
+
|
349
|
+
# if transient_time is not None:
|
350
|
+
# u = iterate_mapping(u, parameters, transient_time, mapping)
|
351
|
+
# total_time -= transient_time
|
352
|
+
|
353
|
+
# # Generate trajectory
|
354
|
+
# try:
|
355
|
+
# time_series = generate_trajectory(u, parameters, total_time, mapping)
|
356
|
+
# except Exception as e:
|
357
|
+
# raise ValueError(f"Trajectory generation failed: {str(e)}")
|
358
|
+
|
359
|
+
# # Threshold calculation
|
360
|
+
# if config.threshold_std:
|
361
|
+
# std = np.std(time_series, axis=0)
|
362
|
+
# eps = config.threshold * np.linalg.norm(std, ord=ord)
|
363
|
+
# if eps <= 0:
|
364
|
+
# eps = 0.1
|
365
|
+
# else:
|
366
|
+
# eps = config.threshold
|
367
|
+
|
368
|
+
# # Recurrence matrix calculation
|
369
|
+
# recmat = recurrence_matrix(time_series, float(eps), metric=config.metric)
|
370
|
+
|
371
|
+
# # White line distribution
|
372
|
+
# P = white_vertline_distr(recmat)[config.lmin:]
|
373
|
+
# P = P[P > 0] # Remove zeros
|
374
|
+
# P /= P.sum() # Normalize
|
375
|
+
|
376
|
+
# # Entropy calculation
|
377
|
+
# rte = -np.sum(P * np.log(P))
|
378
|
+
|
379
|
+
# # Prepare output
|
380
|
+
# result = [rte]
|
381
|
+
# if config.return_final_state:
|
382
|
+
# result.append(time_series[-1])
|
383
|
+
# if config.return_recmat:
|
384
|
+
# result.append(recmat)
|
385
|
+
# if config.return_p:
|
386
|
+
# result.append(P)
|
387
|
+
|
388
|
+
# return result[0] if len(result) == 1 else tuple(result)
|
389
|
+
|
390
|
+
|
391
|
+
# def finite_time_RTE(
|
392
|
+
# u: NDArray[np.float64],
|
393
|
+
# parameters: NDArray[np.float64],
|
394
|
+
# total_time: int,
|
395
|
+
# finite_time: int,
|
396
|
+
# mapping: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
397
|
+
# return_points: bool = False,
|
398
|
+
# **kwargs
|
399
|
+
# ) -> Union[NDArray[np.float64], Tuple[NDArray[np.float64], NDArray[np.float64]]]:
|
400
|
+
# # Validate window size
|
401
|
+
# if finite_time > total_time:
|
402
|
+
# raise ValueError(
|
403
|
+
# f"finite_time ({finite_time}) exceeds available samples ({total_time})")
|
404
|
+
|
405
|
+
# num_windows = total_time // finite_time
|
406
|
+
# RTE_values = np.zeros(num_windows)
|
407
|
+
# phase_space_points = np.zeros((num_windows, u.shape[0]))
|
408
|
+
|
409
|
+
# for i in range(num_windows):
|
410
|
+
# result = RTE(
|
411
|
+
# u,
|
412
|
+
# parameters,
|
413
|
+
# finite_time,
|
414
|
+
# mapping,
|
415
|
+
# return_final_state=True,
|
416
|
+
# **kwargs
|
417
|
+
# )
|
418
|
+
# if isinstance(result, tuple):
|
419
|
+
# RTE_values[i], u_new = result
|
420
|
+
# phase_space_points[i] = u
|
421
|
+
# u = u_new.copy()
|
422
|
+
|
423
|
+
# if return_points:
|
424
|
+
# return RTE_values, phase_space_points
|
425
|
+
# else:
|
426
|
+
# return RTE_values
|