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,313 @@
|
|
1
|
+
# validators.py
|
2
|
+
|
3
|
+
# Copyright (C) 2025 Matheus Rolim Sales
|
4
|
+
#
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
8
|
+
# (at your option) any later version.
|
9
|
+
#
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
+
# GNU General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU General Public License
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
17
|
+
|
18
|
+
import numpy as np
|
19
|
+
from numbers import Integral, Real
|
20
|
+
from numpy.typing import NDArray
|
21
|
+
|
22
|
+
|
23
|
+
def validate_initial_conditions(
|
24
|
+
u, system_dimension, allow_ensemble=True
|
25
|
+
) -> NDArray[np.float64]:
|
26
|
+
"""
|
27
|
+
Validate and format the initial condition(s).
|
28
|
+
|
29
|
+
Parameters
|
30
|
+
----------
|
31
|
+
u : scalar, 1D or 2D array
|
32
|
+
Initial condition(s).
|
33
|
+
system_dimension : int
|
34
|
+
Expected number of variables in the system.
|
35
|
+
allow_ensemble : bool, optional
|
36
|
+
Whether 2D array of ICs (ensemble) is allowed. Default is True.
|
37
|
+
|
38
|
+
Returns
|
39
|
+
-------
|
40
|
+
u : np.ndarray
|
41
|
+
Validated and contiguous copy of initial conditions.
|
42
|
+
|
43
|
+
Raises
|
44
|
+
------
|
45
|
+
ValueError
|
46
|
+
If `u` is not a scalar, 1D, or 2D array, or if its shape does not match
|
47
|
+
the expected system dimension.
|
48
|
+
If `u` is a 1D array but its length does not match the system dimension,
|
49
|
+
or if `u` is a 2D array but does not match the expected shape for an ensemble.
|
50
|
+
TypeError
|
51
|
+
If `u` is not a scalar or array-like type.
|
52
|
+
|
53
|
+
"""
|
54
|
+
if np.isscalar(u):
|
55
|
+
u = np.array([u], dtype=np.float64)
|
56
|
+
else:
|
57
|
+
u = np.asarray(u, dtype=np.float64)
|
58
|
+
if u.ndim not in (1, 2):
|
59
|
+
raise ValueError("Initial condition must be 1D or 2D array")
|
60
|
+
|
61
|
+
u = np.ascontiguousarray(u).copy()
|
62
|
+
|
63
|
+
if u.ndim == 1:
|
64
|
+
if len(u) != system_dimension:
|
65
|
+
raise ValueError(
|
66
|
+
f"1D initial condition must have length {system_dimension}"
|
67
|
+
)
|
68
|
+
elif u.ndim == 2:
|
69
|
+
if not allow_ensemble:
|
70
|
+
raise ValueError(
|
71
|
+
"Ensemble of initial conditions not allowed in this context"
|
72
|
+
)
|
73
|
+
if u.shape[1] != system_dimension:
|
74
|
+
raise ValueError(
|
75
|
+
f"Each initial condition must have length {system_dimension}"
|
76
|
+
)
|
77
|
+
return u
|
78
|
+
|
79
|
+
|
80
|
+
def validate_parameters(parameters, number_of_parameters) -> NDArray[np.float64]:
|
81
|
+
"""
|
82
|
+
Validate and standardize parameter vector.
|
83
|
+
|
84
|
+
Parameters
|
85
|
+
----------
|
86
|
+
parameters : scalar, 1D array-like, or None
|
87
|
+
The parameter values to validate. `None` is allowed if number_of_parameters == 0.
|
88
|
+
number_of_parameters : int
|
89
|
+
The required number of parameters (defined by the system).
|
90
|
+
|
91
|
+
Returns
|
92
|
+
-------
|
93
|
+
parameters : np.ndarray
|
94
|
+
Validated 1D parameter array (empty if no parameters are required and input is None).
|
95
|
+
|
96
|
+
Raises
|
97
|
+
------
|
98
|
+
ValueError
|
99
|
+
If `parameters` is not None and does not match the expected number of parameters.
|
100
|
+
If `parameters` is None but the system expects parameters.
|
101
|
+
If `parameters` is a scalar or array-like but not 1D.
|
102
|
+
TypeError
|
103
|
+
If `parameters` is not a scalar or array-like type.
|
104
|
+
"""
|
105
|
+
if number_of_parameters == 0:
|
106
|
+
if parameters is not None:
|
107
|
+
raise ValueError("This system does not expect any parameters.")
|
108
|
+
return np.array([0], dtype=np.float64)
|
109
|
+
|
110
|
+
if parameters is None:
|
111
|
+
raise ValueError(
|
112
|
+
f"This system expects {number_of_parameters} parameter(s), but got None."
|
113
|
+
)
|
114
|
+
|
115
|
+
if np.isscalar(parameters):
|
116
|
+
parameters = np.array([parameters], dtype=np.float64)
|
117
|
+
else:
|
118
|
+
parameters = np.asarray(parameters, dtype=np.float64)
|
119
|
+
if parameters.ndim != 1:
|
120
|
+
raise ValueError(
|
121
|
+
f"`parameters` must be a 1D array or scalar. Got shape {parameters.shape}."
|
122
|
+
)
|
123
|
+
|
124
|
+
if parameters.size != number_of_parameters:
|
125
|
+
raise ValueError(
|
126
|
+
f"Expected {number_of_parameters} parameter(s), but got {parameters.size}."
|
127
|
+
)
|
128
|
+
|
129
|
+
return parameters
|
130
|
+
|
131
|
+
|
132
|
+
def validate_non_negative(value, name, type_=Integral) -> None:
|
133
|
+
"""Ensure value is non-negative of specified type.
|
134
|
+
|
135
|
+
Parameters
|
136
|
+
----------
|
137
|
+
value : Any
|
138
|
+
The value to validate.
|
139
|
+
name : str
|
140
|
+
The name of the value for error messages.
|
141
|
+
type_ : type, optional
|
142
|
+
The expected type of the value (default is int).
|
143
|
+
Raises
|
144
|
+
------
|
145
|
+
TypeError
|
146
|
+
If value is not of the expected type.
|
147
|
+
ValueError
|
148
|
+
If value is negative.
|
149
|
+
"""
|
150
|
+
if not isinstance(value, type_):
|
151
|
+
raise TypeError(f"{name} must be of type {type_.__name__}")
|
152
|
+
if value < 0:
|
153
|
+
raise ValueError(f"{name} must be non-negative")
|
154
|
+
|
155
|
+
|
156
|
+
def validate_positive(value, name, type_=Integral) -> None:
|
157
|
+
"""Ensure value is >= 1 and of specified type.
|
158
|
+
|
159
|
+
Parameters
|
160
|
+
----------
|
161
|
+
value : Any
|
162
|
+
The value to validate.
|
163
|
+
name : str
|
164
|
+
The name of the value for error messages.
|
165
|
+
type_ : type, optional
|
166
|
+
The expected type of the value (default is int).
|
167
|
+
Raises
|
168
|
+
------
|
169
|
+
TypeError
|
170
|
+
If value is not of the expected type.
|
171
|
+
ValueError
|
172
|
+
If value is less than 1.
|
173
|
+
"""
|
174
|
+
if not isinstance(value, type_):
|
175
|
+
raise TypeError(f"{name} must be of type {type_.__name__}")
|
176
|
+
if value < 1:
|
177
|
+
raise ValueError(f"{name} must be greater than or equal to 1")
|
178
|
+
|
179
|
+
|
180
|
+
def validate_transient_time(transient_time, total_time, type_=Integral) -> None:
|
181
|
+
"""Ensure transient_time is valid relative to total_time.
|
182
|
+
Parameters
|
183
|
+
----------
|
184
|
+
transient_time : int
|
185
|
+
The transient time to validate.
|
186
|
+
total_time : int
|
187
|
+
The total time of the simulation.
|
188
|
+
Raises
|
189
|
+
------
|
190
|
+
TypeError
|
191
|
+
If transient_time is not int.
|
192
|
+
ValueError
|
193
|
+
If transient_time is negative.
|
194
|
+
If transient_time is greater than or equal to total_time.
|
195
|
+
"""
|
196
|
+
|
197
|
+
if transient_time is not None:
|
198
|
+
validate_non_negative(transient_time, "transient_time", type_=type_)
|
199
|
+
|
200
|
+
if transient_time >= total_time:
|
201
|
+
raise ValueError("transient_time must be less than total_time")
|
202
|
+
|
203
|
+
|
204
|
+
def validate_finite_time(finite_time, total_time) -> None:
|
205
|
+
if finite_time > total_time // 2:
|
206
|
+
raise ValueError(f"finite_time must be less than or equal to {total_time // 2}")
|
207
|
+
|
208
|
+
|
209
|
+
def validate_and_convert_param_range(param_range) -> NDArray[np.float64]:
|
210
|
+
"""
|
211
|
+
Validate and convert `param_range` input to a 1D numpy array.
|
212
|
+
|
213
|
+
Accepts either:
|
214
|
+
- A tuple (start, stop, num_points) for linspace generation
|
215
|
+
- A 1D array-like of precomputed values
|
216
|
+
|
217
|
+
Returns
|
218
|
+
-------
|
219
|
+
param_values : np.ndarray
|
220
|
+
1D array of parameter values.
|
221
|
+
|
222
|
+
Raises
|
223
|
+
------
|
224
|
+
ValueError
|
225
|
+
If `param_range` is a tuple but does not have exactly 3 elements.
|
226
|
+
If `param_range` is a tuple but elements are not numbers or if the
|
227
|
+
precomputed array is not 1D.
|
228
|
+
If `param_range` cannot be converted to a 1D numpy array.
|
229
|
+
If `param_range` is a tuple but the elements are not numbers.
|
230
|
+
TypeError
|
231
|
+
If `param_range` is not a tuple or a 1D array-like.
|
232
|
+
|
233
|
+
"""
|
234
|
+
if isinstance(param_range, tuple):
|
235
|
+
if len(param_range) != 3:
|
236
|
+
raise ValueError("param_range tuple must have (start, stop, num_points)")
|
237
|
+
try:
|
238
|
+
param_values = np.linspace(*param_range)
|
239
|
+
except TypeError as e:
|
240
|
+
raise ValueError("param_range tuple elements must be numbers") from e
|
241
|
+
else:
|
242
|
+
try:
|
243
|
+
param_values = np.asarray(param_range, dtype=np.float64)
|
244
|
+
if param_values.ndim != 1:
|
245
|
+
raise ValueError("Precomputed param_range must be 1D array")
|
246
|
+
except (TypeError, ValueError) as e:
|
247
|
+
raise TypeError(
|
248
|
+
"param_range must be a 1D array-like or a (start, stop, num_points) tuple"
|
249
|
+
) from e
|
250
|
+
|
251
|
+
return param_values
|
252
|
+
|
253
|
+
|
254
|
+
def validate_sample_times(sample_times, total_time):
|
255
|
+
"""
|
256
|
+
Validate and convert sample_times to a 1D int32 array within [0, total_time].
|
257
|
+
|
258
|
+
Parameters
|
259
|
+
----------
|
260
|
+
sample_times : array-like or None
|
261
|
+
Optional time indices to sample from, must be non-negative and ≤ total_time.
|
262
|
+
total_time : int
|
263
|
+
Maximum valid time index.
|
264
|
+
|
265
|
+
Returns
|
266
|
+
-------
|
267
|
+
sample_times_arr : np.ndarray or None
|
268
|
+
Validated 1D array of sample times, or None if input was None.
|
269
|
+
|
270
|
+
Raises
|
271
|
+
------
|
272
|
+
TypeError
|
273
|
+
If sample_times is not convertible to a 1D int32 array.
|
274
|
+
ValueError
|
275
|
+
If sample_times is not 1D or contains out-of-bound values.
|
276
|
+
"""
|
277
|
+
if sample_times is None:
|
278
|
+
return None
|
279
|
+
|
280
|
+
try:
|
281
|
+
sample_times_arr = np.asarray(sample_times, dtype=np.int32)
|
282
|
+
if sample_times_arr.ndim != 1:
|
283
|
+
raise ValueError("sample_times must be a 1D array")
|
284
|
+
if np.any(sample_times_arr < 0) or np.any(sample_times_arr > total_time):
|
285
|
+
raise ValueError("sample_times must be in the range [0, total_time]")
|
286
|
+
except (TypeError, ValueError) as e:
|
287
|
+
raise TypeError("sample_times must be convertible to a 1D int32 array") from e
|
288
|
+
|
289
|
+
return sample_times_arr
|
290
|
+
|
291
|
+
|
292
|
+
def validate_axis(axis, system_dimension):
|
293
|
+
"""
|
294
|
+
Validate that axis is a non-negative integer within system dimension bounds.
|
295
|
+
|
296
|
+
Parameters
|
297
|
+
----------
|
298
|
+
axis : int
|
299
|
+
Axis index to validate.
|
300
|
+
system_dimension : int
|
301
|
+
The number of dimensions in the system.
|
302
|
+
|
303
|
+
Raises
|
304
|
+
------
|
305
|
+
TypeError
|
306
|
+
If axis is not an integer.
|
307
|
+
ValueError
|
308
|
+
If axis is not in [0, system_dimension - 1].
|
309
|
+
"""
|
310
|
+
if not isinstance(axis, Integral):
|
311
|
+
raise TypeError("axis must be an integer")
|
312
|
+
if axis < 0 or axis >= system_dimension:
|
313
|
+
raise ValueError(f"axis must be in the range [0, {system_dimension - 1}]")
|