pynamicalsys 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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}]")