Skip to content

Commit bcd3f69

Browse files
committed
MNT: minor fixes and docstring update.
1 parent 54c1aed commit bcd3f69

9 files changed

Lines changed: 440 additions & 78 deletions

File tree

rocketpy/__init__.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from . import utilities
12
from .control import _Controller
23
from .environment import Environment, EnvironmentAnalysis
34
from .exceptions import (
@@ -68,8 +69,3 @@
6869
StochasticTail,
6970
StochasticTrapezoidalFins,
7071
)
71-
72-
# Imported last: utilities pulls in Environment/Rocket/encoders, which are only
73-
# fully available once the imports above have run. Exposes
74-
# ``rocketpy.utilities`` (including ``enable_logging``) on ``import rocketpy``.
75-
from . import utilities

rocketpy/mathutils/_calc/evaluator.py

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,20 @@
66

77
from rocketpy.mathutils._calc.polation_base import PolationBase
88

9+
_COMPLEX_ND_ERROR = (
10+
"Complex coordinates are not supported for N-D array-based Function interpolation."
11+
)
12+
913

1014
class PolationEvaluator1D(PolationBase):
11-
"""Route 1D evaluation to interpolation or extrapolation."""
15+
"""Route 1-D evaluation to interpolation or extrapolation.
16+
17+
Complex queries are propagated only to support complex-step
18+
differentiation of a real 1-D sampled Function. Routing uses the real
19+
component so the imaginary perturbation cannot change the selected real
20+
interval. This does not define general interpolation over the complex
21+
plane.
22+
"""
1223

