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,3391 @@
|
|
1
|
+
# discrete_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
|
+
|
19
|
+
import numpy as np
|
20
|
+
from numbers import Integral, Real
|
21
|
+
from typing import Optional, Tuple, Union, Callable, List, Dict, Sequence, Any
|
22
|
+
from numpy.typing import NDArray
|
23
|
+
|
24
|
+
from pynamicalsys.common.recurrence_quantification_analysis import RTEConfig
|
25
|
+
from pynamicalsys.discrete_time.dynamical_indicators import (
|
26
|
+
lyapunov_er,
|
27
|
+
lyapunov_qr,
|
28
|
+
finite_time_lyapunov,
|
29
|
+
lyapunov_1D,
|
30
|
+
SALI,
|
31
|
+
LDI_k,
|
32
|
+
lagrangian_descriptors,
|
33
|
+
dig,
|
34
|
+
hurst_exponent,
|
35
|
+
finite_time_hurst_exponent,
|
36
|
+
RTE,
|
37
|
+
finite_time_RTE,
|
38
|
+
)
|
39
|
+
from pynamicalsys.discrete_time.models import (
|
40
|
+
standard_map,
|
41
|
+
standard_map_backwards,
|
42
|
+
standard_map_jacobian,
|
43
|
+
unbounded_standard_map,
|
44
|
+
henon_map,
|
45
|
+
henon_map_jacobian,
|
46
|
+
logistic_map,
|
47
|
+
logistic_map_jacobian,
|
48
|
+
standard_nontwist_map,
|
49
|
+
standard_nontwist_map_backwards,
|
50
|
+
standard_nontwist_map_jacobian,
|
51
|
+
extended_standard_nontwist_map,
|
52
|
+
extended_standard_nontwist_map_backwards,
|
53
|
+
extended_standard_nontwist_map_jacobian,
|
54
|
+
leonel_map,
|
55
|
+
leonel_map_jacobian,
|
56
|
+
leonel_map_backwards,
|
57
|
+
symplectic_map_4D,
|
58
|
+
symplectic_map_4D_backwards,
|
59
|
+
symplectic_map_4D_jacobian,
|
60
|
+
lozi_map,
|
61
|
+
lozi_map_jacobian,
|
62
|
+
rulkov_map,
|
63
|
+
rulkov_map_jacobian,
|
64
|
+
)
|
65
|
+
from pynamicalsys.discrete_time.trajectory_analysis import (
|
66
|
+
generate_trajectory,
|
67
|
+
ensemble_trajectories,
|
68
|
+
bifurcation_diagram,
|
69
|
+
period_counter,
|
70
|
+
escape_basin_and_time_entering,
|
71
|
+
escape_time_exiting,
|
72
|
+
survival_probability,
|
73
|
+
find_periodic_orbit,
|
74
|
+
find_periodic_orbit_symmetry_line,
|
75
|
+
eigenvalues_and_eigenvectors,
|
76
|
+
classify_stability,
|
77
|
+
calculate_manifolds,
|
78
|
+
rotation_number,
|
79
|
+
iterate_mapping,
|
80
|
+
ensemble_time_average,
|
81
|
+
)
|
82
|
+
from pynamicalsys.discrete_time.transport import (
|
83
|
+
diffusion_coefficient,
|
84
|
+
average_vs_time,
|
85
|
+
root_mean_squared,
|
86
|
+
mean_squared_displacement,
|
87
|
+
recurrence_times,
|
88
|
+
cumulative_average_vs_time,
|
89
|
+
)
|
90
|
+
|
91
|
+
from pynamicalsys.common.utils import finite_difference_jacobian, householder_qr
|
92
|
+
|
93
|
+
from .time_series_metrics import TimeSeriesMetrics as tsm
|
94
|
+
|
95
|
+
from pynamicalsys.discrete_time.validators import (
|
96
|
+
validate_initial_conditions,
|
97
|
+
validate_parameters,
|
98
|
+
validate_non_negative,
|
99
|
+
validate_transient_time,
|
100
|
+
validate_and_convert_param_range,
|
101
|
+
validate_positive,
|
102
|
+
validate_sample_times,
|
103
|
+
validate_axis,
|
104
|
+
validate_finite_time,
|
105
|
+
)
|
106
|
+
|
107
|
+
|
108
|
+
class DiscreteDynamicalSystem:
|
109
|
+
"""Class representing a discrete dynamical system with various models and methods for analysis.
|
110
|
+
|
111
|
+
This class allows users to work with predefined dynamical models or custom mappings,
|
112
|
+
compute trajectories, bifurcation diagrams, periods, and perform various dynamical analyses.
|
113
|
+
It supports both single initial conditions and ensembles of initial conditions, providing
|
114
|
+
methods for generating trajectories, computing bifurcation diagrams, and analyzing stability.
|
115
|
+
|
116
|
+
Parameters
|
117
|
+
----------
|
118
|
+
model : str, optional
|
119
|
+
Name of the predefined model to use (e.g., "henon map"). If provided, overrides custom mappings.
|
120
|
+
mapping : callable, optional
|
121
|
+
Custom mapping function with signature f(u, parameters) -> array_like.
|
122
|
+
If provided, model must be None.
|
123
|
+
jacobian : callable, optional
|
124
|
+
Custom Jacobian function with signature J(u, parameters, *args) -> array_like.
|
125
|
+
If provided, must be compatible with the mapping function.
|
126
|
+
backwards_mapping : callable, optional
|
127
|
+
Custom inverse mapping function with signature f_inv(u, parameters) -> array_like.
|
128
|
+
If provided, must be compatible with the mapping function.
|
129
|
+
system_dimension : int, optional
|
130
|
+
Dimension of the system (number of variables in the mapping).
|
131
|
+
Required if using custom mappings without a predefined model.
|
132
|
+
|
133
|
+
Raises
|
134
|
+
------
|
135
|
+
ValueError
|
136
|
+
- If neither model nor mapping is provided, or if provided model name is not implemented.
|
137
|
+
- If mapping is provided without jacobian for models requiring it.
|
138
|
+
|
139
|
+
TypeError
|
140
|
+
- If provided mapping or jacobian is not callable.
|
141
|
+
|
142
|
+
Notes
|
143
|
+
-----
|
144
|
+
- When providing custom functions, either provide both mapping and jacobian,
|
145
|
+
or just mapping (in which case finite differences will be used for Jacobian)
|
146
|
+
- When providing custom functions, the mapping function signature should be f(u, parameters) -> NDArray[np.float64]
|
147
|
+
- The class supports various predefined models such as the standard map, Hénon map, logistic map, and others.
|
148
|
+
- The available models can be queried using the `available_models` class method.
|
149
|
+
|
150
|
+
Examples
|
151
|
+
--------
|
152
|
+
>>> # Using predefined model
|
153
|
+
>>> system = DiscreteDynamicalSystem(model="henon map")
|
154
|
+
>>> # Using custom mappings
|
155
|
+
>>> def my_map(u, parameters):
|
156
|
+
... return np.array([u[0] + parameters[0] * u[1], u[1] - parameters[1] * u[0]])
|
157
|
+
>>> def my_jacobian(u, parameters):
|
158
|
+
... return np.array([[1, parameters[0]], [-parameters[1], 1]])
|
159
|
+
>>> system = DiscreteDynamicalSystem(
|
160
|
+
mapping=my_map,
|
161
|
+
jacobian=my_jacobian,
|
162
|
+
system_dimension=2,
|
163
|
+
number_of_parameters=2
|
164
|
+
)
|
165
|
+
"""
|
166
|
+
|
167
|
+
# Class-level constant defining all available models
|
168
|
+
__AVAILABLE_MODELS: Dict[str, Dict[str, Any]] = {
|
169
|
+
"standard map": {
|
170
|
+
"description": "Standard Chirikov-Taylor map (area-preserving 2D)",
|
171
|
+
"has_jacobian": True,
|
172
|
+
"has_backwards_map": True,
|
173
|
+
"mapping": standard_map,
|
174
|
+
"jacobian": standard_map_jacobian,
|
175
|
+
"backwards_mapping": standard_map_backwards,
|
176
|
+
"dimension": 2,
|
177
|
+
"number_of_parameters": 1,
|
178
|
+
"parameters": ["k"],
|
179
|
+
},
|
180
|
+
"unbounded standard map": {
|
181
|
+
"description": "Standard Chirikov-Taylor map withou boundaries on the y varibles. Useful to study diffusion",
|
182
|
+
"has_jacobian": False,
|
183
|
+
"has_backwards_map": False,
|
184
|
+
"mapping": unbounded_standard_map,
|
185
|
+
"jacobian": None,
|
186
|
+
"backwards_mapping": None,
|
187
|
+
"dimension": 2,
|
188
|
+
"number_of_parameters": 1,
|
189
|
+
"parameters": ["k"],
|
190
|
+
},
|
191
|
+
"henon map": {
|
192
|
+
"description": "Hénon quadratic map",
|
193
|
+
"has_jacobian": True,
|
194
|
+
"has_backwards_map": False,
|
195
|
+
"mapping": henon_map,
|
196
|
+
"jacobian": henon_map_jacobian,
|
197
|
+
"backwards_mapping": None,
|
198
|
+
"dimension": 2,
|
199
|
+
"number_of_parameters": 2,
|
200
|
+
"parameters": ["a", "b"],
|
201
|
+
},
|
202
|
+
"lozi map": {
|
203
|
+
"description": "Lozi map",
|
204
|
+
"has_jacobian": True,
|
205
|
+
"has_backwards_map": False,
|
206
|
+
"mapping": lozi_map,
|
207
|
+
"jacobian": lozi_map_jacobian,
|
208
|
+
"backwards_mapping": None,
|
209
|
+
"dimension": 2,
|
210
|
+
"number_of_parameters": 2,
|
211
|
+
"parameters": ["a", "b"],
|
212
|
+
},
|
213
|
+
"rulkov map": {
|
214
|
+
"description": "Rulkov map",
|
215
|
+
"has_jacobian": True,
|
216
|
+
"has_backwards_map": False,
|
217
|
+
"mapping": rulkov_map,
|
218
|
+
"jacobian": rulkov_map_jacobian,
|
219
|
+
"backwards_mapping": None,
|
220
|
+
"dimension": 2,
|
221
|
+
"number_of_parameters": 3,
|
222
|
+
"parameters": ["alpha", "sigma", "mu"],
|
223
|
+
},
|
224
|
+
"logistic map": {
|
225
|
+
"description": "Logistic map (1D nonlinear system)",
|
226
|
+
"has_jacobian": True,
|
227
|
+
"has_backwards_map": False,
|
228
|
+
"mapping": logistic_map,
|
229
|
+
"jacobian": logistic_map_jacobian,
|
230
|
+
"backwards_mapping": None,
|
231
|
+
"dimension": 1,
|
232
|
+
"number_of_parameters": 1,
|
233
|
+
"parameters": ["r"],
|
234
|
+
},
|
235
|
+
"standard nontwist map": {
|
236
|
+
"description": "Standard nontwist map (area-preserving but violates twist condition)",
|
237
|
+
"has_jacobian": True,
|
238
|
+
"has_backwards_map": True,
|
239
|
+
"mapping": standard_nontwist_map,
|
240
|
+
"jacobian": standard_nontwist_map_jacobian,
|
241
|
+
"backwards_mapping": standard_nontwist_map_backwards,
|
242
|
+
"dimension": 2,
|
243
|
+
"number_of_parameters": 2,
|
244
|
+
"parameters": ["a", "b"],
|
245
|
+
},
|
246
|
+
"extended standard nontwist map": {
|
247
|
+
"description": "Extended version of standard nontwist map",
|
248
|
+
"has_jacobian": True,
|
249
|
+
"has_backwards_map": True,
|
250
|
+
"mapping": extended_standard_nontwist_map,
|
251
|
+
"jacobian": extended_standard_nontwist_map_jacobian,
|
252
|
+
"backwards_mapping": extended_standard_nontwist_map_backwards,
|
253
|
+
"dimension": 2,
|
254
|
+
"number_of_parameters": 4,
|
255
|
+
"parameters": ["a", "b", "c", "m"],
|
256
|
+
},
|
257
|
+
"leonel map": {
|
258
|
+
"description": "Leonel's map model",
|
259
|
+
"has_jacobian": True,
|
260
|
+
"has_backwards_map": True,
|
261
|
+
"mapping": leonel_map,
|
262
|
+
"jacobian": leonel_map_jacobian,
|
263
|
+
"backwards_mapping": leonel_map_backwards,
|
264
|
+
"dimension": 2,
|
265
|
+
"number_of_parameters": 2,
|
266
|
+
"parameters": ["eps", "gamma"],
|
267
|
+
},
|
268
|
+
"4d symplectic map": {
|
269
|
+
"description": "4D symplectic map: two coupled standard maps",
|
270
|
+
"has_jacobian": True,
|
271
|
+
"has_backwards_map": True,
|
272
|
+
"mapping": symplectic_map_4D,
|
273
|
+
"jacobian": symplectic_map_4D_jacobian,
|
274
|
+
"backwards_mapping": symplectic_map_4D_backwards,
|
275
|
+
"dimension": 4,
|
276
|
+
"number_of_parameters": 3,
|
277
|
+
"parameters": ["eps1", "eps2", "xi"],
|
278
|
+
},
|
279
|
+
}
|
280
|
+
|
281
|
+
def __init__(
|
282
|
+
self,
|
283
|
+
model: Optional[str] = None,
|
284
|
+
mapping: Optional[Callable] = None,
|
285
|
+
jacobian: Optional[Callable] = None,
|
286
|
+
backwards_mapping: Optional[Callable] = None,
|
287
|
+
system_dimension: Optional[int] = None,
|
288
|
+
number_of_parameters: Optional[int] = None,
|
289
|
+
) -> None:
|
290
|
+
"""Initialize the discrete dynamical system with either a predefined model or custom mappings.
|
291
|
+
|
292
|
+
Parameters
|
293
|
+
----------
|
294
|
+
model : str, optional
|
295
|
+
Name of the predefined model to use.
|
296
|
+
mapping : callable, optional
|
297
|
+
Custom mapping function with signature f(u, parameters) -> array_like
|
298
|
+
jacobian : callable, optional
|
299
|
+
Custom Jacobian function with signature J(u, parameters, *args) -> array_like
|
300
|
+
backwards_mapping : callable, optional
|
301
|
+
Custom inverse mapping function with signature f_inv(u, parameters) -> array_like
|
302
|
+
system_dimension : int, optional
|
303
|
+
Dimension of the system (number of variables in the mapping).
|
304
|
+
|
305
|
+
Raises
|
306
|
+
------
|
307
|
+
ValueError
|
308
|
+
- If neither model nor mapping is provided.
|
309
|
+
- If both model or mapping are provided.
|
310
|
+
- If provided model name is not implemented.
|
311
|
+
|
312
|
+
Notes
|
313
|
+
-----
|
314
|
+
- When providing custom functions, either provide both mapping and jacobian,
|
315
|
+
or just mapping (in which case finite differences will be used for Jacobian)
|
316
|
+
- When providing custom functions, the mapping function signature should be f(u, parameters) -> NDArray[np.float64]
|
317
|
+
|
318
|
+
Examples
|
319
|
+
--------
|
320
|
+
>>> # Using predefined model
|
321
|
+
>>> system = DynamicalSystem(model="henon_map")
|
322
|
+
>>> # Using custom mappings
|
323
|
+
>>> system = DynamicalSystem(mapping=my_map, jacobian=my_jacobian, system_dimension=dim)
|
324
|
+
"""
|
325
|
+
|
326
|
+
if model is not None and mapping is not None:
|
327
|
+
raise ValueError("Cannot specify both model and custom mapping")
|
328
|
+
|
329
|
+
if model is not None:
|
330
|
+
model = model.lower()
|
331
|
+
if model not in self.__AVAILABLE_MODELS:
|
332
|
+
available = "\n".join(
|
333
|
+
f"- {name}: {info['description']}"
|
334
|
+
for name, info in self.__AVAILABLE_MODELS.items()
|
335
|
+
)
|
336
|
+
raise ValueError(
|
337
|
+
f"Model '{model}' not implemented. Available models:\n{available}"
|
338
|
+
)
|
339
|
+
|
340
|
+
model_info = self.__AVAILABLE_MODELS[model]
|
341
|
+
self.__model = model
|
342
|
+
self.__mapping = model_info["mapping"]
|
343
|
+
self.__jacobian = model_info["jacobian"]
|
344
|
+
self.__backwards_mapping = model_info["backwards_mapping"]
|
345
|
+
self.__system_dimension = model_info["dimension"]
|
346
|
+
self.__number_of_parameters = model_info["number_of_parameters"]
|
347
|
+
|
348
|
+
if jacobian is not None: # Allow override of default Jacobian
|
349
|
+
self.__jacobian = jacobian
|
350
|
+
|
351
|
+
if backwards_mapping is not None: # Allow override of default backwards map
|
352
|
+
self.__backwards_mapping = backwards_mapping
|
353
|
+
|
354
|
+
elif (
|
355
|
+
mapping is not None
|
356
|
+
and system_dimension is not None
|
357
|
+
and number_of_parameters is not None
|
358
|
+
):
|
359
|
+
self.__mapping = mapping
|
360
|
+
self.__jacobian = (
|
361
|
+
jacobian if jacobian is not None else finite_difference_jacobian
|
362
|
+
)
|
363
|
+
self.__backwards_mapping = backwards_mapping
|
364
|
+
|
365
|
+
validate_non_negative(system_dimension, "system_dimension", Integral)
|
366
|
+
validate_non_negative(
|
367
|
+
number_of_parameters, "number_of_parameters", Integral
|
368
|
+
)
|
369
|
+
|
370
|
+
self.__system_dimension = system_dimension
|
371
|
+
self.__number_of_parameters = number_of_parameters
|
372
|
+
|
373
|
+
# Validate custom functions
|
374
|
+
if not callable(self.__mapping):
|
375
|
+
raise TypeError("Custom mapping must be callable")
|
376
|
+
|
377
|
+
if self.__jacobian is not None and not callable(self.__jacobian):
|
378
|
+
raise TypeError("Custom Jacobian must be callable or None")
|
379
|
+
|
380
|
+
if self.__backwards_mapping is not None and not callable(
|
381
|
+
self.__backwards_mapping
|
382
|
+
):
|
383
|
+
raise TypeError("Custom backwards mapping must be callable or None")
|
384
|
+
|
385
|
+
else:
|
386
|
+
raise ValueError(
|
387
|
+
"Must specify either a model name or custom mapping function with its dimension and number of paramters."
|
388
|
+
)
|
389
|
+
|
390
|
+
@classmethod
|
391
|
+
def available_models(cls) -> List[str]:
|
392
|
+
"""Return a list of available models."""
|
393
|
+
return list(cls.__AVAILABLE_MODELS.keys())
|
394
|
+
|
395
|
+
@property
|
396
|
+
def info(self) -> Dict[str, Any]:
|
397
|
+
"""Return a dictionary with information about the current model."""
|
398
|
+
|
399
|
+
if self.__model is None:
|
400
|
+
raise ValueError(
|
401
|
+
"The 'info' property is only available when a model is provided."
|
402
|
+
)
|
403
|
+
|
404
|
+
model = self.__model.lower()
|
405
|
+
|
406
|
+
return self.__AVAILABLE_MODELS[model]
|
407
|
+
|
408
|
+
def trajectory(
|
409
|
+
self,
|
410
|
+
u: Union[NDArray[np.float64], Sequence[float], float],
|
411
|
+
total_time: int,
|
412
|
+
parameters: Union[
|
413
|
+
None, float, Sequence[np.float64], NDArray[np.float64]
|
414
|
+
] = None,
|
415
|
+
transient_time: Optional[int] = None,
|
416
|
+
) -> NDArray[np.float64]:
|
417
|
+
"""Generate trajectory for either single initial condition or ensemble of initial conditions.
|
418
|
+
|
419
|
+
Automatically dispatches to appropriate implementation based on input dimensionality.
|
420
|
+
For ensembles, trajectories are concatenated along time dimension for efficient storage.
|
421
|
+
|
422
|
+
Parameters
|
423
|
+
----------
|
424
|
+
u : Union[NDArray[np.float64], Sequence[float]]
|
425
|
+
Initial condition(s):
|
426
|
+
- Single IC: 1D array of shape (d,) where d is system dimension
|
427
|
+
- Ensemble: 2D array of shape (n, d) for n initial conditions
|
428
|
+
- Also accepts sequence types that will be converted to numpy arrays
|
429
|
+
- Scalar (will be converted to 1D array)
|
430
|
+
total_time : int
|
431
|
+
Total number of iterations to compute
|
432
|
+
parameters : Union[NDArray[np.float64], Sequence[float], float], optional
|
433
|
+
Parameters of the dynamical system, shape (p,) where p is number of parameters
|
434
|
+
transient_time : Optional[int], optional
|
435
|
+
Number of initial iterations to discard as transient, by default None
|
436
|
+
If provided, must be less than total_time
|
437
|
+
|
438
|
+
Returns
|
439
|
+
-------
|
440
|
+
NDArray[np.float64]
|
441
|
+
Time series array:
|
442
|
+
|
443
|
+
- Single IC: shape (sample_size, d)
|
444
|
+
- Ensemble: shape (sample_size * n, d) where sample_size = total_time - (transient_time or 0)
|
445
|
+
|
446
|
+
Raises
|
447
|
+
------
|
448
|
+
ValueError
|
449
|
+
- If `u` is not a scalar, 1D, or 2D array, or if its shape does not match the expected system dimension.
|
450
|
+
- If `u` is a 1D array but its length does not match the system dimension, or if `u` is a 2D array but does not match the expected shape for an ensemble.
|
451
|
+
- If `parameters` is not None and does not match the expected number of parameters.
|
452
|
+
- If `parameters` is None but the system expects parameters.
|
453
|
+
- If `parameters` is a scalar or array-like but not 1D.
|
454
|
+
- If `total_time` is negative.
|
455
|
+
- If `trasient_time` is negative.
|
456
|
+
- If `transient_time` is greater than or equal to total_time.
|
457
|
+
|
458
|
+
TypeError
|
459
|
+
- If `u` is not a scalar or array-like type.
|
460
|
+
- If `parameters` is not a scalar or array-like type.
|
461
|
+
- If `total_time` is not int.
|
462
|
+
- If `transient_time` is not int.
|
463
|
+
|
464
|
+
Notes
|
465
|
+
-----
|
466
|
+
- For ensembles, use reshape() to separate trajectories: result.reshape(n, sample_size, d)
|
467
|
+
|
468
|
+
Examples
|
469
|
+
--------
|
470
|
+
>>> # Single initial condition
|
471
|
+
>>> u0 = np.array([0.1, 0.2])
|
472
|
+
>>> ts = system.trajectory(u0, 5000, parameters=[0.5, 1.0])
|
473
|
+
>>> ts.shape # (5000, 2)
|
474
|
+
|
475
|
+
>>> # Ensemble of 100 initial conditions
|
476
|
+
>>> ics = np.random.rand(100, 2)
|
477
|
+
>>> ts = system.trajectory(ics, 10000, parameters=[1.0, 0.1], transient_time=1000)
|
478
|
+
>>> separated = ts.reshape(100, 9000, 2) # 9000 = 10000-1000
|
479
|
+
"""
|
480
|
+
|
481
|
+
u = validate_initial_conditions(u, self.__system_dimension, allow_ensemble=True)
|
482
|
+
|
483
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
484
|
+
|
485
|
+
validate_non_negative(total_time, "total_time", Integral)
|
486
|
+
validate_transient_time(transient_time, total_time, type_=Integral)
|
487
|
+
|
488
|
+
if u.ndim == 1:
|
489
|
+
return generate_trajectory(
|
490
|
+
u, parameters, total_time, self.__mapping, transient_time=transient_time
|
491
|
+
)
|
492
|
+
else:
|
493
|
+
return ensemble_trajectories(
|
494
|
+
u, parameters, total_time, self.__mapping, transient_time=transient_time
|
495
|
+
)
|
496
|
+
|
497
|
+
def bifurcation_diagram(
|
498
|
+
self,
|
499
|
+
u: Union[NDArray[np.float64], Sequence[float], float],
|
500
|
+
param_index: int,
|
501
|
+
param_range: Union[NDArray[np.float64], Tuple[float, float, int]],
|
502
|
+
total_time: int,
|
503
|
+
parameters: Optional[NDArray[np.float64]] = None,
|
504
|
+
transient_time: Optional[int] = None,
|
505
|
+
continuation: bool = False,
|
506
|
+
return_last_state: bool = False,
|
507
|
+
observable_index: int = 0,
|
508
|
+
) -> Union[
|
509
|
+
Tuple[NDArray[np.float64], NDArray[np.float64]],
|
510
|
+
Tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]],
|
511
|
+
]:
|
512
|
+
"""Compute bifurcation diagram by varying a specified parameter.
|
513
|
+
|
514
|
+
Parameters
|
515
|
+
----------
|
516
|
+
u : Union[NDArray[np.float64], Sequence[float]]
|
517
|
+
Initial condition vector. Can be:
|
518
|
+
- 1D numpy array of shape (d,) where d is system dimension
|
519
|
+
- Sequence that can be converted to numpy array
|
520
|
+
- Scalar (will be converted to 1D array)
|
521
|
+
parameters : Union[None, float, Sequence[np.float64], NDArray[np.float64]], optional
|
522
|
+
Base parameter array of shape (p,)
|
523
|
+
param_index : int
|
524
|
+
Index of parameter to vary in parameters array (0 <= param_index < p)
|
525
|
+
param_range : Union[NDArray[np.float64], Tuple[float, float, int]]
|
526
|
+
Parameter range specification, either:
|
527
|
+
- Precomputed 1D array of parameter values, or
|
528
|
+
- Tuple (start, stop, num_points) for linear spacing
|
529
|
+
total_time : int, optional
|
530
|
+
Total iterations per parameter value, by default 10000
|
531
|
+
transient_time : Optional[int], optional
|
532
|
+
Burn-in iterations to discard (default: 20% of total_time)
|
533
|
+
continuation: bool, optional
|
534
|
+
Whether to perform a continuation sweep, i.e., the initial condition for the next parameter value is the last state of the previous parameter.
|
535
|
+
return_last_state: bool, optional
|
536
|
+
Whether to return the last state at the last parameter value.
|
537
|
+
observable_index: int, optional
|
538
|
+
Defines the coordinate to be used in the bifurcation diagram (default = 0).
|
539
|
+
|
540
|
+
Returns
|
541
|
+
-------
|
542
|
+
Tuple[NDArray[np.float64], NDArray[np.float64]]
|
543
|
+
Tuple containing:
|
544
|
+
|
545
|
+
- parameter_values: 1D array of varied parameter values
|
546
|
+
- observables: 1D array of observable values (after transients)
|
547
|
+
|
548
|
+
Raises
|
549
|
+
------
|
550
|
+
ValueError
|
551
|
+
- If `u` is not a scalar, or 1D array, or if its shape does not match the expected system dimension.
|
552
|
+
- If `parameters` is not None and does not match the expected number of parameters.
|
553
|
+
- If `parameters` is None but the system expects parameters.
|
554
|
+
- If `parameters` is a scalar or array-like but not 1D.
|
555
|
+
- If `total_time` is negative.
|
556
|
+
- If `trasient_time` is negative.
|
557
|
+
- If `transient_time` is greater than or equal to total_time.
|
558
|
+
- If `param_index` is negative or out of bounds for the number of parameters.
|
559
|
+
- If `observable_index` is negative or out of bounds for the system dimension.
|
560
|
+
TypeError
|
561
|
+
- If `u` is not a scalar or array-like type.
|
562
|
+
- If `parameters` is not a scalar or array-like type.
|
563
|
+
- If `total_time` is not int.
|
564
|
+
- If `transient_time` is not int.
|
565
|
+
|
566
|
+
Notes
|
567
|
+
-----
|
568
|
+
- Uses Numba-optimized bifurcation_diagram function
|
569
|
+
- For large total_time, consider using a smaller transient_time
|
570
|
+
- The observable function should be vectorized for best performance
|
571
|
+
|
572
|
+
Examples
|
573
|
+
--------
|
574
|
+
>>> # Basic usage with precomputed parameter range
|
575
|
+
>>> param_range = np.linspace(0.5, 1.5, 100)
|
576
|
+
>>> u0 = np.array([0.1, 0.1])
|
577
|
+
>>> param_vals, obs = sys.bifurcation_diagram(
|
578
|
+
... u0, 0, param_range, 5000, parameters=[0.5, 1.0])
|
579
|
+
|
580
|
+
>>> # With tuple parameter range
|
581
|
+
>>> param_range = (0.5, 1.5, 100)
|
582
|
+
>>> param_vals, obs = sys.bifurcation_diagram(
|
583
|
+
... u0, 0, param_range, 5000, parameters=[0.5, 1.0])
|
584
|
+
|
585
|
+
"""
|
586
|
+
|
587
|
+
u = validate_initial_conditions(
|
588
|
+
u, self.__system_dimension, allow_ensemble=False
|
589
|
+
)
|
590
|
+
|
591
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters - 1)
|
592
|
+
|
593
|
+
validate_non_negative(param_index, "param_index", Integral)
|
594
|
+
if param_index >= self.__number_of_parameters:
|
595
|
+
raise ValueError(
|
596
|
+
f"param_index {param_index} out of bounds for system with {self.__number_of_parameters} parameters"
|
597
|
+
)
|
598
|
+
|
599
|
+
parameters = np.insert(parameters, param_index, 0)
|
600
|
+
|
601
|
+
param_values = validate_and_convert_param_range(param_range)
|
602
|
+
|
603
|
+
validate_non_negative(total_time, "total_time", Integral)
|
604
|
+
validate_transient_time(transient_time, total_time, Integral)
|
605
|
+
|
606
|
+
validate_non_negative(observable_index, "observable_index", Integral)
|
607
|
+
if observable_index >= self.__system_dimension:
|
608
|
+
raise ValueError(
|
609
|
+
f"observable_index {observable_index} out of bounds for system dimension {self.__system_dimension}"
|
610
|
+
)
|
611
|
+
|
612
|
+
def observable_fn(x):
|
613
|
+
return x[observable_index]
|
614
|
+
|
615
|
+
return bifurcation_diagram(
|
616
|
+
u=u,
|
617
|
+
parameters=parameters,
|
618
|
+
param_index=param_index,
|
619
|
+
param_range=param_values,
|
620
|
+
total_time=total_time,
|
621
|
+
mapping=self.__mapping,
|
622
|
+
transient_time=transient_time,
|
623
|
+
continuation=continuation,
|
624
|
+
return_last_state=return_last_state,
|
625
|
+
observable_fn=observable_fn,
|
626
|
+
)
|
627
|
+
|
628
|
+
def period(
|
629
|
+
self,
|
630
|
+
u: Union[NDArray[np.float64], Sequence[float], float],
|
631
|
+
max_time: int = 10000,
|
632
|
+
parameters: Union[
|
633
|
+
None, float, Sequence[np.float64], NDArray[np.float64]
|
634
|
+
] = None,
|
635
|
+
transient_time: Optional[int] = None,
|
636
|
+
tolerance: float = 1e-10,
|
637
|
+
min_period: int = 1,
|
638
|
+
max_period: int = 1000,
|
639
|
+
stability_checks: int = 3,
|
640
|
+
) -> int:
|
641
|
+
"""Compute the period of a trajectory.
|
642
|
+
|
643
|
+
This function determines the smallest period p where the system satisfies:
|
644
|
+
||x_{n+p} - x_n|| < tolerance for consecutive states after transients.
|
645
|
+
|
646
|
+
Parameters
|
647
|
+
----------
|
648
|
+
u : Union[NDArray[np.float64], Sequence[float]], float
|
649
|
+
Initial condition of the system. Can be:
|
650
|
+
- 1D numpy array of shape (d,) where d is system dimension
|
651
|
+
- Sequence that can be converted to numpy array
|
652
|
+
- Scalar (will be converted to 1D array)
|
653
|
+
parameters : Union[None, float, Sequence[np.float64], NDArray[np.float64]], optional
|
654
|
+
Parameters of the dynamical system, shape (p,)
|
655
|
+
max_time : int, optional
|
656
|
+
Total number of iterations to compute, by default 10000
|
657
|
+
Must be sufficiently large to detect periodicity
|
658
|
+
transient_time : Optional[int], optional
|
659
|
+
Number of initial iterations to discard as transient, by default None
|
660
|
+
If None, uses 10% of total_time
|
661
|
+
tolerance : float, optional
|
662
|
+
Numerical tolerance for period detection (default: 1e-10)
|
663
|
+
min_period : int, optional
|
664
|
+
Minimum period to consider (default: 1)
|
665
|
+
max_period : int, optional
|
666
|
+
Maximum period to consider (default: 1000)
|
667
|
+
stability_checks : int, optional
|
668
|
+
Number of consecutive period matches required (default: 3)
|
669
|
+
|
670
|
+
Returns
|
671
|
+
-------
|
672
|
+
int
|
673
|
+
The detected period of the trajectory.
|
674
|
+
|
675
|
+
- A positive integer indicates a periodic orbit.
|
676
|
+
- -1 indicates aperiodic or chaotic behavior.
|
677
|
+
- 1 indicates a fixed point.
|
678
|
+
|
679
|
+
Raises
|
680
|
+
------
|
681
|
+
ValueError
|
682
|
+
- If `u` is not a scalar, or 1D array, or if its shape does not match the expected system dimension.
|
683
|
+
- If `parameters` is not None and does not match the expected number of parameters.
|
684
|
+
- If `parameters` is None but the system expects parameters.
|
685
|
+
- If `parameters` is a scalar or array-like but not 1D.
|
686
|
+
- If `max_time` is negative.
|
687
|
+
- If `trasient_time` is negative.
|
688
|
+
- If `transient_time` is greater than or equal to total_time.
|
689
|
+
- If `min_period` is negative or zero.
|
690
|
+
- If `max_period` is negative or less than min_period.
|
691
|
+
- If `stability_checks` is not larger than 1.
|
692
|
+
- If `tolerance` is negative or zero.
|
693
|
+
TypeError
|
694
|
+
- If `u` is not a scalar or array-like type.
|
695
|
+
- If `parameters` is not a scalar or array-like type.
|
696
|
+
- If `max_time` is not int.
|
697
|
+
- If `transient_time` is not int.
|
698
|
+
- If `min_period` is not int.
|
699
|
+
- If `max_period` is not int.
|
700
|
+
- If `stability_checks` is not int.
|
701
|
+
|
702
|
+
Notes
|
703
|
+
-----
|
704
|
+
- For reliable results, max_time should be much larger than expected period
|
705
|
+
- Fixed points return period 1 (constant signal)
|
706
|
+
- Chaotic trajectories return period -1 (no significant autocorrelation peaks)
|
707
|
+
|
708
|
+
Examples
|
709
|
+
--------
|
710
|
+
>>> # Basic usage
|
711
|
+
>>> u0 = np.array([0.1, 0.2])
|
712
|
+
>>> params = np.array([0.5, 1.0])
|
713
|
+
>>> period = system.period(u0, params)
|
714
|
+
|
715
|
+
>>> # With custom time parameters
|
716
|
+
>>> period = system.period(
|
717
|
+
... u0, params, total_time=5000, transient_time=500)
|
718
|
+
"""
|
719
|
+
|
720
|
+
# Validate initial condition
|
721
|
+
u = validate_initial_conditions(
|
722
|
+
u, self.__system_dimension, allow_ensemble=False
|
723
|
+
)
|
724
|
+
|
725
|
+
# Validate parameters
|
726
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
727
|
+
|
728
|
+
# Validate time parameters
|
729
|
+
validate_non_negative(max_time, "total_time", Integral)
|
730
|
+
validate_transient_time(transient_time, max_time, Integral)
|
731
|
+
|
732
|
+
# Validate min and max period
|
733
|
+
validate_positive(min_period, "min_period", Integral)
|
734
|
+
validate_positive(max_period, "max_period", Integral)
|
735
|
+
|
736
|
+
# Validate stability checks
|
737
|
+
if not isinstance(stability_checks, Integral) or stability_checks < 1:
|
738
|
+
raise ValueError("stability_checks must be positive integer")
|
739
|
+
|
740
|
+
# Validade tolerance
|
741
|
+
validate_non_negative(tolerance, "tolerance", Real)
|
742
|
+
|
743
|
+
return period_counter(
|
744
|
+
u=u,
|
745
|
+
parameters=parameters,
|
746
|
+
mapping=self.__mapping,
|
747
|
+
total_time=max_time,
|
748
|
+
transient_time=transient_time,
|
749
|
+
tolerance=tolerance,
|
750
|
+
min_period=min_period,
|
751
|
+
max_period=max_period,
|
752
|
+
stability_checks=stability_checks,
|
753
|
+
)
|
754
|
+
|
755
|
+
def find_periodic_orbit(
|
756
|
+
self,
|
757
|
+
grid_points: Union[NDArray[np.float64], Sequence[float]],
|
758
|
+
period: int,
|
759
|
+
parameters: Union[None, NDArray[np.float64], Sequence[float], float] = None,
|
760
|
+
tolerance: float = 1e-5,
|
761
|
+
max_iter: int = 1000,
|
762
|
+
convergence_threshold: float = 1e-15,
|
763
|
+
tolerance_decay_factor: float = 1 / 2,
|
764
|
+
verbose: bool = False,
|
765
|
+
symmetry_line: Optional[Callable] = None,
|
766
|
+
axis: Optional[int] = None,
|
767
|
+
) -> NDArray[np.float64]:
|
768
|
+
"""
|
769
|
+
Find a periodic orbit using iterative grid refinement.
|
770
|
+
|
771
|
+
Parameters
|
772
|
+
----------
|
773
|
+
grid_points : np.ndarray
|
774
|
+
n
|
775
|
+
period : int
|
776
|
+
The period of the orbit to find. Must be ≥ 1.
|
777
|
+
parameters : Union[None, NDArray[np.float64], Sequence[float], float], optional
|
778
|
+
Array of system parameters (shape (p,)) or a scalar to be broadcasted.
|
779
|
+
tolerance : float, optional
|
780
|
+
Initial periodicity tolerance. Must be positive. Default is 1e-5.
|
781
|
+
max_iter : int, optional
|
782
|
+
Maximum number of refinement iterations. Must be ≥ 1. Default is 1000.
|
783
|
+
convergence_threshold : float, optional
|
784
|
+
Convergence threshold for both position and bounding box. Must be positive.
|
785
|
+
Default is 1e-15.
|
786
|
+
tolerance_decay_factor : float, optional
|
787
|
+
Factor by which to reduce tolerance each iteration. Must be in (0, 1).
|
788
|
+
Default is 0.25.
|
789
|
+
verbose : bool, optional
|
790
|
+
Whether to print iteration progress and convergence information. Default is False.
|
791
|
+
symmetry_line : Optional[Callable], optional
|
792
|
+
A callable function representing a symmetry line in the system. If provided,
|
793
|
+
the search will be restricted to points on this line.
|
794
|
+
axis : Optional[int], optional
|
795
|
+
Axis of symmetry line. Must be 0 (x-axis) or 1 (y-axis)
|
796
|
+
|
797
|
+
Returns
|
798
|
+
-------
|
799
|
+
np.ndarray
|
800
|
+
A 1D array of shape (2,) representing the coordinates of the found periodic orbit.
|
801
|
+
|
802
|
+
Raises
|
803
|
+
------
|
804
|
+
ValueError
|
805
|
+
- If grid_points is not of shape (grid_size_x, grid_size_y, 2).
|
806
|
+
- If period is less than 1.
|
807
|
+
- If tolerance is not positive.
|
808
|
+
- If max_iter is not positive.
|
809
|
+
- If convergence_threshold is not positive.
|
810
|
+
- If tolerance_decay_factor is not in (0, 1).
|
811
|
+
- If symmetry_line is provided but axis is not specified.
|
812
|
+
- If symmetry_line is not callable.
|
813
|
+
- If axis is not 0 (x-axis) or 1 (y-axis).
|
814
|
+
- If system_dimension is not 2D.
|
815
|
+
- If grid_points is a scalar or not a 3D array when symmetry_line is None.
|
816
|
+
|
817
|
+
TypeError
|
818
|
+
- If grid_points is not a numpy array or a sequence that can be converted to a numpy array.
|
819
|
+
- If parameters is not None and not a numpy array or sequence that can be converted to a numpy array.
|
820
|
+
- If period is not an integer.
|
821
|
+
- If tolerance is not float.
|
822
|
+
- If max_iter is not int.
|
823
|
+
- If convergence_threshold is not float.
|
824
|
+
- If tolerance_decay_factor is not float.
|
825
|
+
- If axis is not an integer.
|
826
|
+
|
827
|
+
Notes
|
828
|
+
-----
|
829
|
+
This function wraps the core `find_periodic_orbit` function using the instance's mapping.
|
830
|
+
The underlying implementation performs a multiscale search for periodic points
|
831
|
+
through iterative refinement around previously found periodic locations.
|
832
|
+
"""
|
833
|
+
|
834
|
+
if self.__system_dimension != 2:
|
835
|
+
raise ValueError("find_periodic_orbit is only implemented for 2D systems")
|
836
|
+
|
837
|
+
# Check if symmetry line is provided
|
838
|
+
if symmetry_line is not None and axis is None:
|
839
|
+
raise ValueError("axis must be provided when symmetry_line is specified")
|
840
|
+
|
841
|
+
# Check if symmetry line is valid
|
842
|
+
if symmetry_line is not None and not callable(symmetry_line):
|
843
|
+
raise ValueError("symmetry_line must be a callable function")
|
844
|
+
|
845
|
+
# Check if axis is valid
|
846
|
+
if axis is not None and axis not in [0, 1]:
|
847
|
+
raise ValueError("axis must be 0 (x-axis) or 1 (y-axis)")
|
848
|
+
|
849
|
+
if np.isscalar(grid_points):
|
850
|
+
raise ValueError(
|
851
|
+
"grid_points must be a 3D array with shape (grid_size_x, grid_size_y, 2) if symmetry_line is None and 1D array otherwise"
|
852
|
+
)
|
853
|
+
|
854
|
+
grid_points = np.asarray(grid_points, dtype=np.float64)
|
855
|
+
# Validate grid points
|
856
|
+
if symmetry_line is None:
|
857
|
+
if grid_points.ndim != 3 or grid_points.shape[2] != 2:
|
858
|
+
raise ValueError(
|
859
|
+
"grid_points must be a 3D array with shape (grid_size_x, grid_size_y, 2)"
|
860
|
+
)
|
861
|
+
else:
|
862
|
+
if grid_points.ndim != 1:
|
863
|
+
raise ValueError(
|
864
|
+
"grid_points must be a 1D array when symmetry_line is provided"
|
865
|
+
)
|
866
|
+
|
867
|
+
# Validate parameters
|
868
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
869
|
+
|
870
|
+
# Validate period
|
871
|
+
validate_positive(period, "period", Integral)
|
872
|
+
|
873
|
+
# Validate tolerance
|
874
|
+
validate_non_negative(tolerance, "tolerance", Real)
|
875
|
+
|
876
|
+
# Validate max_iter
|
877
|
+
validate_positive(max_iter, "max_iter", Integral)
|
878
|
+
|
879
|
+
# Validate convergence threshold
|
880
|
+
validate_non_negative(convergence_threshold, "convergence_threshold", Real)
|
881
|
+
|
882
|
+
# Validate tolerance decay factor
|
883
|
+
validate_non_negative(tolerance_decay_factor, "tolerance_decay_factor", Real)
|
884
|
+
|
885
|
+
if tolerance_decay_factor >= 1:
|
886
|
+
raise ValueError("tolerance_decay_factor must be in (0, 1)")
|
887
|
+
|
888
|
+
if symmetry_line is not None:
|
889
|
+
return find_periodic_orbit_symmetry_line(
|
890
|
+
grid_points,
|
891
|
+
parameters,
|
892
|
+
self.__mapping,
|
893
|
+
period,
|
894
|
+
symmetry_line,
|
895
|
+
axis,
|
896
|
+
tolerance=tolerance,
|
897
|
+
max_iter=max_iter,
|
898
|
+
convergence_threshold=convergence_threshold,
|
899
|
+
tolerance_decay_factor=tolerance_decay_factor,
|
900
|
+
verbose=verbose,
|
901
|
+
)
|
902
|
+
else:
|
903
|
+
return find_periodic_orbit(
|
904
|
+
grid_points,
|
905
|
+
parameters,
|
906
|
+
self.__mapping,
|
907
|
+
period,
|
908
|
+
tolerance=tolerance,
|
909
|
+
max_iter=max_iter,
|
910
|
+
convergence_threshold=convergence_threshold,
|
911
|
+
tolerance_decay_factor=tolerance_decay_factor,
|
912
|
+
verbose=verbose,
|
913
|
+
)
|
914
|
+
|
915
|
+
def eigenvalues_and_eigenvectors(
|
916
|
+
self,
|
917
|
+
u: Union[NDArray[np.float64], Sequence[float]],
|
918
|
+
period: int,
|
919
|
+
parameters: Union[
|
920
|
+
None, float, Sequence[np.float64], NDArray[np.float64]
|
921
|
+
] = None,
|
922
|
+
normalize: bool = True,
|
923
|
+
sort_by_magnitude: bool = True,
|
924
|
+
) -> Tuple[NDArray[np.complex128], NDArray[np.complex128]]:
|
925
|
+
"""
|
926
|
+
Compute eigenvalues and eigenvectors of the Jacobian matrix for a periodic orbit.
|
927
|
+
|
928
|
+
Parameters
|
929
|
+
----------
|
930
|
+
u : Union[NDArray[np.float64], Sequence[float]]
|
931
|
+
Initial condition of the system. Can be:
|
932
|
+
- 1D numpy array of shape (d,) where d is the system dimension
|
933
|
+
- Sequence that can be converted to numpy array
|
934
|
+
parameters : Union[None, float, Sequence[np.float64], NDArray[np.float64]], optional
|
935
|
+
System parameters of shape (p,).
|
936
|
+
period : int
|
937
|
+
Period of the orbit (must be ≥ 1).
|
938
|
+
normalize : bool, optional
|
939
|
+
Whether to normalize eigenvectors to unit length (default is True).
|
940
|
+
sort_by_magnitude : bool, optional
|
941
|
+
Whether to sort eigenvalues and eigenvectors by the magnitude of the eigenvalues (default is True).
|
942
|
+
|
943
|
+
Returns
|
944
|
+
-------
|
945
|
+
Tuple[NDArray[np.complex128], NDArray[np.complex128]]
|
946
|
+
|
947
|
+
- eigenvalues : (d,) array of complex eigenvalues.
|
948
|
+
- eigenvectors : (d, d) array where each column is a normalized eigenvector corresponding to an eigenvalue.
|
949
|
+
|
950
|
+
Raises
|
951
|
+
------
|
952
|
+
ValueError
|
953
|
+
- If `u` is not a scalar, or 1D array, or if its shape does not match the expected system dimension.
|
954
|
+
- If `parameters` is not None and does not match the expected number of parameters.
|
955
|
+
- If `parameters` is None but the system expects parameters.
|
956
|
+
- If `parameters` is a scalar or array-like but not 1D.
|
957
|
+
- If `period` is negative or zero.
|
958
|
+
TypeError
|
959
|
+
- If `u` is not a scalar or array-like type.
|
960
|
+
- If `parameters` is not a scalar or array-like type.
|
961
|
+
- If `period` is not int.
|
962
|
+
|
963
|
+
Notes
|
964
|
+
-----
|
965
|
+
- Computes the Jacobian matrix over `period` iterations using the product of Jacobians.
|
966
|
+
- Eigenvectors indicate local directions of stretching or contraction in phase space.
|
967
|
+
- Complex eigenvalues appear in conjugate pairs in real-valued systems.
|
968
|
+
|
969
|
+
Examples
|
970
|
+
--------
|
971
|
+
>>> # Example usage
|
972
|
+
>>> from pynamicalsys import DiscreteDynamicalSystem as dds
|
973
|
+
>>> obj = dds(model="henon map")
|
974
|
+
>>> u0 = np.array([0.1, 0.2])
|
975
|
+
>>> params = np.array([1.0, 0.1])
|
976
|
+
>>> evals, evecs = obj.eigenvalues_and_eigenvectors(
|
977
|
+
... u0, params, period=3)
|
978
|
+
"""
|
979
|
+
|
980
|
+
# Validate initial condition
|
981
|
+
u = validate_initial_conditions(
|
982
|
+
u, self.__system_dimension, allow_ensemble=False
|
983
|
+
)
|
984
|
+
|
985
|
+
# Validate parameters
|
986
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
987
|
+
|
988
|
+
# Validate period
|
989
|
+
validate_positive(period, "period", Integral)
|
990
|
+
|
991
|
+
return eigenvalues_and_eigenvectors(
|
992
|
+
u,
|
993
|
+
parameters,
|
994
|
+
self.__mapping,
|
995
|
+
self.__jacobian,
|
996
|
+
period,
|
997
|
+
normalize,
|
998
|
+
sort_by_magnitude,
|
999
|
+
)
|
1000
|
+
|
1001
|
+
def classify_stability(
|
1002
|
+
self,
|
1003
|
+
u: Union[NDArray[np.float64], Sequence[float]],
|
1004
|
+
period: int,
|
1005
|
+
parameters: Union[
|
1006
|
+
None, float, Sequence[np.float64], NDArray[np.float64]
|
1007
|
+
] = None,
|
1008
|
+
threshold: float = 1.0,
|
1009
|
+
tol: float = 1e-8,
|
1010
|
+
) -> Dict[str, Union[str, NDArray[np.complex128]]]:
|
1011
|
+
"""
|
1012
|
+
Classify the stability of a periodic orbit using the eigenvalues of the Jacobian matrix for a 2D discrete map.
|
1013
|
+
|
1014
|
+
Parameters
|
1015
|
+
----------
|
1016
|
+
u : Union[NDArray[np.float64], Sequence[float]]
|
1017
|
+
Initial condition of the system. Can be:
|
1018
|
+
- 1D numpy array of shape (2,) where 2 is the system dimension
|
1019
|
+
- Sequence that can be converted to numpy array
|
1020
|
+
period : int
|
1021
|
+
Period of the orbit (must be ≥ 1).
|
1022
|
+
parameters : Union[None, float, Sequence[np.float64], NDArray[np.float64]], optional
|
1023
|
+
System parameters of shape (p,).
|
1024
|
+
threshold : float, optional
|
1025
|
+
Threshold for stability classification (default is 1.0).
|
1026
|
+
tol : float, optional
|
1027
|
+
Tolerance for numerical stability checks (default is 1e-8).
|
1028
|
+
|
1029
|
+
Returns
|
1030
|
+
-------
|
1031
|
+
dict
|
1032
|
+
Dictionary with:
|
1033
|
+
|
1034
|
+
- "classification": str
|
1035
|
+
- "eigenvalues": ndarray
|
1036
|
+
- "eigenvectors": ndarray
|
1037
|
+
|
1038
|
+
Raises
|
1039
|
+
------
|
1040
|
+
ValueError
|
1041
|
+
- If `u` is not a scalar, or 1D array, or if its shape does not match the expected system dimension.
|
1042
|
+
- If `parameters` is not None and does not match the expected number of parameters.
|
1043
|
+
- If `parameters` is None but the system expects parameters.
|
1044
|
+
- If `parameters` is a scalar or array-like but not 1D.
|
1045
|
+
- If `period` is negative or zero.
|
1046
|
+
TypeError
|
1047
|
+
- If `u` is not a scalar or array-like type.
|
1048
|
+
- If `parameters` is not a scalar or array-like type.
|
1049
|
+
- If `period` is not int.
|
1050
|
+
|
1051
|
+
Notes
|
1052
|
+
-----
|
1053
|
+
- The classification is based on the eigenvalues of the Jacobian matrix.
|
1054
|
+
- The eigenvalues are computed over `period` iterations using the product of Jacobians.
|
1055
|
+
- The classification can be one of:
|
1056
|
+
- "stable node": All eigenvalues have magnitudes < threshold.
|
1057
|
+
- "stable spiral": Complex conjugate eigenvalues with magnitudes < threshold.
|
1058
|
+
- "unstable node": All eigenvalues have magnitudes > threshold.
|
1059
|
+
- "unstable spiral": Complex conjugate eigenvalues with magnitudes > threshold.
|
1060
|
+
- "saddle": One eigenvalue > threshold and one < threshold.
|
1061
|
+
- "center": Real eigenvalues with magnitudes ≈ threshold.
|
1062
|
+
- "elliptic": Complex eigenvalues with magnitudes ≈ threshold.
|
1063
|
+
- "marginal or degenerate": Eigenvalues with magnitudes ≈ 1.
|
1064
|
+
|
1065
|
+
Examples
|
1066
|
+
--------
|
1067
|
+
>>> u0 = np.array([0.1, 0.2])
|
1068
|
+
>>> params = np.array([1.0, 0.1])
|
1069
|
+
>>> stability = obj.classify_stability(u0, params, period=3)
|
1070
|
+
>>> print(stability["classification"]) # e.g., "stable node"
|
1071
|
+
>>> print(stability["eigenvalues"]) # Eigenvalues of the Jacobian
|
1072
|
+
>>> print(stability["eigenvectors"])
|
1073
|
+
"""
|
1074
|
+
|
1075
|
+
if self.__system_dimension != 2:
|
1076
|
+
raise ValueError("classify_stability is only implemented for 2D systems")
|
1077
|
+
|
1078
|
+
u = validate_initial_conditions(
|
1079
|
+
u, self.__system_dimension, allow_ensemble=False
|
1080
|
+
)
|
1081
|
+
|
1082
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
1083
|
+
|
1084
|
+
validate_positive(period, "period", Integral)
|
1085
|
+
|
1086
|
+
return classify_stability(
|
1087
|
+
u,
|
1088
|
+
parameters,
|
1089
|
+
self.__mapping,
|
1090
|
+
self.__jacobian,
|
1091
|
+
period,
|
1092
|
+
threshold=threshold,
|
1093
|
+
tol=tol,
|
1094
|
+
)
|
1095
|
+
|
1096
|
+
def manifold(
|
1097
|
+
self,
|
1098
|
+
u: Union[NDArray[np.float64], Sequence[float]],
|
1099
|
+
period: int,
|
1100
|
+
parameters: Union[
|
1101
|
+
None, float, Sequence[np.float64], NDArray[np.float64]
|
1102
|
+
] = None,
|
1103
|
+
delta: float = 1e-4,
|
1104
|
+
n_points: Union[NDArray[np.int32], List[int], int] = 100,
|
1105
|
+
iter_time: Union[List[int], int] = 100,
|
1106
|
+
stability: str = "unstable",
|
1107
|
+
) -> List[np.ndarray]:
|
1108
|
+
"""Calculate stable or unstable manifolds of a saddle periodic orbit of the system.
|
1109
|
+
|
1110
|
+
Parameters
|
1111
|
+
----------
|
1112
|
+
u : Union[NDArray[np.float64], Sequence[float]]
|
1113
|
+
Initial condition of the system. Can be:
|
1114
|
+
- 1D numpy array of shape (2,) where 2 is the system dimension
|
1115
|
+
- Sequence that can be converted to numpy array
|
1116
|
+
period : int
|
1117
|
+
Period of the orbit (must be ≥ 1)
|
1118
|
+
parameters : Union[None, float, Sequence[np.float64], NDArray[np.float64]], optional
|
1119
|
+
Parameters of the dynamical system. Can be:
|
1120
|
+
|
1121
|
+
- 1D numpy array of shape (p,) where p is the number of parameters
|
1122
|
+
- Sequence that can be converted to numpy array
|
1123
|
+
- Scalar value (will be broadcasted)
|
1124
|
+
delta : float, optional
|
1125
|
+
Initial displacement from orbit (default: 1e-4)
|
1126
|
+
n_points : Union[List[int], int], optional
|
1127
|
+
Number of points per branch (default: 100)
|
1128
|
+
iter_time : Union[List[int], int], optional
|
1129
|
+
Iterations per branch (default: 100)
|
1130
|
+
stability : str, optional
|
1131
|
+
"stable" or "unstable" manifold (default: "unstable")
|
1132
|
+
|
1133
|
+
Returns
|
1134
|
+
-------
|
1135
|
+
List[np.ndarray]
|
1136
|
+
List containing two arrays: [0] is upper branch manifold points and [1] is lower branch manifold points. Each array has shape (n_points * iter_time, 2)
|
1137
|
+
|
1138
|
+
Raises
|
1139
|
+
------
|
1140
|
+
ValueError
|
1141
|
+
- If `u` is not a scalar, or 1D array, or if its shape does not match the expected system dimension.
|
1142
|
+
- If `parameters` is not None and does not match the expected number of parameters.
|
1143
|
+
- If `parameters` is None but the system expects parameters.
|
1144
|
+
- If `parameters` is a scalar or array-like but not 1D.
|
1145
|
+
- If `period` is negative or zero.
|
1146
|
+
- If `delta` is negative or zero.
|
1147
|
+
- If `n_points` is not a positive integer or a list of two positive integers.
|
1148
|
+
- If `iter_time` is not a positive integer or a list of two positive integers.
|
1149
|
+
- If `stability` is not "stable" or "unstable".
|
1150
|
+
- If system dimension is not 2D.
|
1151
|
+
TypeError
|
1152
|
+
- If `u` is not a scalar or array-like type.
|
1153
|
+
- If `parameters` is not a scalar or array-like type.
|
1154
|
+
- If `period` is not int.
|
1155
|
+
RuntimeError
|
1156
|
+
- If `stability` is "stable" but backwards mapping function is not defined.
|
1157
|
+
|
1158
|
+
Notes
|
1159
|
+
-----
|
1160
|
+
- Works only for 2D systems
|
1161
|
+
- The periodic orbit must be a saddle point
|
1162
|
+
- Manifold quality depends on:
|
1163
|
+
- delta (smaller = closer to linear approximation)
|
1164
|
+
- n_points (more = smoother manifold)
|
1165
|
+
- iter_time (more = longer manifold)
|
1166
|
+
|
1167
|
+
Examples
|
1168
|
+
--------
|
1169
|
+
>>> # Example usage
|
1170
|
+
>>> from pynamicalsys import DiscreteDynamicalSystem as dds
|
1171
|
+
>>> # Define the system
|
1172
|
+
>>> obj = dds(model="standard map")
|
1173
|
+
>>> # Calculate unstable manifold
|
1174
|
+
>>> mani = obj.manifold(
|
1175
|
+
... orbit_point, params,
|
1176
|
+
... period=3, delta=1e-5, n_points=200, iter_time=500)
|
1177
|
+
>>> upper_branch, lower_branch = manifolds
|
1178
|
+
"""
|
1179
|
+
|
1180
|
+
if self.__system_dimension != 2:
|
1181
|
+
raise ValueError("manifold is only implemented for 2D systems")
|
1182
|
+
|
1183
|
+
if self.__backwards_mapping is None and stability == "stable":
|
1184
|
+
raise RuntimeError("Backwards mapping function must be provided")
|
1185
|
+
|
1186
|
+
u = validate_initial_conditions(
|
1187
|
+
u, self.__system_dimension, allow_ensemble=False
|
1188
|
+
)
|
1189
|
+
|
1190
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
1191
|
+
|
1192
|
+
validate_positive(period, "period", Integral)
|
1193
|
+
|
1194
|
+
validate_non_negative(delta, "delta", Real)
|
1195
|
+
|
1196
|
+
# Validate n_points
|
1197
|
+
if isinstance(n_points, int):
|
1198
|
+
# If n_points is a single integer, make it a list of two identical integers
|
1199
|
+
n_points = [n_points] * 2
|
1200
|
+
n_points = np.asarray(n_points, dtype=np.int32)
|
1201
|
+
elif isinstance(n_points, (list, np.ndarray)):
|
1202
|
+
if len(n_points) != 2:
|
1203
|
+
raise ValueError("n_points must be a list or array of two integers")
|
1204
|
+
if not all(isinstance(n, int) and n > 0 for n in n_points):
|
1205
|
+
raise ValueError("n_points must be a list of two positive integers")
|
1206
|
+
n_points = np.asarray(n_points, dtype=np.int32)
|
1207
|
+
else:
|
1208
|
+
raise ValueError("n_points must be an int or a list of two ints")
|
1209
|
+
|
1210
|
+
return calculate_manifolds(
|
1211
|
+
u,
|
1212
|
+
parameters,
|
1213
|
+
self.__mapping,
|
1214
|
+
self.__backwards_mapping,
|
1215
|
+
self.__jacobian,
|
1216
|
+
period,
|
1217
|
+
delta=delta,
|
1218
|
+
n_points=n_points,
|
1219
|
+
iter_time=iter_time,
|
1220
|
+
stability=stability,
|
1221
|
+
)
|
1222
|
+
|
1223
|
+
def rotation_number(
|
1224
|
+
self,
|
1225
|
+
u: Union[NDArray[np.float64], Sequence[float], float],
|
1226
|
+
total_time: int,
|
1227
|
+
parameters: Union[
|
1228
|
+
None, float, Sequence[np.float64], NDArray[np.float64]
|
1229
|
+
] = None,
|
1230
|
+
mod: int = 1,
|
1231
|
+
) -> float:
|
1232
|
+
"""Compute the rotation number of a trajectory.
|
1233
|
+
|
1234
|
+
Parameters
|
1235
|
+
----------
|
1236
|
+
u : Union[NDArray[np.float64], Sequence[float], float]
|
1237
|
+
Initial condition of the system. Can be:
|
1238
|
+
- 1D numpy array of shape (d,) where d is system dimension
|
1239
|
+
- Sequence that can be converted to numpy array
|
1240
|
+
- Scalar value
|
1241
|
+
total_time : int
|
1242
|
+
Total number of iterations to compute
|
1243
|
+
parameters : Union[None, float,
|
1244
|
+
Sequence[np.float64], NDArray[np.float64]]
|
1245
|
+
Parameters of the dynamical system, shape (p,)
|
1246
|
+
mod : int, optional
|
1247
|
+
Modulus for the rotation number calculation, by default 1
|
1248
|
+
|
1249
|
+
Returns
|
1250
|
+
-------
|
1251
|
+
float
|
1252
|
+
The computed rotation number.
|
1253
|
+
|
1254
|
+
Raises
|
1255
|
+
------
|
1256
|
+
ValueError
|
1257
|
+
- If `u` is not a scalar, or 1D array, or if its shape does not match the expected system dimension.
|
1258
|
+
- If `parameters` is not None and does not match the expected number of parameters.
|
1259
|
+
- If `parameters` is None but the system expects parameters.
|
1260
|
+
- If `parameters` is a scalar or array-like but not 1D.
|
1261
|
+
- If `total_time` is negative.
|
1262
|
+
TypeError
|
1263
|
+
- If `u` is not a scalar or array-like type.
|
1264
|
+
- If `parameters` is not a scalar or array-like type.
|
1265
|
+
- If `total_time` is not int.
|
1266
|
+
|
1267
|
+
Notes
|
1268
|
+
-----
|
1269
|
+
- The rotation number is a measure of the average angular displacement
|
1270
|
+
of a trajectory in phase space.
|
1271
|
+
- It is computed as the limit of the average angular displacement
|
1272
|
+
over a large number of iterations.
|
1273
|
+
- The rotation number is useful for analyzing the behavior of
|
1274
|
+
periodic orbits and chaotic dynamics.
|
1275
|
+
|
1276
|
+
Examples
|
1277
|
+
--------
|
1278
|
+
>>> # Basic usage
|
1279
|
+
>>> u0 = np.array([0.1, 0.2])
|
1280
|
+
>>> params = np.array([0.5, 1.0])
|
1281
|
+
>>> rotation_num = system.compute_rotation_number(u0, params)
|
1282
|
+
>>> # With custom time parameters
|
1283
|
+
>>> rotation_num = system.compute_rotation_number(
|
1284
|
+
... u0, params, total_time=5000)
|
1285
|
+
"""
|
1286
|
+
|
1287
|
+
if self.__system_dimension != 2:
|
1288
|
+
raise ValueError("rotation_number is only implemented for 2D systems")
|
1289
|
+
|
1290
|
+
u = validate_initial_conditions(
|
1291
|
+
u, self.__system_dimension, allow_ensemble=False
|
1292
|
+
)
|
1293
|
+
|
1294
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
1295
|
+
|
1296
|
+
validate_non_negative(total_time, "total_time", Integral)
|
1297
|
+
|
1298
|
+
return rotation_number(u, parameters, total_time, self.__mapping, mod=mod)
|
1299
|
+
|
1300
|
+
def escape_analysis(
|
1301
|
+
self,
|
1302
|
+
u: Union[NDArray[np.float64], Sequence[float]],
|
1303
|
+
max_time: int,
|
1304
|
+
exits: Union[List[NDArray[np.float64]], NDArray[np.float64]],
|
1305
|
+
parameters: Union[
|
1306
|
+
None, float, Sequence[np.float64], NDArray[np.float64]
|
1307
|
+
] = None,
|
1308
|
+
escape: str = "entering",
|
1309
|
+
hole_size: Optional[float] = None,
|
1310
|
+
) -> Tuple[int, int]:
|
1311
|
+
"""Compute escape basin index and time for a single trajectory.
|
1312
|
+
|
1313
|
+
Parameters
|
1314
|
+
----------
|
1315
|
+
u : Union[NDArray[np.float64], Sequence[float]]
|
1316
|
+
Initial state vector of shape (d,) where d is system dimension.
|
1317
|
+
Can be any sequence convertible to numpy array.
|
1318
|
+
max_time : int
|
1319
|
+
Maximum number of iterations to simulate (must be positive).
|
1320
|
+
exits : Union[List[NDArray[np.float64]], NDArray[np.float64]]
|
1321
|
+
- Exit regions specification:
|
1322
|
+
- List of d arrays of shape (2,) representing [min, max] per dimension
|
1323
|
+
- Array of shape (n_exits, d, 2) for multiple exit regions
|
1324
|
+
parameters : Union[None, float, Sequence[np.float64], NDArray[np.float64]], optional
|
1325
|
+
System parameters of shape (p,) passed to the mapping function.
|
1326
|
+
escape : str, optional
|
1327
|
+
Escape condition type: "entering" or "exiting" (default "entering").
|
1328
|
+
hole_size : Optional[float], optional
|
1329
|
+
Size of the hole (default None, meaning no size constraint). Only used for "entering" escape type.
|
1330
|
+
|
1331
|
+
Returns
|
1332
|
+
-------
|
1333
|
+
Tuple[int, int]
|
1334
|
+
A tuple containing:
|
1335
|
+
|
1336
|
+
- exit_index: 0-based index of escape region (-1 if no escape)
|
1337
|
+
- escape_time: Time step of escape (max_time if no escape)
|
1338
|
+
|
1339
|
+
Raises
|
1340
|
+
------
|
1341
|
+
ValueError
|
1342
|
+
- If `u` is not a scalar, or 1D array, or if its shape does not match the expected system dimension.
|
1343
|
+
- If `parameters` is not None and does not match the expected number of parameters.
|
1344
|
+
- If `parameters` is None but the system expects parameters.
|
1345
|
+
- If `parameters` is a scalar or array-like but not 1D.
|
1346
|
+
- If `max_time` is negative or zero.
|
1347
|
+
- If `exits` is not a list of (d,2) arrays or (n,d,2) array.
|
1348
|
+
- If `escape` is not "entering" or "exiting".
|
1349
|
+
- If exit regions do not match system dimension.
|
1350
|
+
- If exit regions do not provide [min, max] pairs.
|
1351
|
+
TypeError
|
1352
|
+
- If `u` is not a scalar or array-like type.
|
1353
|
+
- If `parameters` is not a scalar or array-like type.
|
1354
|
+
- If `max_time` is not int.
|
1355
|
+
|
1356
|
+
Notes
|
1357
|
+
-----
|
1358
|
+
- For "entering": trajectory must enter the exit region
|
1359
|
+
- For "exiting": trajectory must exit the region of interest
|
1360
|
+
- Exit regions are defined as hyperrectangles [min, max] in each dimension
|
1361
|
+
|
1362
|
+
Examples
|
1363
|
+
--------
|
1364
|
+
>>> # Single exit region (entering)
|
1365
|
+
>>> u0 = np.array([0.1, 0.2])
|
1366
|
+
>>> params = np.array([1.0, 0.1])
|
1367
|
+
>>> exit_region = np.array([[-1, 1], [-1, 1]]) # 2D box
|
1368
|
+
>>> idx, time = sys.escape_analysis(u0, params, 1000, exit_region)
|
1369
|
+
|
1370
|
+
>>> # Multiple exit regions (exiting)
|
1371
|
+
>>> exits = [
|
1372
|
+
... np.array([[0, 1], [0, 1]]), # First exit region
|
1373
|
+
... np.array([[-1, 0], [-1, 0]]) # Second exit region
|
1374
|
+
... ]
|
1375
|
+
>>> idx, time = sys.escape_analysis(u0, params, 1000, exits, "exiting")
|
1376
|
+
"""
|
1377
|
+
|
1378
|
+
u = validate_initial_conditions(
|
1379
|
+
u, self.__system_dimension, allow_ensemble=False
|
1380
|
+
)
|
1381
|
+
|
1382
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
1383
|
+
|
1384
|
+
validate_non_negative(max_time, "max_time", Integral)
|
1385
|
+
|
1386
|
+
# Validate escape type
|
1387
|
+
if escape not in ("entering", "exiting"):
|
1388
|
+
raise ValueError("escape must be either 'entering' or 'exiting'")
|
1389
|
+
|
1390
|
+
if escape == "entering" and hole_size is None:
|
1391
|
+
raise ValueError("hole_size must be specified for 'entering' escape type")
|
1392
|
+
|
1393
|
+
# Process exit regions
|
1394
|
+
if escape == "entering":
|
1395
|
+
# If exits is a list, convert to an array
|
1396
|
+
if isinstance(exits, list):
|
1397
|
+
exits_arr = np.stack(exits, axis=0)
|
1398
|
+
else:
|
1399
|
+
exits_arr = np.asarray(exits, dtype=np.float64)
|
1400
|
+
|
1401
|
+
# If exits is a single point, convert to 2D array
|
1402
|
+
if exits_arr.ndim == 1:
|
1403
|
+
exits_arr = exits_arr.reshape(1, -1)
|
1404
|
+
|
1405
|
+
# Validate exits array shape
|
1406
|
+
if exits_arr.ndim != 2:
|
1407
|
+
raise ValueError(
|
1408
|
+
"Exits must be a list of (d,) arrays or a 2D array of shape (n, d)"
|
1409
|
+
)
|
1410
|
+
|
1411
|
+
# Validate exits dimension
|
1412
|
+
if exits_arr.shape[1] != self.__system_dimension:
|
1413
|
+
raise ValueError(
|
1414
|
+
f"Exit region dimension {exits_arr.shape[1]} != system dimension {self.__system_dimension}"
|
1415
|
+
)
|
1416
|
+
|
1417
|
+
# Create the exit regions as hyperrectangles
|
1418
|
+
# Stack per coordinate axis
|
1419
|
+
lower = exits_arr - hole_size / 2
|
1420
|
+
upper = exits_arr + hole_size / 2
|
1421
|
+
exits_arr = np.stack([lower.T, upper.T], axis=1).transpose(2, 0, 1)
|
1422
|
+
|
1423
|
+
if escape == "exiting":
|
1424
|
+
if isinstance(exits, list):
|
1425
|
+
exits_arr = np.asarray(exits, dtype=np.float64)
|
1426
|
+
else:
|
1427
|
+
exits_arr = np.asarray(exits, dtype=np.float64)
|
1428
|
+
|
1429
|
+
# Validate exits array shape
|
1430
|
+
if exits_arr.ndim != 2 or exits_arr.shape[1] != 2:
|
1431
|
+
raise ValueError(
|
1432
|
+
"Exits must be a 2D array of shape (d, 2) for exiting escape type"
|
1433
|
+
)
|
1434
|
+
|
1435
|
+
# Validate exits dimension
|
1436
|
+
if exits_arr.shape[0] != self.__system_dimension:
|
1437
|
+
raise ValueError(
|
1438
|
+
f"Exit region dimension {exits_arr.shape[0]} != system dimension {self.__system_dimension}"
|
1439
|
+
)
|
1440
|
+
|
1441
|
+
# Dispatch to appropriate computation
|
1442
|
+
if escape == "entering":
|
1443
|
+
return escape_basin_and_time_entering(
|
1444
|
+
u=u,
|
1445
|
+
parameters=parameters,
|
1446
|
+
mapping=self.__mapping,
|
1447
|
+
max_time=max_time,
|
1448
|
+
exits=exits_arr,
|
1449
|
+
)
|
1450
|
+
else:
|
1451
|
+
return escape_time_exiting(
|
1452
|
+
u=u,
|
1453
|
+
parameters=parameters,
|
1454
|
+
mapping=self.__mapping,
|
1455
|
+
max_time=max_time,
|
1456
|
+
region_limits=exits_arr,
|
1457
|
+
)
|
1458
|
+
|
1459
|
+
def survival_probability(
|
1460
|
+
self, escape_times: Union[NDArray[np.int32], Sequence[int]], max_time: np.int32
|
1461
|
+
) -> Tuple[NDArray[np.int64], NDArray[np.float64]]:
|
1462
|
+
"""Compute the survival probability based on escape times.
|
1463
|
+
|
1464
|
+
Parameters
|
1465
|
+
----------
|
1466
|
+
escape_times : Union[NDArray[np.float64], Sequence[int]]
|
1467
|
+
Array of escape times for N trajectories where:
|
1468
|
+
- escape_times[i] = time when i-th trajectory escaped
|
1469
|
+
- Use max_time for trajectories that didn't escape
|
1470
|
+
- Should be shape (N,) with dtype=int32
|
1471
|
+
max_time : int
|
1472
|
+
Maximum simulation time (must be > 0)
|
1473
|
+
|
1474
|
+
Returns
|
1475
|
+
-------
|
1476
|
+
NDArray[np.float64][float64]
|
1477
|
+
Survival probability curve S(t) where:
|
1478
|
+
|
1479
|
+
- S[0] = 1.0 (all trajectories survive at t=0)
|
1480
|
+
- S[t] = fraction surviving at time t
|
1481
|
+
- Shape (max_time + 1,)
|
1482
|
+
|
1483
|
+
Raises
|
1484
|
+
------
|
1485
|
+
ValueError
|
1486
|
+
- If escape_times contains values > max_time
|
1487
|
+
- If escape_times contains negative values
|
1488
|
+
- If max_time <= 0
|
1489
|
+
TypeError
|
1490
|
+
- If escape_times cannot be converted to int32 array
|
1491
|
+
|
1492
|
+
Notes
|
1493
|
+
-----
|
1494
|
+
- S(t) = P(T > t) where T is escape time
|
1495
|
+
- Implemented via survival_probability() function
|
1496
|
+
- For N trajectories: S(t) = (number of T_i > t) / N
|
1497
|
+
|
1498
|
+
Examples
|
1499
|
+
--------
|
1500
|
+
>>> escape_times = np.array([5, 10, 10, 20], dtype=np.int32)
|
1501
|
+
>>> surv = system.compute_survival_probability(escape_times, 20)
|
1502
|
+
>>> surv[0] # 1.0 at t=0
|
1503
|
+
>>> surv[5] # 0.75 at t=5
|
1504
|
+
>>> surv[10] # 0.25 at t=10
|
1505
|
+
>>> surv[20] # 0.0 at t=20
|
1506
|
+
"""
|
1507
|
+
# Input validation
|
1508
|
+
try:
|
1509
|
+
escape_arr = np.asarray(escape_times, dtype=np.int32)
|
1510
|
+
except (TypeError, ValueError) as e:
|
1511
|
+
raise TypeError("escape_times must be convertible to int32 array") from e
|
1512
|
+
|
1513
|
+
if escape_arr.ndim != 1:
|
1514
|
+
raise ValueError("escape_times must be 1D array")
|
1515
|
+
|
1516
|
+
validate_non_negative(max_time, "max_time", Integral)
|
1517
|
+
|
1518
|
+
if np.any(escape_arr < 0):
|
1519
|
+
raise ValueError("escape_times cannot contain negative values")
|
1520
|
+
|
1521
|
+
if np.any(escape_arr > max_time):
|
1522
|
+
raise ValueError(f"escape_times cannot exceed max_time ({max_time})")
|
1523
|
+
|
1524
|
+
# Compute survival probability
|
1525
|
+
return survival_probability(escape_arr, max_time)
|
1526
|
+
|
1527
|
+
def diffusion_coefficient(
|
1528
|
+
self,
|
1529
|
+
u: Union[NDArray[np.float64], Sequence[Sequence[float]]],
|
1530
|
+
total_time: int,
|
1531
|
+
parameters: Union[
|
1532
|
+
None, float, Sequence[np.float64], NDArray[np.float64]
|
1533
|
+
] = None,
|
1534
|
+
axis: int = 1,
|
1535
|
+
) -> np.float64:
|
1536
|
+
"""Compute the diffusion coefficient from ensemble trajectories.
|
1537
|
+
|
1538
|
+
Parameters
|
1539
|
+
----------
|
1540
|
+
u : Union[NDArray[np.float64], Sequence[Sequence[float]]]
|
1541
|
+
Initial conditions array where:
|
1542
|
+
- Shape (N, d) for N trajectories in d-dimensional space
|
1543
|
+
- Can be list of lists or numpy array
|
1544
|
+
total_time : int
|
1545
|
+
Number of iterations to compute (must be ≥ 1)
|
1546
|
+
parameters : Union[None, float, Sequence[np.float64], NDArray[np.float64]], optional
|
1547
|
+
System parameters passed to mapping function, shape (p,)
|
1548
|
+
axis : int, default=1
|
1549
|
+
Coordinate index to compute diffusion (0 for x, 1 for y, etc.)
|
1550
|
+
|
1551
|
+
Returns
|
1552
|
+
-------
|
1553
|
+
float
|
1554
|
+
Diffusion coefficient D calculated as:
|
1555
|
+
D = ⟨(y(t) - y(0))²⟩/(2t) where y is typically the second coordinate and ⟨·⟩ denotes ensemble average
|
1556
|
+
|
1557
|
+
Raises
|
1558
|
+
------
|
1559
|
+
ValueError
|
1560
|
+
- If `u` is not a 2D array, or if its shape does not match the expected system dimension.
|
1561
|
+
- If `parameters` is not None and does not match the expected number of parameters.
|
1562
|
+
- If `parameters` is None but the system expects parameters.
|
1563
|
+
- If `parameters` is a scalar or array-like but not 1D.
|
1564
|
+
- If `total_time` is negative or zero.
|
1565
|
+
- If `axis` is not valid for the system dimension.
|
1566
|
+
TypeError
|
1567
|
+
- If `u` is not a scalar or array-like type.
|
1568
|
+
- If `parameters` is not a scalar or array-like type.
|
1569
|
+
- If `total_time` is not int.
|
1570
|
+
- If `axis` is not int.
|
1571
|
+
|
1572
|
+
Notes
|
1573
|
+
-----
|
1574
|
+
- Uses the system's mapping function for evolution
|
1575
|
+
- For accurate results, use:
|
1576
|
+
- total_time >> 1
|
1577
|
+
- N >> 1 initial conditions
|
1578
|
+
- Implements Einstein relation for discrete time
|
1579
|
+
|
1580
|
+
Examples
|
1581
|
+
--------
|
1582
|
+
>>> # With numpy array input
|
1583
|
+
>>> ics = np.random.rand(100, 2) # 100 trajectories in 2D
|
1584
|
+
>>> params = np.array([0.5, 1.0])
|
1585
|
+
>>> D = system.diffusion_coefficient(ics, params, 1000)
|
1586
|
+
|
1587
|
+
>>> # With list input
|
1588
|
+
>>> ics = [[0.1, 0.2], [0.3, 0.4]] # 2 trajectories
|
1589
|
+
>>> D = system.diffusion_coefficient(ics, params, 500)
|
1590
|
+
"""
|
1591
|
+
|
1592
|
+
u = validate_initial_conditions(u, self.__system_dimension, allow_ensemble=True)
|
1593
|
+
|
1594
|
+
if u.ndim != 2:
|
1595
|
+
raise ValueError(
|
1596
|
+
f"Initial conditions must be a 2D array of shape (N, d), got shape {u.shape}"
|
1597
|
+
)
|
1598
|
+
|
1599
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
1600
|
+
|
1601
|
+
validate_non_negative(total_time, "total_time", Integral)
|
1602
|
+
|
1603
|
+
validate_axis(axis, self.__system_dimension)
|
1604
|
+
|
1605
|
+
return diffusion_coefficient(
|
1606
|
+
u, parameters, total_time, self.__mapping, axis=axis
|
1607
|
+
)
|
1608
|
+
|
1609
|
+
def average_in_time(
|
1610
|
+
self,
|
1611
|
+
u: Union[NDArray[np.float64], Sequence[Sequence[float]]],
|
1612
|
+
total_time: int,
|
1613
|
+
parameters: Union[
|
1614
|
+
None, float, Sequence[np.float64], NDArray[np.float64]
|
1615
|
+
] = None,
|
1616
|
+
sample_times: Optional[Union[NDArray[np.float64], Sequence[int]]] = None,
|
1617
|
+
axis: int = 1,
|
1618
|
+
) -> NDArray[np.float64]:
|
1619
|
+
"""Compute time evolution of coordinate average across trajectories.
|
1620
|
+
|
1621
|
+
Parameters
|
1622
|
+
----------
|
1623
|
+
u : Union[NDArray[np.float64], Sequence[Sequence[float]]]
|
1624
|
+
Initial conditions array where:
|
1625
|
+
- Shape (N, d) for N trajectories in d-dimensional space
|
1626
|
+
- Can be list of lists or numpy array
|
1627
|
+
total_time : int
|
1628
|
+
Total number of iterations to compute (must be ≥ 1)
|
1629
|
+
parameters : Union[None, float, Sequence[np.float64], NDArray[np.float64]], optional
|
1630
|
+
System parameters passed to mapping function, shape (p,)
|
1631
|
+
sample_times : Optional[Union[NDArray[np.float64], Sequence[int]]], default=None
|
1632
|
+
Specific time steps to record (1D array of integers). If None,
|
1633
|
+
records at every time step from 0 to total_time.
|
1634
|
+
axis : int, default=1
|
1635
|
+
Coordinate index to average over (0 for x, 1 for y, etc.)
|
1636
|
+
|
1637
|
+
Returns
|
1638
|
+
-------
|
1639
|
+
NDArray[np.float64]
|
1640
|
+
Array of average values with shape:
|
1641
|
+
|
1642
|
+
- (len(sample_times),) if sample_times provided
|
1643
|
+
- (total_time + 1,) if sample_times=None
|
1644
|
+
|
1645
|
+
Raises
|
1646
|
+
------
|
1647
|
+
ValueError
|
1648
|
+
- If `u` is not a 2D array, or if its shape does not match the expected system dimension.
|
1649
|
+
- If `parameters` is not None and does not match the expected number of parameters.
|
1650
|
+
- If `parameters` is None but the system expects parameters.
|
1651
|
+
- If `parameters` is a scalar or array-like but not 1D.
|
1652
|
+
- If `total_time` is negative or zero.
|
1653
|
+
- If `sample_times` contains invalid values.
|
1654
|
+
- If `sample_times` is not a 1D array of integers.
|
1655
|
+
- If `axis` is not valid for the system dimension.
|
1656
|
+
TypeError
|
1657
|
+
- If `u` is not a scalar or array-like type.
|
1658
|
+
- If `parameters` is not a scalar or array-like type.
|
1659
|
+
- If `total_time` is not int.
|
1660
|
+
- If `axis` is not int.
|
1661
|
+
|
1662
|
+
Notes
|
1663
|
+
-----
|
1664
|
+
- Uses the system's mapping function for trajectory evolution
|
1665
|
+
- For smooth results, use N >> 1 initial conditions
|
1666
|
+
- The average is computed as ⟨xᵢ(t)⟩ where i is the axis index
|
1667
|
+
- First output value (t=0) is the initial average
|
1668
|
+
|
1669
|
+
Examples
|
1670
|
+
--------
|
1671
|
+
>>> # Basic usage with default sampling
|
1672
|
+
>>> ics = np.random.rand(100, 2) # 100 trajectories in 2D
|
1673
|
+
>>> params = np.array([1.0, 0.1])
|
1674
|
+
>>> avg = system.average_in_time(ics, params, 1000)
|
1675
|
+
|
1676
|
+
>>> # With custom sampling times
|
1677
|
+
>>> times = np.linspace(0, 1000, 11, dtype=int)
|
1678
|
+
>>> avg = system.average_in_time(ics, params, 1000, times)
|
1679
|
+
"""
|
1680
|
+
|
1681
|
+
u = validate_initial_conditions(u, self.__system_dimension, allow_ensemble=True)
|
1682
|
+
|
1683
|
+
if u.ndim != 2:
|
1684
|
+
raise ValueError(
|
1685
|
+
f"Initial conditions must be a 2D array of shape (N, d), got shape {u.shape}"
|
1686
|
+
)
|
1687
|
+
|
1688
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
1689
|
+
|
1690
|
+
validate_non_negative(total_time, "total_time", Integral)
|
1691
|
+
|
1692
|
+
sample_times_arr = validate_sample_times(sample_times, total_time)
|
1693
|
+
|
1694
|
+
validate_axis(axis, self.__system_dimension)
|
1695
|
+
|
1696
|
+
return average_vs_time(
|
1697
|
+
u,
|
1698
|
+
parameters,
|
1699
|
+
total_time,
|
1700
|
+
self.__mapping,
|
1701
|
+
sample_times=sample_times_arr,
|
1702
|
+
axis=axis,
|
1703
|
+
)
|
1704
|
+
|
1705
|
+
def cumulative_average(
|
1706
|
+
self,
|
1707
|
+
u: Union[NDArray[np.float64], Sequence[Sequence[float]]],
|
1708
|
+
total_time: int,
|
1709
|
+
parameters: Union[
|
1710
|
+
None, float, Sequence[np.float64], NDArray[np.float64]
|
1711
|
+
] = None,
|
1712
|
+
sample_times: Optional[Union[NDArray[np.float64], Sequence[int]]] = None,
|
1713
|
+
axis: int = 1,
|
1714
|
+
) -> NDArray[np.float64]:
|
1715
|
+
"""Compute cumulative average of a coordinate across trajectories.
|
1716
|
+
|
1717
|
+
Parameters
|
1718
|
+
----------
|
1719
|
+
u : Union[NDArray[np.float64], Sequence[Sequence[float]]]
|
1720
|
+
Initial conditions array where:
|
1721
|
+
- Shape (N, d) for N trajectories in d-dimensional space
|
1722
|
+
- Can be list of lists or numpy array
|
1723
|
+
parameters : Union[None, float, Sequence[np.float64], NDArray[np.float64]], optional
|
1724
|
+
System parameters passed to mapping function, shape (p,)
|
1725
|
+
total_time : int
|
1726
|
+
Total number of iterations to compute (must be ≥ 1)
|
1727
|
+
sample_times : Optional[Union[NDArray[np.float64], Sequence[int]]], default=None
|
1728
|
+
Specific time steps to record (1D array of integers). If None,
|
1729
|
+
records at every time step from 0 to total_time.
|
1730
|
+
axis : int, default=1
|
1731
|
+
Coordinate index to average over (0 for x, 1 for y, etc.)
|
1732
|
+
|
1733
|
+
Returns
|
1734
|
+
-------
|
1735
|
+
NDArray[np.float64]
|
1736
|
+
Array of average values with shape:
|
1737
|
+
|
1738
|
+
- (len(sample_times),) if sample_times provided
|
1739
|
+
- (total_time + 1,) if sample_times=None
|
1740
|
+
|
1741
|
+
Raises
|
1742
|
+
------
|
1743
|
+
ValueError
|
1744
|
+
- If `u` is not a 2D array, or if its shape does not match the expected system dimension.
|
1745
|
+
- If `parameters` is not None and does not match the expected number of parameters.
|
1746
|
+
- If `parameters` is None but the system expects parameters.
|
1747
|
+
- If `parameters` is a scalar or array-like but not 1D.
|
1748
|
+
- If `total_time` is negative or zero.
|
1749
|
+
- If `sample_times` contains invalid values.
|
1750
|
+
- If `sample_times` is not a 1D array of integers.
|
1751
|
+
- If `axis` is not valid for the system dimension.
|
1752
|
+
TypeError
|
1753
|
+
- If `u` is not a scalar or array-like type.
|
1754
|
+
- If `parameters` is not a scalar or array-like type.
|
1755
|
+
- If `total_time` is not int.
|
1756
|
+
- If `axis` is not int.
|
1757
|
+
|
1758
|
+
Notes
|
1759
|
+
-----
|
1760
|
+
- Uses the system's mapping function for trajectory evolution
|
1761
|
+
- For smooth results, use N >> 1 initial conditions
|
1762
|
+
- The average is computed as ⟨xᵢ(t)⟩ where i is the axis index
|
1763
|
+
- First output value (t=0) is the initial average
|
1764
|
+
|
1765
|
+
Examples
|
1766
|
+
--------
|
1767
|
+
>>> # Basic usage with default sampling
|
1768
|
+
>>> ics = np.random.rand(100, 2) # 100 trajectories in 2D
|
1769
|
+
>>> params = np.array([1.0, 0.1])
|
1770
|
+
>>> avg = system.cumulative_average(ics, params, 1000)
|
1771
|
+
|
1772
|
+
>>> # With custom sampling times
|
1773
|
+
>>> times = np.linspace(0, 1000, 11, dtype=int)
|
1774
|
+
>>> avg = system.cumulative_average(ics, params, 1000, times)
|
1775
|
+
"""
|
1776
|
+
|
1777
|
+
u = validate_initial_conditions(u, self.__system_dimension, allow_ensemble=True)
|
1778
|
+
|
1779
|
+
if u.ndim != 2:
|
1780
|
+
raise ValueError(
|
1781
|
+
f"Initial conditions must be a 2D array of shape (N, d), got shape {u.shape}"
|
1782
|
+
)
|
1783
|
+
|
1784
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
1785
|
+
|
1786
|
+
validate_non_negative(total_time, "total_time", Integral)
|
1787
|
+
|
1788
|
+
sample_times_arr = validate_sample_times(sample_times, total_time)
|
1789
|
+
|
1790
|
+
validate_axis(axis, self.__system_dimension)
|
1791
|
+
|
1792
|
+
return cumulative_average_vs_time(
|
1793
|
+
u,
|
1794
|
+
parameters,
|
1795
|
+
total_time,
|
1796
|
+
self.__mapping,
|
1797
|
+
sample_times=sample_times_arr,
|
1798
|
+
axis=axis,
|
1799
|
+
)
|
1800
|
+
|
1801
|
+
def root_mean_squared(
|
1802
|
+
self,
|
1803
|
+
u: Union[NDArray[np.float64], Sequence[Sequence[float]]],
|
1804
|
+
total_time: int,
|
1805
|
+
parameters: Union[
|
1806
|
+
None, float, Sequence[np.float64], NDArray[np.float64]
|
1807
|
+
] = None,
|
1808
|
+
sample_times: Optional[Union[NDArray[np.float64], Sequence[int]]] = None,
|
1809
|
+
axis: int = 1,
|
1810
|
+
) -> NDArray[np.float64]:
|
1811
|
+
"""Compute root mean squared (RMS) evolution of a coordinate across trajectories.
|
1812
|
+
|
1813
|
+
Parameters
|
1814
|
+
----------
|
1815
|
+
u : Union[NDArray[np.float64], Sequence[Sequence[float]]]
|
1816
|
+
Initial conditions array where:
|
1817
|
+
- Shape (N, d) for N trajectories in d-dimensional space
|
1818
|
+
- Can be list of lists or numpy array
|
1819
|
+
total_time : int
|
1820
|
+
Total number of iterations to compute (must be ≥ 1)
|
1821
|
+
parameters : Union[None, float, Sequence[np.float64], NDArray[np.float64]], optional
|
1822
|
+
System parameters passed to mapping function, shape (p,)
|
1823
|
+
Must be 1D float array
|
1824
|
+
sample_times : Optional[Union[NDArray[np.float64], Sequence[int]]], default=None
|
1825
|
+
Specific time steps to record (1D array of integers). If None,
|
1826
|
+
records at every time step from 0 to total_time.
|
1827
|
+
axis : int, default=1
|
1828
|
+
Coordinate index for RMS calculation (0 for x, 1 for y, etc.)
|
1829
|
+
|
1830
|
+
Returns
|
1831
|
+
-------
|
1832
|
+
NDArray[np.float64]
|
1833
|
+
root mean squared values with shape:
|
1834
|
+
|
1835
|
+
- (len(sample_times),) if sample_times provided
|
1836
|
+
- (total_time + 1,) if sample_times=None
|
1837
|
+
|
1838
|
+
Raises
|
1839
|
+
------
|
1840
|
+
ValueError
|
1841
|
+
- If `u` is not a 2D array, or if its shape does not match the expected system dimension.
|
1842
|
+
- If `parameters` is not None and does not match the expected number of parameters.
|
1843
|
+
- If `parameters` is None but the system expects parameters.
|
1844
|
+
- If `parameters` is a scalar or array-like but not 1D.
|
1845
|
+
- If `total_time` is negative or zero.
|
1846
|
+
- If `sample_times` contains invalid values.
|
1847
|
+
- If `sample_times` is not a 1D array of integers.
|
1848
|
+
- If `axis` is not valid for the system dimension.
|
1849
|
+
TypeError
|
1850
|
+
- If `u` is not a scalar or array-like type.
|
1851
|
+
- If `parameters` is not a scalar or array-like type.
|
1852
|
+
- If `total_time` is not int.
|
1853
|
+
- If `axis` is not int.
|
1854
|
+
|
1855
|
+
Notes
|
1856
|
+
-----
|
1857
|
+
- root mean squared is computed as sqrt(⟨xᵢ(t)²⟩) where:
|
1858
|
+
- i is the axis index
|
1859
|
+
- ⟨·⟩ denotes ensemble average
|
1860
|
+
- First output value (t=0) is the initial RMS
|
1861
|
+
- For diffusion analysis, often used with axis=1 (y-coordinate)
|
1862
|
+
|
1863
|
+
Examples
|
1864
|
+
--------
|
1865
|
+
>>> # Basic usage with default sampling
|
1866
|
+
>>> ics = np.random.rand(100, 2) # 100 trajectories in 2D
|
1867
|
+
>>> params = np.array([1.0, 0.1], dtype=np.float64)
|
1868
|
+
>>> rms = system.root_mean_squared(ics, params, 1000)
|
1869
|
+
|
1870
|
+
>>> # With custom sampling times and x-coordinate (axis=0)
|
1871
|
+
>>> times = np.arange(0, 1001, 100, dtype=int)
|
1872
|
+
>>> rms = system.root_mean_squared(ics, params, 1000, times, axis=0)
|
1873
|
+
"""
|
1874
|
+
|
1875
|
+
u = validate_initial_conditions(u, self.__system_dimension, allow_ensemble=True)
|
1876
|
+
|
1877
|
+
if u.ndim != 2:
|
1878
|
+
raise ValueError(
|
1879
|
+
f"Initial conditions must be a 2D array of shape (N, d), got shape {u.shape}"
|
1880
|
+
)
|
1881
|
+
|
1882
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
1883
|
+
|
1884
|
+
validate_non_negative(total_time, "total_time", Integral)
|
1885
|
+
|
1886
|
+
sample_times_arr = validate_sample_times(sample_times, total_time)
|
1887
|
+
|
1888
|
+
validate_axis(axis, self.__system_dimension)
|
1889
|
+
|
1890
|
+
return root_mean_squared(
|
1891
|
+
u,
|
1892
|
+
parameters,
|
1893
|
+
total_time,
|
1894
|
+
self.__mapping,
|
1895
|
+
sample_times=sample_times_arr,
|
1896
|
+
axis=axis,
|
1897
|
+
)
|
1898
|
+
|
1899
|
+
def mean_squared_displacement(
|
1900
|
+
self,
|
1901
|
+
u: Union[NDArray[np.float64], Sequence[Sequence[float]]],
|
1902
|
+
total_time: int,
|
1903
|
+
parameters: Union[
|
1904
|
+
None, float, Sequence[np.float64], NDArray[np.float64]
|
1905
|
+
] = None,
|
1906
|
+
sample_times: Optional[Union[NDArray[np.int32], Sequence[int]]] = None,
|
1907
|
+
axis: int = 1,
|
1908
|
+
) -> NDArray[np.float64]:
|
1909
|
+
"""Compute the Mean Squared Displacement (MSD) for system trajectories.
|
1910
|
+
|
1911
|
+
Parameters
|
1912
|
+
----------
|
1913
|
+
u : Union[NDArray[np.float64], Sequence[Sequence[float]]]
|
1914
|
+
Initial conditions array where:
|
1915
|
+
- Shape (N, d) for N trajectories in d-dimensional space
|
1916
|
+
- Can be list of lists or numpy array
|
1917
|
+
total_time : int
|
1918
|
+
Total number of iterations (must be > transient_time)
|
1919
|
+
parameters : Union[None, float, Sequence[np.float64], NDArray[np.float64]], optional
|
1920
|
+
System parameters of shape (p,) passed to mapping function
|
1921
|
+
sample_times : Optional[Union[NDArray[np.float64], Sequence[int]]], default=None
|
1922
|
+
Specific time steps to record (1D array of integers). If None,
|
1923
|
+
records at every time step after transient_time.
|
1924
|
+
axis : int, default=1
|
1925
|
+
Coordinate index to analyze (0 for x, 1 for y, etc.)
|
1926
|
+
transient_time : Optional[int], default=None
|
1927
|
+
Initial iterations to discard (default: 0 if None)
|
1928
|
+
|
1929
|
+
Returns
|
1930
|
+
-------
|
1931
|
+
NDArray[np.float64]
|
1932
|
+
Mean Squared Displacement values with shape:
|
1933
|
+
|
1934
|
+
- (len(sample_times),) if sample_times provided
|
1935
|
+
- (total_time - transient_time,) if sample_times=None
|
1936
|
+
|
1937
|
+
Raises
|
1938
|
+
------
|
1939
|
+
ValueError
|
1940
|
+
- If `u` is not a 2D array, or if its shape does not match the expected system dimension.
|
1941
|
+
- If `parameters` is not None and does not match the expected number of parameters.
|
1942
|
+
- If `parameters` is None but the system expects parameters.
|
1943
|
+
- If `parameters` is a scalar or array-like but not 1D.
|
1944
|
+
- If `total_time` is negative or zero.
|
1945
|
+
- If `sample_times` contains invalid values.
|
1946
|
+
- If `sample_times` is not a 1D array of integers.
|
1947
|
+
- If `axis` is not valid for the system dimension.
|
1948
|
+
TypeError
|
1949
|
+
- If `u` is not a scalar or array-like type.
|
1950
|
+
- If `parameters` is not a scalar or array-like type.
|
1951
|
+
- If `total_time` is not int.
|
1952
|
+
- If `axis` is not int.
|
1953
|
+
|
1954
|
+
Notes
|
1955
|
+
-----
|
1956
|
+
- Mean Squared Displacement is calculated as ⟨(x_i(t) - x_i(0))²⟩ where ⟨·⟩ is ensemble average
|
1957
|
+
- For normal diffusion, Mean Squared Displacement ∝ t
|
1958
|
+
- For anomalous diffusion, Mean Squared Displacement ∝ t^α (α≠1)
|
1959
|
+
- Uses parallel processing for efficient computation
|
1960
|
+
|
1961
|
+
Examples
|
1962
|
+
--------
|
1963
|
+
>>> # Basic usage with default sampling
|
1964
|
+
>>> ics = np.random.rand(100, 2) # 100 trajectories in 2D
|
1965
|
+
>>> params = np.array([1.0, 0.1])
|
1966
|
+
>>> msd_vals = system.mean_squared_displacement(ics, params, 1000)
|
1967
|
+
|
1968
|
+
>>> # With custom sampling times
|
1969
|
+
>>> times = np.arange(0, 1000, 10, dtype=int)
|
1970
|
+
>>> msd_vals = system.mean_squared_displacement(ics, params, 1000, sample_times=times)
|
1971
|
+
"""
|
1972
|
+
|
1973
|
+
u = validate_initial_conditions(u, self.__system_dimension, allow_ensemble=True)
|
1974
|
+
|
1975
|
+
if u.ndim != 2:
|
1976
|
+
raise ValueError(
|
1977
|
+
f"Initial conditions must be a 2D array of shape (N, d), got shape {u.shape}"
|
1978
|
+
)
|
1979
|
+
|
1980
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
1981
|
+
|
1982
|
+
validate_non_negative(total_time, "total_time", Integral)
|
1983
|
+
|
1984
|
+
sample_times_arr = validate_sample_times(sample_times, total_time)
|
1985
|
+
|
1986
|
+
validate_axis(axis, self.__system_dimension)
|
1987
|
+
|
1988
|
+
return mean_squared_displacement(
|
1989
|
+
u,
|
1990
|
+
parameters,
|
1991
|
+
total_time,
|
1992
|
+
self.__mapping,
|
1993
|
+
sample_times=sample_times_arr,
|
1994
|
+
axis=axis,
|
1995
|
+
)
|
1996
|
+
|
1997
|
+
def ensemble_time_average(
|
1998
|
+
self,
|
1999
|
+
u: Union[NDArray[np.float64], Sequence[Sequence[float]]],
|
2000
|
+
total_time: int,
|
2001
|
+
parameters: Union[
|
2002
|
+
None, float, Sequence[np.float64], NDArray[np.float64]
|
2003
|
+
] = None,
|
2004
|
+
axis: int = 1,
|
2005
|
+
) -> NDArray[np.float64]:
|
2006
|
+
"""Compute ensemble time average of a coordinate across trajectories.
|
2007
|
+
|
2008
|
+
Parameters
|
2009
|
+
----------
|
2010
|
+
u : Union[NDArray[np.float64], Sequence[Sequence[float]]]
|
2011
|
+
Initial conditions array where:
|
2012
|
+
- Shape (N, d) for N trajectories in d-dimensional space
|
2013
|
+
- Can be list of lists or numpy array
|
2014
|
+
total_time : int
|
2015
|
+
Total number of iterations to compute (must be ≥ 1)
|
2016
|
+
parameters : Union[None, float, Sequence[np.float64], NDArray[np.float64]], optional
|
2017
|
+
System parameters passed to mapping function, shape (p,)
|
2018
|
+
axis : int, default=1
|
2019
|
+
Coordinate index to average over (0 for x, 1 for y, etc.)
|
2020
|
+
|
2021
|
+
Returns
|
2022
|
+
-------
|
2023
|
+
NDArray[np.float64]
|
2024
|
+
Array of average values with shape (u.shape[0],)
|
2025
|
+
|
2026
|
+
Raises
|
2027
|
+
------
|
2028
|
+
ValueError
|
2029
|
+
- If `u` is not a 2D array, or if its shape does not match the expected system dimension.
|
2030
|
+
- If `parameters` is not None and does not match the expected number of parameters.
|
2031
|
+
- If `parameters` is None but the system expects parameters.
|
2032
|
+
- If `parameters` is a scalar or array-like but not 1D.
|
2033
|
+
- If `total_time` is negative or zero.
|
2034
|
+
- If `axis` is not valid for the system dimension.
|
2035
|
+
TypeError
|
2036
|
+
- If `u` is not a scalar or array-like type.
|
2037
|
+
- If `parameters` is not a scalar or array-like type.
|
2038
|
+
- If `total_time` is not int.
|
2039
|
+
- If `axis` is not int.
|
2040
|
+
|
2041
|
+
Notes
|
2042
|
+
-----
|
2043
|
+
- Uses the system's mapping function for trajectory evolution
|
2044
|
+
- For smooth results, use N >> 1 initial conditions
|
2045
|
+
- The average is computed as ⟨xᵢ(t)⟩ where i is the axis index
|
2046
|
+
- First output value (t=0) is the initial average
|
2047
|
+
|
2048
|
+
Examples
|
2049
|
+
--------
|
2050
|
+
>>> # Basic usage with default axis (1)
|
2051
|
+
>>> ics = np.random.rand(100, 2) # 100 trajectories in 2D
|
2052
|
+
>>> params = np.array([1.0, 0.1])
|
2053
|
+
>>> avg = system.ensemble_time_average(ics, params, 1000)
|
2054
|
+
>>> # With custom axis (0 for x-coordinate)
|
2055
|
+
>>> avg_x = system.ensemble_time_average(ics, params, 1000, axis=0)
|
2056
|
+
"""
|
2057
|
+
|
2058
|
+
u = validate_initial_conditions(u, self.__system_dimension, allow_ensemble=True)
|
2059
|
+
|
2060
|
+
if u.ndim != 2:
|
2061
|
+
raise ValueError(
|
2062
|
+
f"Initial conditions must be a 2D array of shape (N, d), got shape {u.shape}"
|
2063
|
+
)
|
2064
|
+
|
2065
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
2066
|
+
|
2067
|
+
validate_non_negative(total_time, "total_time", Integral)
|
2068
|
+
|
2069
|
+
validate_axis(axis, self.__system_dimension)
|
2070
|
+
|
2071
|
+
return ensemble_time_average(
|
2072
|
+
u, parameters, self.__mapping, total_time, axis=axis
|
2073
|
+
)
|
2074
|
+
|
2075
|
+
def recurrence_times(
|
2076
|
+
self,
|
2077
|
+
u: Union[NDArray[np.float64], Sequence[float], float],
|
2078
|
+
total_time: int,
|
2079
|
+
parameters: Union[
|
2080
|
+
None, float, Sequence[np.float64], NDArray[np.float64]
|
2081
|
+
] = None,
|
2082
|
+
eps: float = 1e-2,
|
2083
|
+
transient_time: Optional[int] = None,
|
2084
|
+
) -> NDArray[np.float64]:
|
2085
|
+
"""
|
2086
|
+
Compute recurrence times to a neighborhood around the initial condition.
|
2087
|
+
|
2088
|
+
Parameters
|
2089
|
+
----------
|
2090
|
+
u : Union[NDArray[np.float64], list, tuple]
|
2091
|
+
Initial condition vector (shape: `(neq,)`). Will be converted to a contiguous float64 NumPy array.
|
2092
|
+
total_time : int
|
2093
|
+
Total number of iterations to simulate. Must be a positive integer.
|
2094
|
+
parameters : Union[None, float, Sequence[np.float64], NDArray[np.float64]], optional
|
2095
|
+
System parameters passed to the mapping function. Scalars and sequences will be converted automatically.
|
2096
|
+
eps : float, optional
|
2097
|
+
Size of the neighborhood for recurrence detection (default is 1e-2).
|
2098
|
+
Must be a positive number.
|
2099
|
+
transient_time : Optional[int], optional
|
2100
|
+
Initial iterations to discard (default is None, meaning no transient time).
|
2101
|
+
If provided, must be a non-negative integer.
|
2102
|
+
|
2103
|
+
Returns
|
2104
|
+
-------
|
2105
|
+
NDArray[np.float64]
|
2106
|
+
Array of recurrence times (time steps between re-entries into the neighborhood). Returns an empty array if no recurrences occur.
|
2107
|
+
|
2108
|
+
Raises
|
2109
|
+
------
|
2110
|
+
TypeError
|
2111
|
+
- If `u` is not a scalar, or 1D array, or if its shape does not match the expected system dimension.
|
2112
|
+
- If `parameters` is not None and does not match the expected number of parameters.
|
2113
|
+
- If `parameters` is None but the system expects parameters.
|
2114
|
+
- If `parameters` is a scalar or array-like but not 1D.
|
2115
|
+
- If `total_time` is negative.
|
2116
|
+
- If `trasient_time` is negative.
|
2117
|
+
- If `transient_time` is greater than or equal to total_time.
|
2118
|
+
- If `eps` is not a positive float.
|
2119
|
+
TypeError
|
2120
|
+
- If `u` is not a scalar or array-like type.
|
2121
|
+
- If `parameters` is not a scalar or array-like type
|
2122
|
+
- If `total_time` is not int.
|
2123
|
+
- If `transient_time` is not int.
|
2124
|
+
- If `eps` is not float.
|
2125
|
+
|
2126
|
+
|
2127
|
+
Notes
|
2128
|
+
-----
|
2129
|
+
- This method wraps a JIT-compiled function for performance.
|
2130
|
+
- A recurrence is counted when the system state re-enters the axis-aligned hypercube:
|
2131
|
+
[u - eps/2, u + eps/2]^d
|
2132
|
+
- This is commonly used in nonlinear dynamics to study:
|
2133
|
+
- Stickiness
|
2134
|
+
- Poincaré recurrences
|
2135
|
+
- Mixing and ergodicity
|
2136
|
+
|
2137
|
+
Examples
|
2138
|
+
--------
|
2139
|
+
>>> u0 = [0.1, 0.1]
|
2140
|
+
>>> parameters = [0.6, 0.4]
|
2141
|
+
>>> rec_times = system.recurrence_times(u0, parameters, 10000, eps=0.01)
|
2142
|
+
>>> print(rec_times)
|
2143
|
+
array([400, 523, 861, ...])
|
2144
|
+
"""
|
2145
|
+
|
2146
|
+
u = validate_initial_conditions(
|
2147
|
+
u, self.__system_dimension, allow_ensemble=False
|
2148
|
+
)
|
2149
|
+
|
2150
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
2151
|
+
|
2152
|
+
validate_non_negative(total_time, "total_time", Integral)
|
2153
|
+
|
2154
|
+
validate_transient_time(transient_time, total_time, Integral)
|
2155
|
+
|
2156
|
+
validate_non_negative(eps, "eps", Real)
|
2157
|
+
|
2158
|
+
return recurrence_times(
|
2159
|
+
u,
|
2160
|
+
parameters,
|
2161
|
+
total_time,
|
2162
|
+
self.__mapping,
|
2163
|
+
eps,
|
2164
|
+
transient_time=transient_time,
|
2165
|
+
)
|
2166
|
+
|
2167
|
+
def dig(
|
2168
|
+
self,
|
2169
|
+
u: Union[NDArray[np.float64], Sequence[float]],
|
2170
|
+
total_time: int,
|
2171
|
+
parameters: Union[
|
2172
|
+
None, float, Sequence[np.float64], NDArray[np.float64]
|
2173
|
+
] = None,
|
2174
|
+
func: Callable[[NDArray[np.float64]], NDArray[np.float64]] = lambda x: np.cos(
|
2175
|
+
2 * np.pi * x[:, 0]
|
2176
|
+
),
|
2177
|
+
transient_time: Optional[int] = None,
|
2178
|
+
) -> float:
|
2179
|
+
"""Compute the number of zeros after the decimal point of the average
|
2180
|
+
of the observable function over time.
|
2181
|
+
|
2182
|
+
Parameters
|
2183
|
+
----------
|
2184
|
+
u : Union[NDArray[np.float64], Sequence[float]]
|
2185
|
+
Initial condition of shape (d,) where d is system dimension
|
2186
|
+
parameters : Union[None, float, Sequence[np.float64], NDArray[np.float64]], optional
|
2187
|
+
System parameters of shape (p,)
|
2188
|
+
total_time : int
|
2189
|
+
Total iterations to compute (must be even and ≥ 100)
|
2190
|
+
func : Callable[[NDArray[np.float64]], float], optional
|
2191
|
+
Observable function (default: lambda x: np.cos(x[:, 0]))
|
2192
|
+
Should accept a 2D array (sample_size, ndim) and return a 1D array
|
2193
|
+
of shape (sample_size,) with the observable values
|
2194
|
+
transient_time : Optional[int], optional
|
2195
|
+
Initial iterations to discard (default None)
|
2196
|
+
|
2197
|
+
Returns
|
2198
|
+
-------
|
2199
|
+
float
|
2200
|
+
DIG value where:
|
2201
|
+
|
2202
|
+
- Higher values indicate better convergence, i.e., regular dynamics
|
2203
|
+
|
2204
|
+
Raises
|
2205
|
+
------
|
2206
|
+
ValueError
|
2207
|
+
- If `u` is not a scalar, or 1D array, or if its shape does not match the expected system dimension.
|
2208
|
+
- If `parameters` is not None and does not match the expected number of parameters.
|
2209
|
+
- If `parameters` is None but the system expects parameters.
|
2210
|
+
- If `parameters` is a scalar or array-like but not 1D.
|
2211
|
+
- If `total_time` is negative.
|
2212
|
+
- If `trasient_time` is negative.
|
2213
|
+
- If `transient_time` is greater than or equal to total_time.
|
2214
|
+
- If `func` is not callable or does not return a 1D array.
|
2215
|
+
TypeError
|
2216
|
+
- If `u` is not a scalar or array-like type.
|
2217
|
+
- If `parameters` is not a scalar or array-like type.
|
2218
|
+
- If `total_time` is not int.
|
2219
|
+
- If `transient_time` is not int.
|
2220
|
+
|
2221
|
+
Examples
|
2222
|
+
--------
|
2223
|
+
>>> # Using cosine of x-coordinate observable
|
2224
|
+
>>> x_obs = lambda X: cos(X[:, 0])
|
2225
|
+
>>> convergence = system.dig(u0, params, 1000, x_obs)
|
2226
|
+
>>> # Using sin of the sum of x and y coordinates
|
2227
|
+
>>> convergence = system.dig(u0, params, 1000, func=lambda X: sin(X[:, 0] + X[:, 1]))
|
2228
|
+
>>> # With transient period
|
2229
|
+
>>> convergence = system.dig(u0, params, 2000, x_obs, transient_time=500)
|
2230
|
+
"""
|
2231
|
+
|
2232
|
+
u = validate_initial_conditions(
|
2233
|
+
u, self.__system_dimension, allow_ensemble=False
|
2234
|
+
)
|
2235
|
+
|
2236
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
2237
|
+
|
2238
|
+
validate_non_negative(total_time, "total_time", Integral)
|
2239
|
+
|
2240
|
+
if total_time % 2 != 0:
|
2241
|
+
total_time += 1 # Ensure even total_time
|
2242
|
+
|
2243
|
+
validate_transient_time(transient_time, total_time, Integral)
|
2244
|
+
|
2245
|
+
if not callable(func):
|
2246
|
+
raise ValueError("`func` must be a callable function")
|
2247
|
+
if (
|
2248
|
+
not isinstance(func(np.array([u])), np.ndarray)
|
2249
|
+
or func(np.array([u])).ndim != 1
|
2250
|
+
):
|
2251
|
+
raise ValueError("`func` must return a 1D array")
|
2252
|
+
|
2253
|
+
return dig(
|
2254
|
+
u,
|
2255
|
+
parameters,
|
2256
|
+
total_time,
|
2257
|
+
self.__mapping,
|
2258
|
+
func,
|
2259
|
+
transient_time=transient_time,
|
2260
|
+
)
|
2261
|
+
|
2262
|
+
def lyapunov(
|
2263
|
+
self,
|
2264
|
+
u: Union[NDArray[np.float64], Sequence[float], float],
|
2265
|
+
total_time: int,
|
2266
|
+
parameters: Union[
|
2267
|
+
None, float, Sequence[np.float64], NDArray[np.float64]
|
2268
|
+
] = None,
|
2269
|
+
method: str = "QR",
|
2270
|
+
return_history: bool = False,
|
2271
|
+
sample_times: Optional[Union[NDArray[np.int32], Sequence[int]]] = None,
|
2272
|
+
transient_time: Optional[int] = None,
|
2273
|
+
log_base: float = np.e,
|
2274
|
+
) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
|
2275
|
+
"""Compute Lyapunov exponents using specified numerical method.
|
2276
|
+
|
2277
|
+
Parameters
|
2278
|
+
----------
|
2279
|
+
u : Union[NDArray[np.float64], Sequence[float]]
|
2280
|
+
Initial condition(s) of shape (d,) or (n, d) where d is system dimension
|
2281
|
+
total_time : int
|
2282
|
+
Total iterations to compute (default 10000, must be ≥ 1)
|
2283
|
+
parameters : Union[None, float, Sequence[np.float64], NDArray[np.float64]], optional
|
2284
|
+
System parameters of shape (p,) passed to mapping function
|
2285
|
+
method : str, optional
|
2286
|
+
Computation method:
|
2287
|
+
- "QR": QR decomposition
|
2288
|
+
- "QR_HH": Householder QR (more stable)
|
2289
|
+
return_history : bool, optional
|
2290
|
+
If True, returns convergence history (default False)
|
2291
|
+
sample_times : Optional[Union[NDArray[np.float64], Sequence[int]]], optional
|
2292
|
+
Specific times to sample when return_history=True
|
2293
|
+
transient_time : Optional[int], optional
|
2294
|
+
Initial iterations to discard (default None → total_time//10)
|
2295
|
+
log_base : float, optional (default np.e)
|
2296
|
+
Logarithm base for exponents (e.g. e, 2, or 10)
|
2297
|
+
|
2298
|
+
Returns
|
2299
|
+
-------
|
2300
|
+
Union[Tuple[NDArray[np.float64], NDArray[np.float64]],
|
2301
|
+
Tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]]
|
2302
|
+
|
2303
|
+
- If return_history=False: exponents
|
2304
|
+
- If return_history=True: history
|
2305
|
+
|
2306
|
+
Raises
|
2307
|
+
------
|
2308
|
+
ValueError
|
2309
|
+
- If `u` is not a scalar, or 1D array, or if its shape does not match the expected system dimension.
|
2310
|
+
- If `parameters` is not None and does not match the expected number of parameters.
|
2311
|
+
- If `parameters` is None but the system expects parameters.
|
2312
|
+
- If `parameters` is a scalar or array-like but not 1D.
|
2313
|
+
- If `total_time` is negative.
|
2314
|
+
- If `trasient_time` is negative.
|
2315
|
+
- If `transient_time` is greater than or equal to total_time.
|
2316
|
+
- If `method` is not "QR" or "QR_HH".
|
2317
|
+
- If `sample_times` is not a 1D array of integers.
|
2318
|
+
- If `log_base` is not positive
|
2319
|
+
TypeError
|
2320
|
+
- If `u` is not a scalar or array-like type.
|
2321
|
+
- If `parameters` is not a scalar or array-like type.
|
2322
|
+
- If `total_time` is not int.
|
2323
|
+
- If `transient_time` is not int.
|
2324
|
+
- If `log_base` is not float.
|
2325
|
+
- If sample_times cannot be converted to a 1D array of integers.
|
2326
|
+
- If `method` is not a string.
|
2327
|
+
|
2328
|
+
Notes
|
2329
|
+
-----
|
2330
|
+
- ER method is fastest for 2D systems
|
2331
|
+
- QR methods are more stable for higher dimensions
|
2332
|
+
- Sample times are automatically sorted and deduplicated
|
2333
|
+
- Final exponents are averaged over last 10% of iterations
|
2334
|
+
|
2335
|
+
References
|
2336
|
+
----------
|
2337
|
+
[1] Eckmann & Ruelle, Rev. Mod. Phys 57, 617 (1985)
|
2338
|
+
[2] Wolf et al., Physica 16D 285-317 (1985)
|
2339
|
+
|
2340
|
+
Examples
|
2341
|
+
--------
|
2342
|
+
>>> # Basic 2D system with ER method
|
2343
|
+
>>> u0 = np.array([0.1, 0.2])
|
2344
|
+
>>> params = np.array([0.5, 1.0])
|
2345
|
+
>>> lyapunov_exponents = system.lyapunov(u0, 10000,
|
2346
|
+
... parameters=params)
|
2347
|
+
|
2348
|
+
>>> # With convergence history
|
2349
|
+
>>> lyapunov_exponents = system.lyapunov(u0, 10000,
|
2350
|
+
... parameters=params, return_history=True)
|
2351
|
+
>>> # Using Householder QR for better stability
|
2352
|
+
>>> lyapunov_exponents = system.lyapunov(u0, 10000,
|
2353
|
+
... parameters=params, method="QR_HH", return_history=True)
|
2354
|
+
>>> # With transient time and logarithm base 10
|
2355
|
+
>>> lyapunov_exponents = system.lyapunov(u0, 10000,
|
2356
|
+
... parameters=params, transient_time=1000,
|
2357
|
+
... log_base=10.0, return_history=True)
|
2358
|
+
"""
|
2359
|
+
|
2360
|
+
u = validate_initial_conditions(
|
2361
|
+
u, self.__system_dimension, allow_ensemble=False
|
2362
|
+
)
|
2363
|
+
|
2364
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
2365
|
+
|
2366
|
+
validate_non_negative(total_time, "total_time", Integral)
|
2367
|
+
validate_transient_time(transient_time, total_time, Integral)
|
2368
|
+
|
2369
|
+
# Validate method
|
2370
|
+
if not isinstance(method, str):
|
2371
|
+
raise TypeError("method must be a string")
|
2372
|
+
method = method.upper()
|
2373
|
+
if method not in ("QR", "QR_HH"):
|
2374
|
+
raise ValueError("method must be 'QR' or 'QR_HH'")
|
2375
|
+
|
2376
|
+
# Validate method for system dimension
|
2377
|
+
if method == "QR" and self.__system_dimension == 2:
|
2378
|
+
method = "ER" # Fallback to QR for higher dimensions
|
2379
|
+
|
2380
|
+
sample_times = validate_sample_times(sample_times, total_time)
|
2381
|
+
|
2382
|
+
validate_non_negative(log_base, "log_base", Real)
|
2383
|
+
if log_base == 1:
|
2384
|
+
raise ValueError("The logarithm function is not defined with base 1.")
|
2385
|
+
|
2386
|
+
# Dispatch to appropriate computation
|
2387
|
+
if self.__system_dimension == 1:
|
2388
|
+
compute_func = lyapunov_1D
|
2389
|
+
else:
|
2390
|
+
if method == "ER":
|
2391
|
+
compute_func = lyapunov_er
|
2392
|
+
elif method == "QR":
|
2393
|
+
compute_func = lyapunov_qr
|
2394
|
+
else: # QR_HH
|
2395
|
+
compute_func = lambda *args, **kwargs: lyapunov_qr(
|
2396
|
+
*args, QR=householder_qr, **kwargs
|
2397
|
+
)
|
2398
|
+
|
2399
|
+
result = compute_func(
|
2400
|
+
u,
|
2401
|
+
parameters,
|
2402
|
+
total_time,
|
2403
|
+
self.__mapping,
|
2404
|
+
self.__jacobian,
|
2405
|
+
return_history=return_history,
|
2406
|
+
sample_times=sample_times,
|
2407
|
+
transient_time=transient_time,
|
2408
|
+
log_base=log_base,
|
2409
|
+
)
|
2410
|
+
|
2411
|
+
if return_history:
|
2412
|
+
return result[0]
|
2413
|
+
else:
|
2414
|
+
return result[0][:, 0] if self.__system_dimension > 1 else result[0]
|
2415
|
+
|
2416
|
+
def finite_time_lyapunov(
|
2417
|
+
self,
|
2418
|
+
u: Union[NDArray[np.float64], Sequence[float], float],
|
2419
|
+
total_time: int,
|
2420
|
+
finite_time: int,
|
2421
|
+
parameters: Union[
|
2422
|
+
None, float, Sequence[np.float64], NDArray[np.float64]
|
2423
|
+
] = None,
|
2424
|
+
method: str = "QR",
|
2425
|
+
transient_time: Optional[int] = None,
|
2426
|
+
log_base: float = np.e,
|
2427
|
+
return_points: bool = False,
|
2428
|
+
) -> Union[NDArray[np.float64], Tuple[NDArray[np.float64], NDArray[np.float64]]]:
|
2429
|
+
"""Compute finite-time Lyapunov exponents (FTLE) along trajectory.
|
2430
|
+
|
2431
|
+
Parameters
|
2432
|
+
----------
|
2433
|
+
u : Union[NDArray[np.float64], Sequence[float]]
|
2434
|
+
Initial condition of shape (d,) where d is system dimension
|
2435
|
+
total_time : int
|
2436
|
+
Total simulation time steps (must be > finite_time, default 10000)
|
2437
|
+
finite_time : int
|
2438
|
+
Averaging window size in time steps (default 100)
|
2439
|
+
parameters : Union[None, float, Sequence[np.float64], NDArray[np.float64]], optional
|
2440
|
+
System parameters of shape (p,) passed to mapping function
|
2441
|
+
method : str, optional
|
2442
|
+
Computation method:
|
2443
|
+
- "ER": Eckmann-Ruelle (optimal for 2D systems)
|
2444
|
+
- "QR": Gram-Schmidt QR decomposition
|
2445
|
+
- "QR_HH": Householder QR (more stable)
|
2446
|
+
transient_time : Optional[int], optional
|
2447
|
+
Initial burn-in period to discard (default None → finite_time)
|
2448
|
+
|
2449
|
+
Returns
|
2450
|
+
-------
|
2451
|
+
NDArray[np.float64]
|
2452
|
+
FTLE matrix of shape (n_windows, d) where:
|
2453
|
+
|
2454
|
+
- n_windows = (total_time - transient_time) // finite_time
|
2455
|
+
- Each row contains exponents for one time window
|
2456
|
+
- Columns are ordered by decreasing exponent magnitude
|
2457
|
+
|
2458
|
+
Raises
|
2459
|
+
------
|
2460
|
+
ValueError
|
2461
|
+
- If `u` is not a scalar, or 1D array, or if its shape does not match the expected system dimension.
|
2462
|
+
- If `parameters` is not None and does not match the expected number of parameters.
|
2463
|
+
- If `parameters` is None but the system expects parameters.
|
2464
|
+
- If `parameters` is a scalar or array-like but not 1D.
|
2465
|
+
- If `total_time` is negative.
|
2466
|
+
- If `finite_time` is negative or zero.
|
2467
|
+
- If `trasient_time` is negative.
|
2468
|
+
- If `transient_time` is greater than or equal to total_time.
|
2469
|
+
- If `method` is not "QR" or "QR_HH".
|
2470
|
+
- If `log_base` is not positive
|
2471
|
+
TypeError
|
2472
|
+
- If `u` is not a scalar or array-like type.
|
2473
|
+
- If `parameters` is not a scalar or array-like type.
|
2474
|
+
- If `total_time` is not int.
|
2475
|
+
- If `transient_time` is not int.
|
2476
|
+
- If `log_base` is not float.
|
2477
|
+
- If `method` is not a string.
|
2478
|
+
- If `return_points` is not a boolean.
|
2479
|
+
|
2480
|
+
Notes
|
2481
|
+
-----
|
2482
|
+
- FTLE measure local stretching rates over finite intervals
|
2483
|
+
- For chaotic systems, FTLE → true exponents as finite_time → ∞
|
2484
|
+
- ER method is faster but limited to 2D systems
|
2485
|
+
- Results are more reliable when:
|
2486
|
+
- finite_time >> 1
|
2487
|
+
- (total_time - transient_time) // finite_time >> 1
|
2488
|
+
|
2489
|
+
Examples
|
2490
|
+
--------
|
2491
|
+
>>> # Basic usage with defaults
|
2492
|
+
>>> u0 = np.array([0.1, 0.2])
|
2493
|
+
>>> params = np.array([0.5, 1.0])
|
2494
|
+
>>> ftle = system.finite_time_lyapunov_exponents(u0, params)
|
2495
|
+
|
2496
|
+
>>> # With custom parameters
|
2497
|
+
>>> ftle = system.finite_time_lyapunov_exponents(
|
2498
|
+
... u0, params,
|
2499
|
+
... total_time=5000,
|
2500
|
+
... finite_time=50,
|
2501
|
+
... method="GS"
|
2502
|
+
... )
|
2503
|
+
"""
|
2504
|
+
|
2505
|
+
u = validate_initial_conditions(
|
2506
|
+
u, self.__system_dimension, allow_ensemble=False
|
2507
|
+
)
|
2508
|
+
|
2509
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
2510
|
+
|
2511
|
+
validate_non_negative(total_time, "total_time", Integral)
|
2512
|
+
validate_positive(finite_time, "finite_time", Integral)
|
2513
|
+
validate_finite_time(finite_time, total_time)
|
2514
|
+
validate_transient_time(transient_time, total_time, Integral)
|
2515
|
+
|
2516
|
+
# Validate method
|
2517
|
+
if not isinstance(method, str):
|
2518
|
+
raise TypeError("method must be a string")
|
2519
|
+
method = method.upper()
|
2520
|
+
if method not in ("QR", "QR_HH"):
|
2521
|
+
raise ValueError("method must be 'QR' or 'QR_HH'")
|
2522
|
+
|
2523
|
+
# Validate method for system dimension
|
2524
|
+
if method == "QR" and self.__system_dimension == 2:
|
2525
|
+
method = "ER" # Fallback to QR for higher dimensions
|
2526
|
+
|
2527
|
+
validate_non_negative(log_base, "log_base", Real)
|
2528
|
+
if log_base == 1:
|
2529
|
+
raise ValueError("The logarithm function is not defined with base 1.")
|
2530
|
+
|
2531
|
+
if not isinstance(return_points, bool):
|
2532
|
+
raise TypeError("return_points must be a boolean")
|
2533
|
+
|
2534
|
+
return finite_time_lyapunov(
|
2535
|
+
u,
|
2536
|
+
parameters,
|
2537
|
+
total_time,
|
2538
|
+
finite_time,
|
2539
|
+
self.__mapping,
|
2540
|
+
self.__jacobian,
|
2541
|
+
method=method,
|
2542
|
+
transient_time=transient_time,
|
2543
|
+
log_base=log_base,
|
2544
|
+
return_points=return_points,
|
2545
|
+
)
|
2546
|
+
|
2547
|
+
def hurst_exponent(
|
2548
|
+
self,
|
2549
|
+
u: Union[NDArray[np.float64], Sequence[float], float],
|
2550
|
+
total_time: int,
|
2551
|
+
parameters: Union[
|
2552
|
+
None, float, Sequence[np.float64], NDArray[np.float64]
|
2553
|
+
] = None,
|
2554
|
+
wmin: int = 2,
|
2555
|
+
transient_time: Optional[int] = None,
|
2556
|
+
) -> NDArray[np.float64]:
|
2557
|
+
"""
|
2558
|
+
Estimate the Hurst exponent for a system trajectory using the rescaled range (R/S) method.
|
2559
|
+
|
2560
|
+
Parameters
|
2561
|
+
----------
|
2562
|
+
u : NDArray[np.float64]
|
2563
|
+
Initial condition vector of shape (n,).
|
2564
|
+
parameters : Union[None, float, Sequence[np.float64], NDArray[np.float64]], optional
|
2565
|
+
Parameters passed to the mapping function.
|
2566
|
+
total_time : int
|
2567
|
+
Total number of iterations used to generate the trajectory.
|
2568
|
+
mapping : Callable[[NDArray[np.float64],
|
2569
|
+
NDArray[np.float64]], NDArray[np.float64]]
|
2570
|
+
A function that defines the system dynamics, i.e., how `u` evolves over time given `parameters`.
|
2571
|
+
wmin : int, optional
|
2572
|
+
Minimum window size for the rescaled range calculation. Default is 2.
|
2573
|
+
transient_time : Optional[int], optional
|
2574
|
+
Number of initial iterations to discard as transient. If `None`, no transient is removed. Default is `None`.
|
2575
|
+
|
2576
|
+
Returns
|
2577
|
+
-------
|
2578
|
+
NDArray[np.float64]
|
2579
|
+
Estimated Hurst exponents for each dimension of the input vector `u`, of shape (n,).
|
2580
|
+
|
2581
|
+
Raises
|
2582
|
+
------
|
2583
|
+
ValueError
|
2584
|
+
- If `u` is not a 2D array, or if its shape does not match the expected system dimension.
|
2585
|
+
- If `parameters` is not None and does not match the expected number of parameters.
|
2586
|
+
- If `parameters` is None but the system expects parameters.
|
2587
|
+
- If `parameters` is a scalar or array-like but not 1D.
|
2588
|
+
- If `total_time` is negative or zero.
|
2589
|
+
- If `transient_time` is negative or greater than or equal to `total_time`.
|
2590
|
+
- If `wmin` is not a positive integer or is less than 2 or greater than total_time // 2.
|
2591
|
+
|
2592
|
+
TypeError
|
2593
|
+
- If `u` is not a scalar or array-like type.
|
2594
|
+
- If `parameters` is not a scalar or array-like type.
|
2595
|
+
- If `total_time` is not int.
|
2596
|
+
- If `wmin` is not a positive integer.
|
2597
|
+
|
2598
|
+
Notes
|
2599
|
+
-----
|
2600
|
+
The Hurst exponent is a measure of the long-term memory of a time series:
|
2601
|
+
|
2602
|
+
- H = 0.5 indicates a random walk (no memory).
|
2603
|
+
- H > 0.5 indicates persistent behavior (positive autocorrelation).
|
2604
|
+
- H < 0.5 indicates anti-persistent behavior (negative autocorrelation).
|
2605
|
+
|
2606
|
+
This implementation computes the rescaled range (R/S) for various window sizes and
|
2607
|
+
performs a linear regression in log-log space to estimate the exponent.
|
2608
|
+
|
2609
|
+
The function supports multivariate time series, estimating one Hurst exponent per dimension.
|
2610
|
+
"""
|
2611
|
+
|
2612
|
+
u = validate_initial_conditions(
|
2613
|
+
u, self.__system_dimension, allow_ensemble=False
|
2614
|
+
)
|
2615
|
+
|
2616
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
2617
|
+
|
2618
|
+
validate_non_negative(total_time, "total_time", Integral)
|
2619
|
+
validate_transient_time(transient_time, total_time, Integral)
|
2620
|
+
|
2621
|
+
validate_positive(wmin, "wmin", Integral)
|
2622
|
+
if wmin < 2 or wmin >= total_time // 2:
|
2623
|
+
raise ValueError(
|
2624
|
+
f"`wmin` must be an integer >= 2 and <= total_time / 2. Got {wmin}."
|
2625
|
+
)
|
2626
|
+
|
2627
|
+
return hurst_exponent(
|
2628
|
+
u,
|
2629
|
+
parameters,
|
2630
|
+
total_time,
|
2631
|
+
self.__mapping,
|
2632
|
+
wmin=wmin,
|
2633
|
+
transient_time=transient_time,
|
2634
|
+
)
|
2635
|
+
|
2636
|
+
def finite_time_hurst_exponent(
|
2637
|
+
self,
|
2638
|
+
u: Union[NDArray[np.float64], Sequence[float], float],
|
2639
|
+
total_time: int,
|
2640
|
+
finite_time: int,
|
2641
|
+
parameters: Union[
|
2642
|
+
None, float, Sequence[np.float64], NDArray[np.float64]
|
2643
|
+
] = None,
|
2644
|
+
wmin: int = 2,
|
2645
|
+
return_points: bool = False,
|
2646
|
+
) -> Union[NDArray[np.float64], Tuple[NDArray[np.float64], NDArray[np.float64]]]:
|
2647
|
+
"""Compute finite-time Hurst exponent along a trajectory.
|
2648
|
+
|
2649
|
+
Parameters
|
2650
|
+
----------
|
2651
|
+
u : Union[NDArray[np.float64], Sequence[float]]
|
2652
|
+
Initial condition of shape (d,) where d is system dimension
|
2653
|
+
total_time : int
|
2654
|
+
Total simulation time steps (must be > finite_time
|
2655
|
+
finite_time : int
|
2656
|
+
Averaging window size in time steps
|
2657
|
+
parameters : Union[None, float, Sequence[np.float64], NDArray[np.float64]], optional
|
2658
|
+
System parameters of shape (p,) passed to mapping function
|
2659
|
+
wmin : int, optional
|
2660
|
+
Minimum window size for the rescaled range calculation (default 2)
|
2661
|
+
return_points : bool, optional
|
2662
|
+
If True, returns full evolution (default False)
|
2663
|
+
|
2664
|
+
Returns
|
2665
|
+
-------
|
2666
|
+
Union[NDArray[np.float64], Tuple[NDArray[np.float64], NDArray[np.float64]]]
|
2667
|
+
- If return_points=False: Hurst exponent(scalar)
|
2668
|
+
- If return_points=True: Tuple of (Hurst history, final state) where Hurst history is 1D array of values
|
2669
|
+
|
2670
|
+
Raises
|
2671
|
+
------
|
2672
|
+
ValueError
|
2673
|
+
- If `u` is not a scalar, or 1D array, or if its shape does not match the expected system dimension.
|
2674
|
+
- If `parameters` is not None and does not match the expected number of parameters.
|
2675
|
+
- If `parameters` is None but the system expects parameters.
|
2676
|
+
- If `parameters` is a scalar or array-like but not 1D.
|
2677
|
+
- If `total_time` is negative.
|
2678
|
+
- If `finite_time` is negative or zero.
|
2679
|
+
- If `trasient_time` is negative.
|
2680
|
+
- If `transient_time` is greater than or equal to total_time.
|
2681
|
+
- If `wmin` is not a positive integer or is less than 2 or greater than total_time // 2.
|
2682
|
+
|
2683
|
+
TypeError
|
2684
|
+
- If `u` is not a scalar or array-like type.
|
2685
|
+
- If `parameters` is not a scalar or array-like type.
|
2686
|
+
- If `total_time` is not int.
|
2687
|
+
- If `finite_time` is not int.
|
2688
|
+
- If `wmin` is not a positive integer.
|
2689
|
+
- If `return_points` is not a boolean.
|
2690
|
+
|
2691
|
+
Notes
|
2692
|
+
-----
|
2693
|
+
- Finite-time Hurst exponent measures local scaling behavior over finite intervals
|
2694
|
+
- For chaotic systems, FTHE → true exponents as finite_time → ∞
|
2695
|
+
- Results are more reliable when:
|
2696
|
+
- finite_time >> 1
|
2697
|
+
- (total_time - transient_time) // finite_time >> 1
|
2698
|
+
|
2699
|
+
Examples
|
2700
|
+
--------
|
2701
|
+
>>> # Basic usage with defaults
|
2702
|
+
>>> u0 = np.array([0.1, 0.2])
|
2703
|
+
>>> params = np.array([0.5, 1.0])
|
2704
|
+
>>> fthe = system.finite_time_hurst_exponent(u0, 100000, 100, parameters=params)
|
2705
|
+
|
2706
|
+
"""
|
2707
|
+
|
2708
|
+
u = validate_initial_conditions(
|
2709
|
+
u, self.__system_dimension, allow_ensemble=False
|
2710
|
+
)
|
2711
|
+
|
2712
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
2713
|
+
|
2714
|
+
validate_non_negative(total_time, "total_time", Integral)
|
2715
|
+
validate_positive(finite_time, "finite_time", Integral)
|
2716
|
+
validate_finite_time(finite_time, total_time)
|
2717
|
+
|
2718
|
+
return finite_time_hurst_exponent(
|
2719
|
+
u,
|
2720
|
+
parameters,
|
2721
|
+
total_time,
|
2722
|
+
finite_time,
|
2723
|
+
self.__mapping,
|
2724
|
+
wmin=wmin,
|
2725
|
+
return_points=return_points,
|
2726
|
+
)
|
2727
|
+
|
2728
|
+
def SALI(
|
2729
|
+
self,
|
2730
|
+
u: Union[NDArray[np.float64], Sequence[float]],
|
2731
|
+
total_time: int,
|
2732
|
+
parameters: Union[
|
2733
|
+
None, float, Sequence[np.float64], NDArray[np.float64]
|
2734
|
+
] = None,
|
2735
|
+
return_history: bool = False,
|
2736
|
+
sample_times: Optional[Union[NDArray[np.int32], Sequence[int]]] = None,
|
2737
|
+
tol: float = 1e-16,
|
2738
|
+
transient_time: Optional[int] = None,
|
2739
|
+
seed: int = 13,
|
2740
|
+
) -> Union[NDArray[np.float64], Tuple[NDArray[np.float64], NDArray[np.float64]]]:
|
2741
|
+
"""Compute Smallest Alignment Index(SALI) for chaos detection.
|
2742
|
+
|
2743
|
+
Parameters
|
2744
|
+
----------
|
2745
|
+
u: Union[NDArray[np.float64], Sequence[float]]
|
2746
|
+
Initial condition of shape(d,) where d is system dimension
|
2747
|
+
total_time: int
|
2748
|
+
Maximum number of iterations(must be ≥ 1)
|
2749
|
+
parameters: Union[None, float, Sequence[np.float64], NDArray[np.float64]], optional
|
2750
|
+
System parameters of shape(p,) passed to mapping function
|
2751
|
+
return_history: bool, optional
|
2752
|
+
If True, returns full evolution(default False)
|
2753
|
+
sample_times: Optional[Union[NDArray[np.float64], Sequence[int]]], optional
|
2754
|
+
Specific times to sample(must be sorted, default None)
|
2755
|
+
tol: float, optional
|
2756
|
+
Early termination threshold(default 1e-16)
|
2757
|
+
transient_time: Optional[int], optional
|
2758
|
+
Initial iterations to discard(default None → total_time//10)
|
2759
|
+
seed: int, optional
|
2760
|
+
Random seed for reproducibility (default 13)
|
2761
|
+
|
2762
|
+
Returns
|
2763
|
+
-------
|
2764
|
+
Union[NDArray[np.float64], Tuple[NDArray[np.float64], NDArray[np.float64]]]
|
2765
|
+
- If return_history = False: Final SALI value(scalar)
|
2766
|
+
- If return_history = True: Tuple of(SALI_history, final_state) where SALI_history is 1D array of values
|
2767
|
+
|
2768
|
+
Raises
|
2769
|
+
------
|
2770
|
+
ValueError
|
2771
|
+
- If `u` is not an 1D array, or if its shape does not match the expected system dimension.
|
2772
|
+
- If `parameters` is not None and does not match the expected number of parameters.
|
2773
|
+
- If `parameters` is None but the system expects parameters.
|
2774
|
+
- If `parameters` is a scalar or array-like but not 1D.
|
2775
|
+
- If `total_time` is negative.
|
2776
|
+
- If `trasient_time` is negative.
|
2777
|
+
- If `transient_time` is greater than or equal to total_time.
|
2778
|
+
- If `sample_times` is not a 1D array of integers.
|
2779
|
+
TypeError
|
2780
|
+
- If `u` is not a scalar or array-like type.
|
2781
|
+
- If `parameters` is not a scalar or array-like type.
|
2782
|
+
- If `total_time` is not int.
|
2783
|
+
- If `transient_time` is not int.
|
2784
|
+
- If sample_times cannot be converted to a 1D array of integers.
|
2785
|
+
- If `tol` is not a positive float.
|
2786
|
+
- If `seed` is not an integer.
|
2787
|
+
|
2788
|
+
Notes
|
2789
|
+
-----
|
2790
|
+
- SALI behavior:
|
2791
|
+
- → 0 exponentially for chaotic orbits
|
2792
|
+
- → positive constant for regular orbits
|
2793
|
+
- Typical threshold: SALI < 1e-8 suggests chaos
|
2794
|
+
- For Hamiltonian systems, uses 2 deviation vectors
|
2795
|
+
- Early termination when SALI < tol
|
2796
|
+
|
2797
|
+
Examples
|
2798
|
+
--------
|
2799
|
+
>>> # Basic usage (final value only)
|
2800
|
+
>>> u0 = np.array([0.1, 0.2])
|
2801
|
+
>>> params = np.array([0.5, 1.0])
|
2802
|
+
>>> sali = system.SALI(u0, params, 10000)
|
2803
|
+
|
2804
|
+
>>> # With full history
|
2805
|
+
>>> sali_hist, final = system.SALI(
|
2806
|
+
... u0, params, 10000, return_history=True)
|
2807
|
+
|
2808
|
+
>>> # With custom sampling
|
2809
|
+
>>> times = np.array([100, 1000, 5000])
|
2810
|
+
>>> sali_samples, _ = system.SALI(
|
2811
|
+
... u0, params, 10000, sample_times=times, return_history=True)
|
2812
|
+
"""
|
2813
|
+
|
2814
|
+
u = validate_initial_conditions(
|
2815
|
+
u, self.__system_dimension, allow_ensemble=False
|
2816
|
+
)
|
2817
|
+
|
2818
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
2819
|
+
|
2820
|
+
validate_non_negative(total_time, "total_time", Integral)
|
2821
|
+
validate_transient_time(transient_time, total_time, Integral)
|
2822
|
+
|
2823
|
+
sample_times = validate_sample_times(sample_times, total_time)
|
2824
|
+
|
2825
|
+
validate_non_negative(tol, "tol", Real)
|
2826
|
+
|
2827
|
+
if not isinstance(seed, Integral):
|
2828
|
+
raise TypeError("seed must be an integer")
|
2829
|
+
|
2830
|
+
result = SALI(
|
2831
|
+
u,
|
2832
|
+
parameters,
|
2833
|
+
total_time,
|
2834
|
+
self.__mapping,
|
2835
|
+
self.__jacobian,
|
2836
|
+
return_history=return_history,
|
2837
|
+
sample_times=sample_times,
|
2838
|
+
transient_time=transient_time,
|
2839
|
+
tol=tol,
|
2840
|
+
seed=seed,
|
2841
|
+
)
|
2842
|
+
|
2843
|
+
return result if return_history else result[0]
|
2844
|
+
|
2845
|
+
def LDI(
|
2846
|
+
self,
|
2847
|
+
u: Union[NDArray[np.float64], Sequence[float]],
|
2848
|
+
total_time: int,
|
2849
|
+
k: int,
|
2850
|
+
parameters: Union[
|
2851
|
+
None, float, Sequence[np.float64], NDArray[np.float64]
|
2852
|
+
] = None,
|
2853
|
+
return_history: bool = False,
|
2854
|
+
sample_times: Optional[Union[NDArray[np.int32], Sequence[int]]] = None,
|
2855
|
+
tol: float = 1e-16,
|
2856
|
+
transient_time: Optional[int] = None,
|
2857
|
+
seed: int = 13,
|
2858
|
+
) -> Union[NDArray[np.float64], Tuple[NDArray[np.float64], NDArray[np.float64]]]:
|
2859
|
+
"""Compute Linear Dependence Index(LDI_k) for chaos detection.
|
2860
|
+
|
2861
|
+
Parameters
|
2862
|
+
----------
|
2863
|
+
u: Union[NDArray[np.float64], Sequence[float]]
|
2864
|
+
Initial condition of shape(d,) where d is system dimension
|
2865
|
+
total_time: int
|
2866
|
+
Maximum number of iterations(must be ≥ 1)
|
2867
|
+
k: int
|
2868
|
+
Number of deviation vectors to use(2 ≤ k ≤ d, default 2)
|
2869
|
+
parameters: Union[None, float, Sequence[np.float64], NDArray[np.float64]], optional
|
2870
|
+
System parameters of shape(p,) passed to mapping function
|
2871
|
+
return_history: bool, optional
|
2872
|
+
If True, returns full evolution(default False)
|
2873
|
+
sample_times: Optional[Union[NDArray[np.float64], Sequence[int]]], optional
|
2874
|
+
Specific times to sample(must be sorted, default None)
|
2875
|
+
tol: float, optional
|
2876
|
+
Early termination threshold(default 1e-16)
|
2877
|
+
transient_time: Optional[int], optional
|
2878
|
+
Initial iterations to discard(default None → total_time//10)
|
2879
|
+
seed: int, optional
|
2880
|
+
Random seed for reproducibility(default 13)
|
2881
|
+
|
2882
|
+
Returns
|
2883
|
+
-------
|
2884
|
+
Union[NDArray[np.float64], Tuple[NDArray[np.float64], NDArray[np.float64]]]
|
2885
|
+
- If return_history = False: Final LDI_k value(scalar)
|
2886
|
+
- If return_history = True: Tuple of(LDI_history, final_state) where LDI_history is 1D array of values
|
2887
|
+
|
2888
|
+
Raises
|
2889
|
+
------
|
2890
|
+
ValueError
|
2891
|
+
- If `u` is not an 1D array, or if its shape does not match the expected system dimension.
|
2892
|
+
- If `parameters` is not None and does not match the expected number of parameters.
|
2893
|
+
- If `parameters` is None but the system expects parameters.
|
2894
|
+
- If `parameters` is a scalar or array-like but not 1D.
|
2895
|
+
- If `total_time` is negative.
|
2896
|
+
- If `trasient_time` is negative.
|
2897
|
+
- If `transient_time` is greater than or equal to total_time.
|
2898
|
+
- If `sample_times` is not a 1D array of integers.
|
2899
|
+
- If `k` is less than 2 or greater than system dimension.
|
2900
|
+
|
2901
|
+
TypeError
|
2902
|
+
- If `u` is not a scalar or array-like type.
|
2903
|
+
- If `parameters` is not a scalar or array-like type.
|
2904
|
+
- If `total_time` is not int.
|
2905
|
+
- If `transient_time` is not int.
|
2906
|
+
- If sample_times cannot be converted to a 1D array of integers.
|
2907
|
+
- If `tol` is not a positive float.
|
2908
|
+
- If `seed` is not an integer.
|
2909
|
+
- If `k` is not a positive integer.
|
2910
|
+
|
2911
|
+
Notes
|
2912
|
+
-----
|
2913
|
+
- LDI_k behavior:
|
2914
|
+
- → 0 exponentially for chaotic orbits(rate depends on k)
|
2915
|
+
- → positive constant for regular orbits
|
2916
|
+
- LDI_2 ~ SALI(same convergence rate)
|
2917
|
+
- Higher k indices decay faster for chaotic orbits
|
2918
|
+
- For Hamiltonian systems, k should be ≤ d/2
|
2919
|
+
- Early termination when LDI_k < tol
|
2920
|
+
|
2921
|
+
Examples
|
2922
|
+
--------
|
2923
|
+
>>> # Basic usage (LDI_2 final value)
|
2924
|
+
>>> u0 = np.array([0.1, 0.2, 0.0, 0.0])
|
2925
|
+
>>> params = np.array([0.5, 1.0])
|
2926
|
+
>>> LDI = system.LDI(u0, params, 10000, k=2)
|
2927
|
+
|
2928
|
+
>>> # LDI_3 with full history
|
2929
|
+
>>> LDI_hist, final = system.LDI(
|
2930
|
+
... u0, params, 10000, k=3, return_history=True)
|
2931
|
+
|
2932
|
+
>>> # With custom sampling
|
2933
|
+
>>> times = np.array([100, 1000, 5000])
|
2934
|
+
>>> LDI_samples, _ = system.LDI(
|
2935
|
+
... u0, params, 10000, k=2, sample_times=times, return_history=True)
|
2936
|
+
"""
|
2937
|
+
|
2938
|
+
u = validate_initial_conditions(
|
2939
|
+
u, self.__system_dimension, allow_ensemble=False
|
2940
|
+
)
|
2941
|
+
|
2942
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
2943
|
+
|
2944
|
+
validate_non_negative(total_time, "total_time", Integral)
|
2945
|
+
validate_transient_time(transient_time, total_time, Integral)
|
2946
|
+
|
2947
|
+
validate_positive(k, "k", Integral)
|
2948
|
+
if k < 2 or k > self.__system_dimension:
|
2949
|
+
raise ValueError(f"k must be in range [2, {self.__system_dimension}]")
|
2950
|
+
|
2951
|
+
sample_times = validate_sample_times(sample_times, total_time)
|
2952
|
+
|
2953
|
+
validate_non_negative(tol, "tol", Real)
|
2954
|
+
|
2955
|
+
if not isinstance(seed, Integral):
|
2956
|
+
raise TypeError("seed must be an integer")
|
2957
|
+
|
2958
|
+
# Call underlying implementation
|
2959
|
+
result = LDI_k(
|
2960
|
+
u,
|
2961
|
+
parameters,
|
2962
|
+
total_time,
|
2963
|
+
self.__mapping,
|
2964
|
+
self.__jacobian,
|
2965
|
+
k=k,
|
2966
|
+
return_history=return_history,
|
2967
|
+
sample_times=sample_times,
|
2968
|
+
transient_time=transient_time,
|
2969
|
+
tol=tol,
|
2970
|
+
seed=seed,
|
2971
|
+
)
|
2972
|
+
|
2973
|
+
return result if return_history else result[0]
|
2974
|
+
|
2975
|
+
def __lagrangian_descriptors(
|
2976
|
+
self,
|
2977
|
+
u: Union[NDArray[np.float64], Sequence[float]],
|
2978
|
+
parameters: Union[float, Sequence[np.float64], NDArray[np.float64]],
|
2979
|
+
total_time: int = 10000,
|
2980
|
+
transient_time: Optional[int] = None,
|
2981
|
+
) -> NDArray[np.float64]:
|
2982
|
+
"""Compute Lagrangian Descriptors(LDs) for the dynamical system.
|
2983
|
+
|
2984
|
+
Parameters
|
2985
|
+
----------
|
2986
|
+
u: Union[NDArray[np.float64], Sequence[float]]
|
2987
|
+
Initial condition of shape(d,) where d is system dimension.
|
2988
|
+
Can be any sequence convertible to numpy array.
|
2989
|
+
parameters: Union[None, float, Sequence[np.float64], NDArray[np.float64]], optional
|
2990
|
+
System parameters of shape(p,) passed to mapping functions.
|
2991
|
+
total_time: int, optional
|
2992
|
+
Total number of iterations to compute(default 10000, must be > 0).
|
2993
|
+
transient_time: Optional[int], optional
|
2994
|
+
Number of initial iterations to discard(default None → no transient).
|
2995
|
+
|
2996
|
+
Returns
|
2997
|
+
-------
|
2998
|
+
NDArray[np.float64]
|
2999
|
+
Array of shape(2,) containing:
|
3000
|
+
|
3001
|
+
- [0]: Forward Lagrangian descriptor
|
3002
|
+
- [1]: Backward Lagrangian descriptor
|
3003
|
+
|
3004
|
+
Raises
|
3005
|
+
------
|
3006
|
+
NotImplementedError
|
3007
|
+
If mapping is not defined
|
3008
|
+
If backwards mapping is not defined for this system
|
3009
|
+
ValueError
|
3010
|
+
If initial condition has wrong dimension
|
3011
|
+
If parameters are invalid
|
3012
|
+
If time parameters are invalid
|
3013
|
+
TypeError
|
3014
|
+
If inputs cannot be converted to required types
|
3015
|
+
|
3016
|
+
Notes
|
3017
|
+
-----
|
3018
|
+
- LDs reveal phase space structures and invariant manifolds
|
3019
|
+
- Higher values indicate stronger stretching in phase space
|
3020
|
+
- For meaningful results:
|
3021
|
+
- Use total_time >> 1 (typically ≥ 1000)
|
3022
|
+
- Ensure mapping and backwards_mapping are exact inverses
|
3023
|
+
- Transient period helps avoid initialization artifacts
|
3024
|
+
|
3025
|
+
Examples
|
3026
|
+
--------
|
3027
|
+
>>> # Basic usage
|
3028
|
+
>>> u0 = np.array([0.1, 0.2])
|
3029
|
+
>>> params = np.array([0.5, 1.0])
|
3030
|
+
>>> lds = system.compute_lagrangian_descriptors(u0, params)
|
3031
|
+
>>> forward_ld, backward_ld = lds
|
3032
|
+
|
3033
|
+
>>> # With transient period
|
3034
|
+
>>> lds = system.compute_lagrangian_descriptors(
|
3035
|
+
... u0, params, total_time=5000, transient_time=1000)
|
3036
|
+
"""
|
3037
|
+
|
3038
|
+
# Check if mapping function is defined
|
3039
|
+
if self.__mapping is None:
|
3040
|
+
raise RuntimeError("Mapping function must be provided")
|
3041
|
+
|
3042
|
+
# Check if jacobian function is defined
|
3043
|
+
if self.__backwards_mapping is None:
|
3044
|
+
raise RuntimeError("Backwards mapping function must be provided")
|
3045
|
+
|
3046
|
+
# Input validation
|
3047
|
+
try:
|
3048
|
+
u_arr = np.asarray(u, dtype=np.float64)
|
3049
|
+
if u_arr.ndim != 1:
|
3050
|
+
raise ValueError("Initial condition must be 1D array")
|
3051
|
+
except (TypeError, ValueError) as e:
|
3052
|
+
raise TypeError(
|
3053
|
+
"Initial condition must be convertible to 1D float array"
|
3054
|
+
) from e
|
3055
|
+
|
3056
|
+
if np.isscalar(parameters):
|
3057
|
+
parameters = np.array([parameters], dtype=np.float64)
|
3058
|
+
elif not isinstance(parameters, np.ndarray):
|
3059
|
+
parameters = np.asarray(parameters, dtype=np.float64)
|
3060
|
+
|
3061
|
+
if len(u_arr) != self.__system_dimension:
|
3062
|
+
raise ValueError(
|
3063
|
+
f"Initial condition dimension {len(u_arr)} != system dimension {self.__system_dimension}"
|
3064
|
+
)
|
3065
|
+
|
3066
|
+
if not isinstance(total_time, int) or total_time <= 0:
|
3067
|
+
raise ValueError("total_time must be positive integer")
|
3068
|
+
|
3069
|
+
if transient_time is not None:
|
3070
|
+
if not isinstance(transient_time, int) or transient_time < 0:
|
3071
|
+
raise ValueError("transient_time must be non-negative integer")
|
3072
|
+
if transient_time >= total_time:
|
3073
|
+
raise ValueError("transient_time must be < total_time")
|
3074
|
+
|
3075
|
+
# Call the compiled computation function
|
3076
|
+
return lagrangian_descriptors(
|
3077
|
+
u_arr,
|
3078
|
+
parameters,
|
3079
|
+
total_time,
|
3080
|
+
self.__mapping,
|
3081
|
+
self.__backwards_mapping,
|
3082
|
+
transient_time=transient_time,
|
3083
|
+
)
|
3084
|
+
|
3085
|
+
def recurrence_matrix(
|
3086
|
+
self,
|
3087
|
+
u: Union[NDArray[np.float64], Sequence[float]],
|
3088
|
+
total_time: int,
|
3089
|
+
parameters: Union[
|
3090
|
+
None, float, Sequence[np.float64], NDArray[np.float64]
|
3091
|
+
] = None,
|
3092
|
+
transient_time: Optional[int] = None,
|
3093
|
+
**kwargs: Any,
|
3094
|
+
) -> NDArray[np.float64]:
|
3095
|
+
"""
|
3096
|
+
Compute the recurrence matrix of a univariate or multivariate time series.
|
3097
|
+
|
3098
|
+
Parameters
|
3099
|
+
----------
|
3100
|
+
u: NDArray
|
3101
|
+
Time series data. Can be 1D(shape: (N,)) or 2D(shape: (N, d)).
|
3102
|
+
If 1D, the array is reshaped to (N, 1) automatically.
|
3103
|
+
total_time: int
|
3104
|
+
Total number of iterations to simulate.
|
3105
|
+
parameters: Union[None, float, Sequence[np.float64], NDArray[np.float64]], optional
|
3106
|
+
Parameters passed to the mapping function.
|
3107
|
+
transient_time: Optional[int], optional
|
3108
|
+
Number of initial iterations to discard as transient(default None).
|
3109
|
+
If None, no transient is removed.
|
3110
|
+
metric: {"supremum", "euclidean", "manhattan"}, default = "supremum"
|
3111
|
+
Distance metric used for phase space reconstruction.
|
3112
|
+
std_metric: {"supremum", "euclidean", "manhattan"}, default = "supremum"
|
3113
|
+
Distance metric used for standard deviation calculation.
|
3114
|
+
threshold: float, default = 0.1
|
3115
|
+
Recurrence threshold(relative to data range).
|
3116
|
+
threshold_std: bool, default = True
|
3117
|
+
Whether to scale threshold by data standard deviation.
|
3118
|
+
|
3119
|
+
Returns
|
3120
|
+
-------
|
3121
|
+
recmat: NDArray of shape(N, N), dtype = np.uint8
|
3122
|
+
Binary recurrence matrix indicating whether each pair of points are within the threshold distance.
|
3123
|
+
|
3124
|
+
Raises
|
3125
|
+
------
|
3126
|
+
ValueError
|
3127
|
+
- If `u` is not an 1D array, or if its shape does not match the expected system dimension.
|
3128
|
+
- If `parameters` is not None and does not match the expected number of parameters.
|
3129
|
+
- If `parameters` is None but the system expects parameters.
|
3130
|
+
- If `parameters` is a scalar or array-like but not 1D.
|
3131
|
+
- If `total_time` is negative.
|
3132
|
+
- If `trasient_time` is negative.
|
3133
|
+
- If `transient_time` is greater than or equal to total_time.
|
3134
|
+
- If `lmin` is not a positive integer or is less than 1.
|
3135
|
+
- If `metric` or `std_metric` is not a valid string.
|
3136
|
+
- If `threshold` is not within [0, 1].
|
3137
|
+
|
3138
|
+
TypeError
|
3139
|
+
- If `u` is not a scalar or array-like type.
|
3140
|
+
- If `parameters` is not a scalar or array-like type.
|
3141
|
+
- If `total_time` is not int.
|
3142
|
+
- If `transient_time` is not int.
|
3143
|
+
- If `metric` or `std_metric` cannot be converted to a string.
|
3144
|
+
- If `threshold` is not a positive float.
|
3145
|
+
- If `lmin` is not an integer.
|
3146
|
+
|
3147
|
+
"""
|
3148
|
+
|
3149
|
+
u = validate_initial_conditions(
|
3150
|
+
u, self.__system_dimension, allow_ensemble=False
|
3151
|
+
)
|
3152
|
+
|
3153
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
3154
|
+
|
3155
|
+
validate_non_negative(total_time, "total_time", Integral)
|
3156
|
+
validate_transient_time(transient_time, total_time, Integral)
|
3157
|
+
|
3158
|
+
# Configuration handling
|
3159
|
+
config = RTEConfig(**kwargs)
|
3160
|
+
|
3161
|
+
if transient_time is not None:
|
3162
|
+
u = iterate_mapping(u, parameters, transient_time, self.__mapping)
|
3163
|
+
total_time -= transient_time
|
3164
|
+
|
3165
|
+
time_series = generate_trajectory(u, parameters, total_time, self.__mapping)
|
3166
|
+
|
3167
|
+
# Recurrence matrix calculation
|
3168
|
+
TSM = tsm(time_series)
|
3169
|
+
recmat = TSM.recurrence_matrix(
|
3170
|
+
threshold=float(config.threshold),
|
3171
|
+
metric=config.metric,
|
3172
|
+
std_metric=config.std_metric,
|
3173
|
+
threshold_std=config.threshold_std,
|
3174
|
+
)
|
3175
|
+
|
3176
|
+
return recmat
|
3177
|
+
|
3178
|
+
def recurrence_time_entropy(
|
3179
|
+
self,
|
3180
|
+
u: Union[NDArray[np.float64], Sequence[float]],
|
3181
|
+
total_time: int,
|
3182
|
+
parameters: Union[
|
3183
|
+
None, float, Sequence[np.float64], NDArray[np.float64]
|
3184
|
+
] = None,
|
3185
|
+
transient_time: Optional[int] = None,
|
3186
|
+
**kwargs: Any,
|
3187
|
+
):
|
3188
|
+
"""Compute Recurrence Time Entropy(RTE) for dynamical system analysis.
|
3189
|
+
|
3190
|
+
Parameters
|
3191
|
+
----------
|
3192
|
+
u: Union[NDArray[np.float64], Sequence[float]]
|
3193
|
+
Initial condition of shape(d,) where d is system dimension
|
3194
|
+
total_time: int
|
3195
|
+
Number of iterations to simulate(must be > 100 for meaningful results)
|
3196
|
+
parameters: Union[None, float, Sequence[np.float64], NDArray[np.float64]], optional
|
3197
|
+
System parameters of shape(p,) passed to mapping function
|
3198
|
+
metric: {"supremum", "euclidean", "manhattan"}, default = "supremum"
|
3199
|
+
Distance metric used for phase space reconstruction.
|
3200
|
+
std_metric: {"supremum", "euclidean", "manhattan"}, default = "supremum"
|
3201
|
+
Distance metric used for standard deviation calculation.
|
3202
|
+
lmin: int, default = 1
|
3203
|
+
Minimum line length to consider in recurrence quantification.
|
3204
|
+
threshold: float, default = 0.1
|
3205
|
+
Recurrence threshold(relative to data range).
|
3206
|
+
threshold_std: bool, default = True
|
3207
|
+
Whether to scale threshold by data standard deviation.
|
3208
|
+
return_final_state: bool, default = False
|
3209
|
+
Whether to return the final system state in results.
|
3210
|
+
return_recmat: bool, default = False
|
3211
|
+
Whether to return the recurrence matrix.
|
3212
|
+
return_p: bool, default = False
|
3213
|
+
Whether to return white vertical line length distribution.
|
3214
|
+
|
3215
|
+
Returns
|
3216
|
+
-------
|
3217
|
+
Union[float, Tuple[float, NDArray[np.float64]]]
|
3218
|
+
- float: RTE value(base case)
|
3219
|
+
- Tuple: (RTE, white_line_distribution) if return_distribution = True
|
3220
|
+
|
3221
|
+
Raises
|
3222
|
+
------
|
3223
|
+
ValueError
|
3224
|
+
- If `u` is not an 1D array, or if its shape does not match the expected system dimension.
|
3225
|
+
- If `parameters` is not None and does not match the expected number of parameters.
|
3226
|
+
- If `parameters` is None but the system expects parameters.
|
3227
|
+
- If `parameters` is a scalar or array-like but not 1D.
|
3228
|
+
- If `total_time` is negative.
|
3229
|
+
- If `trasient_time` is negative.
|
3230
|
+
- If `transient_time` is greater than or equal to total_time.
|
3231
|
+
- If `lmin` is not a positive integer or is less than 1.
|
3232
|
+
- If `metric` or `std_metric` is not a valid string.
|
3233
|
+
- If `threshold` is not within [0, 1].
|
3234
|
+
TypeError
|
3235
|
+
- If `u` is not a scalar or array-like type.
|
3236
|
+
- If `parameters` is not a scalar or array-like type.
|
3237
|
+
- If `total_time` is not int.
|
3238
|
+
- If `transient_time` is not int.
|
3239
|
+
- If `metric` or `std_metric` cannot be converted to a string.
|
3240
|
+
- If `threshold` is not a positive float.
|
3241
|
+
- If `lmin` is not an integer.
|
3242
|
+
|
3243
|
+
Notes
|
3244
|
+
-----
|
3245
|
+
- Higher RTE indicates more complex dynamics
|
3246
|
+
- For reliable results:
|
3247
|
+
- Use total_time > 1000
|
3248
|
+
- Typical threshold range: 0.01-0.3
|
3249
|
+
- Set min_recurrence_time = 2 to ignore single-point recurrences
|
3250
|
+
- Implementation follows[1]
|
3251
|
+
|
3252
|
+
References
|
3253
|
+
----------
|
3254
|
+
[1] Sales et al., Chaos 33, 033140 (2023)
|
3255
|
+
|
3256
|
+
Examples
|
3257
|
+
--------
|
3258
|
+
>>> # Basic usage
|
3259
|
+
>>> rte = system.recurrence_time_entropy(u0, params, 5000)
|
3260
|
+
|
3261
|
+
>>> # With distribution output
|
3262
|
+
>>> rte, dist = system.recurrence_time_entropy(
|
3263
|
+
... u0, params, 5000,
|
3264
|
+
... return_distribution=True,
|
3265
|
+
... recurrence_threshold=0.1
|
3266
|
+
...)
|
3267
|
+
"""
|
3268
|
+
|
3269
|
+
u = validate_initial_conditions(
|
3270
|
+
u, self.__system_dimension, allow_ensemble=False
|
3271
|
+
)
|
3272
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
3273
|
+
validate_non_negative(total_time, "total_time", Integral)
|
3274
|
+
validate_transient_time(transient_time, total_time, Integral)
|
3275
|
+
|
3276
|
+
return RTE(
|
3277
|
+
u,
|
3278
|
+
parameters,
|
3279
|
+
total_time,
|
3280
|
+
self.__mapping,
|
3281
|
+
transient_time=transient_time,
|
3282
|
+
**kwargs,
|
3283
|
+
)
|
3284
|
+
|
3285
|
+
def finite_time_recurrence_time_entropy(
|
3286
|
+
self,
|
3287
|
+
u: Union[NDArray[np.float64], Sequence[float]],
|
3288
|
+
total_time: int,
|
3289
|
+
finite_time: int,
|
3290
|
+
parameters: Union[
|
3291
|
+
None, float, Sequence[np.float64], NDArray[np.float64]
|
3292
|
+
] = None,
|
3293
|
+
return_points: bool = False,
|
3294
|
+
**kwargs: Any,
|
3295
|
+
) -> Union[NDArray[np.float64], Tuple[NDArray[np.float64], NDArray[np.float64]]]:
|
3296
|
+
"""Compute the finite-time Recurrence Time Entropy(RTE) for dynamical system analysis.
|
3297
|
+
|
3298
|
+
Parameters
|
3299
|
+
----------
|
3300
|
+
u: Union[NDArray[np.float64], Sequence[float]]
|
3301
|
+
Initial condition of shape(d,) where d is system dimension
|
3302
|
+
total_time: int
|
3303
|
+
Number of iterations to simulate(must be > 100 for meaningful results)
|
3304
|
+
finite_time: int
|
3305
|
+
Averaging window size in time steps
|
3306
|
+
parameters: Union[None, float, Sequence[np.float64], NDArray[np.float64]], optional
|
3307
|
+
System parameters of shape(p,) passed to mapping function
|
3308
|
+
return_points: bool, default = False
|
3309
|
+
Whether to return the finite-time RTE phase space points
|
3310
|
+
metric: {"supremum", "euclidean", "manhattan"}, default = "supremum"
|
3311
|
+
Distance metric used for phase space reconstruction.
|
3312
|
+
std_metric: {"supremum", "euclidean", "manhattan"}, default = "supremum"
|
3313
|
+
Distance metric used for standard deviation calculation.
|
3314
|
+
lmin: int, default = 1
|
3315
|
+
Minimum line length to consider in recurrence quantification.
|
3316
|
+
threshold: float, default = 0.1
|
3317
|
+
Recurrence threshold(relative to data range).
|
3318
|
+
threshold_std: bool, default = True
|
3319
|
+
Whether to scale threshold by data standard deviation.
|
3320
|
+
return_final_state: bool, default = False
|
3321
|
+
Whether to return the final system state in results.
|
3322
|
+
return_recmat: bool, default = False
|
3323
|
+
Whether to return the recurrence matrix.
|
3324
|
+
return_p: bool, default = False
|
3325
|
+
Whether to return white vertical line length distribution.
|
3326
|
+
|
3327
|
+
Returns
|
3328
|
+
-------
|
3329
|
+
NDArray[np.float64]
|
3330
|
+
|
3331
|
+
Raises
|
3332
|
+
------
|
3333
|
+
ValueError
|
3334
|
+
- If `u` is not an 1D array, or if its shape does not match the expected system dimension.
|
3335
|
+
- If `parameters` is not None and does not match the expected number of parameters.
|
3336
|
+
- If `parameters` is None but the system expects parameters.
|
3337
|
+
- If `parameters` is a scalar or array-like but not 1D.
|
3338
|
+
- If `total_time` is negative.
|
3339
|
+
- If `trasient_time` is negative.
|
3340
|
+
- If `transient_time` is greater than or equal to total_time.
|
3341
|
+
- If `lmin` is not a positive integer or is less than 1.
|
3342
|
+
- If `metric` or `std_metric` is not a valid string.
|
3343
|
+
- If `threshold` is not within [0, 1].
|
3344
|
+
TypeError
|
3345
|
+
- If `u` is not a scalar or array-like type.
|
3346
|
+
- If `parameters` is not a scalar or array-like type.
|
3347
|
+
- If `total_time` is not int.
|
3348
|
+
- If `transient_time` is not int.
|
3349
|
+
- If `metric` or `std_metric` cannot be converted to a string.
|
3350
|
+
- If `threshold` is not a positive float.
|
3351
|
+
- If `lmin` is not an integer.
|
3352
|
+
|
3353
|
+
Notes
|
3354
|
+
-----
|
3355
|
+
- Higher RTE indicates more complex dynamics
|
3356
|
+
- For reliable results:
|
3357
|
+
- Use total_time > 1000
|
3358
|
+
- Typical threshold range: 0.01-0.3
|
3359
|
+
- Set min_recurrence_time = 2 to ignore single-point recurrences
|
3360
|
+
- Implementation follows [1]
|
3361
|
+
|
3362
|
+
References
|
3363
|
+
----------
|
3364
|
+
[1] Sales et al., Chaos 33, 033140 (2023)
|
3365
|
+
|
3366
|
+
Examples
|
3367
|
+
--------
|
3368
|
+
>>> # Basic usage
|
3369
|
+
>>> ftrte = system.finite_time_recurrence_time_entropy(u0, params, 50000, 100)
|
3370
|
+
|
3371
|
+
"""
|
3372
|
+
|
3373
|
+
u = validate_initial_conditions(
|
3374
|
+
u, self.__system_dimension, allow_ensemble=False
|
3375
|
+
)
|
3376
|
+
|
3377
|
+
parameters = validate_parameters(parameters, self.__number_of_parameters)
|
3378
|
+
|
3379
|
+
validate_non_negative(total_time, "total_time", Integral)
|
3380
|
+
validate_positive(finite_time, "finite_time", Integral)
|
3381
|
+
validate_finite_time(finite_time, total_time)
|
3382
|
+
|
3383
|
+
return finite_time_RTE(
|
3384
|
+
u,
|
3385
|
+
parameters,
|
3386
|
+
total_time,
|
3387
|
+
finite_time,
|
3388
|
+
self.__mapping,
|
3389
|
+
return_points=return_points,
|
3390
|
+
**kwargs,
|
3391
|
+
)
|