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,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