1324
__slots__ = (
1425
"_interpolator",
@@ -38,6 +49,9 @@ def expose(self):
3849
"""Flattens the evaluator into a fast closure for the
3950
simulation loop.
4051
"""
52+
if hasattr(self, "_exposed_fn"):
53+
return self._exposed_fn
54+
4155
scalar_eval = self._scalar_fn
4256
vector_eval = self._vector_fn
4357

@@ -53,6 +67,9 @@ def _eval(x, _is_iterable=None):
5367

5468
def expose_scalar(self):
5569
"""Expose a scalar-only evaluator with no iterable checks."""
70+
if hasattr(self, "_scalar_fn"):
71+
return self._scalar_fn
72+
5673
x_min = self._x_min
5774
x_max = self._x_max
5875
interp_eval = self._interpolator.evaluate
@@ -74,6 +91,9 @@ def _scalar(x):
7491

7592
def expose_vector(self):
7693
"""Expose a vector-only evaluator with no iterable checks."""
94+
if hasattr(self, "_vector_fn"):
95+
return self._vector_fn
96+
7797
x_min = self._x_min
7898
x_max = self._x_max
7999
interp_eval = self._interpolator.evaluate
@@ -241,6 +261,9 @@ def evaluate(self, *args, _is_iterable=None):
241261
return self._exposed_fn(*args, _is_iterable=_is_iterable)
242262

243263
def expose(self):
264+
if hasattr(self, "_exposed_fn"):
265+
return self._exposed_fn
266+
244267
scalar_eval = self._scalar_fn
245268
vector_eval = self._vector_fn
246269

@@ -256,6 +279,9 @@ def _eval(*args, _is_iterable=None):
256279
return _eval
257280

258281
def expose_scalar(self):
282+
if hasattr(self, "_scalar_fn"):
283+
return self._scalar_fn
284+
259285
min_domain = self._min_domain
260286
max_domain = self._max_domain
261287
extrapolation_mask = self._extrapolation_mask
@@ -264,7 +290,10 @@ def expose_scalar(self):
264290
np_local = np
265291

266292
def _scalar(*args):
267-
points = np_local.array([args], dtype=float)
293+
points = np_local.array([args])
294+
if np_local.iscomplexobj(points):
295+
raise TypeError(_COMPLEX_ND_ERROR)
296+
points = points.astype(float, copy=False)
268297
point = points[0]
269298
outside_bounds = ((point < min_domain) | (point > max_domain)).any()
270299
outside_hull = (
@@ -283,6 +312,9 @@ def _scalar(*args):
283312
return _scalar
284313

285314
def expose_vector(self): # pylint: disable=too-many-statements
315+
if hasattr(self, "_vector_fn"):
316+
return self._vector_fn
317+
286318
min_domain = self._min_domain
287319
max_domain = self._max_domain
288320
extrapolation_mask = self._extrapolation_mask
@@ -292,11 +324,11 @@ def expose_vector(self): # pylint: disable=too-many-statements
292324

293325
def _vector(*args):
294326
points = np_local.column_stack(args)
295-
out_dtype = complex if np_local.iscomplexobj(points) else float
296-
result = np_local.empty(len(points), dtype=out_dtype)
297-
points_real = points.real
298-
lower = points_real < min_domain
299-
upper = points_real > max_domain
327+
if np_local.iscomplexobj(points):
328+
raise TypeError(_COMPLEX_ND_ERROR)
329+
result = np_local.empty(len(points), dtype=float)
330+
lower = points < min_domain
331+
upper = points > max_domain
300332
extrap_mask = lower.any(axis=1) | upper.any(axis=1)
301333
if extrapolation_mask is not None:
302334
interp_candidates = ~extrap_mask
@@ -352,6 +384,9 @@ def evaluate(self, *args, _is_iterable=None):
352384
return self._exposed_fn(*args, _is_iterable=_is_iterable)
353385

354386
def expose(self):
387+
if hasattr(self, "_exposed_fn"):
388+
return self._exposed_fn
389+
355390
scalar_eval = self._scalar_fn
356391
vector_eval = self._vector_fn
357392

@@ -367,6 +402,9 @@ def _eval(*args, _is_iterable=None):
367402
return _eval
368403

369404
def expose_scalar(self):
405+
if hasattr(self, "_scalar_fn"):
406+
return self._scalar_fn
407+
370408
min_domain = self._min_domain
371409
max_domain = self._max_domain
372410
interp_eval = self._interpolator.evaluate
@@ -376,14 +414,20 @@ def expose_scalar(self):
376414
if self._interpolator is self._extrapolator:
377415

378416
def _scalar_same(*args):
379-
points = np_local.array([args], dtype=float)
417+
points = np_local.array([args])
418+
if np_local.iscomplexobj(points):
419+
raise TypeError(_COMPLEX_ND_ERROR)
420+
points = points.astype(float, copy=False)
380421
res = interp_eval(points)
381422
return float(res[0])
382423

383424
return _scalar_same
384425

385426
def _scalar(*args):
386-
points = np_local.array([args], dtype=float)
427+
points = np_local.array([args])
428+
if np_local.iscomplexobj(points):
429+
raise TypeError(_COMPLEX_ND_ERROR)
430+
points = points.astype(float, copy=False)
387431
point = points[0]
388432
if ((point < min_domain) | (point > max_domain)).any():
389433
return float(extrap_eval(points)[0])
@@ -392,6 +436,9 @@ def _scalar(*args):
392436
return _scalar
393437

394438
def expose_vector(self):
439+
if hasattr(self, "_vector_fn"):
440+
return self._vector_fn
441+
395442
min_domain = self._min_domain
396443
max_domain = self._max_domain
397444
interp_eval = self._interpolator.evaluate
@@ -402,12 +449,16 @@ def expose_vector(self):
402449

403450
def _vector_same(*args):
404451
points = np_local.column_stack(np_local.broadcast_arrays(*args))
452+
if np_local.iscomplexobj(points):
453+
raise TypeError(_COMPLEX_ND_ERROR)
405454
return interp_eval(points)
406455

407456
return _vector_same
408457

409458
def _vector(*args):
410459
points = np_local.column_stack(np_local.broadcast_arrays(*args))
460+
if np_local.iscomplexobj(points):
461+
raise TypeError(_COMPLEX_ND_ERROR)
411462
result = np_local.empty(len(points), dtype=float)
412463
lower = points < min_domain
413464
upper = points > max_domain

rocketpy/mathutils/_calc/polation_1d.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
"""1D interpolation and extrapolation strategies."""
1+
"""1-D interpolation and extrapolation strategies.
2+
3+
The sampled domain and image are real. Complex query values are propagated
4+
only for complex-step differentiation: their real component selects the
5+
piecewise interval and the full value is passed to the selected polynomial.
6+
These strategies do not provide general complex-plane interpolation.
7+
"""
28

39
from __future__ import annotations
410

@@ -30,8 +36,10 @@ def _find_index(
3036
----------
3137
x_arr : np.ndarray
3238
Sorted 1D array of x-coordinates.
33-
xq : float or np.ndarray or complex
34-
The query coordinate(s).
39+
xq : float, complex or np.ndarray
40+
The query coordinate(s). A complex component is meaningful only as an
41+
imaginary perturbation for complex-step differentiation. Interval
42+
ordering is determined exclusively from the real component.
3543
n : int
3644
The size of x_arr.
3745
_is_iterable : bool, optional
@@ -41,6 +49,12 @@ def _find_index(
4149
-------
4250
int or np.ndarray
4351
The index or indices representing the interval.
52+
53+
Notes
54+
-----
55+
Complex numbers have no ordering compatible with the real sampled domain.
56+
Using the real component keeps ``x + i*h`` in the same interval as ``x``,
57+
which is required for complex-step differentiation.
4458
"""
4559
if _is_iterable is None:
4660
_is_iterable = hasattr(xq, "__iter__") and np.ndim(xq) > 0
@@ -50,7 +64,7 @@ def _find_index(
5064
idx = bisect_left(x_arr, xq.real)
5165
return 1 if idx < 1 else (idx if idx < n else n - 1)
5266
else:
53-
idx = np.searchsorted(x_arr, xq, side="left")
67+
idx = np.searchsorted(x_arr, np.real(xq), side="left")
5468
return np.clip(idx, 1, n - 1)
5569

5670

@@ -61,7 +75,7 @@ def _cubic_eval_vec(
6175
c: float | NDArray[np.float64],
6276
d: float | NDArray[np.float64],
6377
) -> float | NDArray[np.float64]:
64-
"""Evaluate a cubic polynomial: a + t * (b + t * (c + t * d)).
78+
"""Evaluate a cubic polynomial: a + b*t + c*t**2 + d*t**3.
6579
6680
Parameters
6781
----------

rocketpy/mathutils/_calc/polation_nd.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
"""ND interpolation and extrapolation strategies."""
1+
"""N-D interpolation and extrapolation strategies.
2+
3+
Sampled N-D coordinates are real-valued. Complex query coordinates are not
4+
supported and are rejected by the evaluator before reaching these strategies.
5+
"""
26

37
from __future__ import annotations
48

0 commit comments

Comments
 (0)