Skip to content

Commit 6f6c4e2

Browse files
committed
MNT: use scipy interpolators in Function class.
1 parent c674725 commit 6f6c4e2

4 files changed

Lines changed: 140 additions & 174 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ Attention: The newest changes should be on top -->
3939

4040
### Changed
4141

42+
- MNT: Use scipy standard interpolators in Function class. [#809](https://github.com/RocketPy-Team/RocketPy/pull/809)
43+
4244

4345
### Fixed
4446

rocketpy/mathutils/function.py

Lines changed: 39 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515

1616
import matplotlib.pyplot as plt
1717
import numpy as np
18-
from scipy import integrate, linalg, optimize
18+
from scipy import integrate, optimize
1919
from scipy.interpolate import (
20+
Akima1DInterpolator,
21+
BarycentricInterpolator,
22+
CubicSpline,
2023
LinearNDInterpolator,
2124
NearestNDInterpolator,
2225
RBFInterpolator,
@@ -312,17 +315,23 @@ def set_interpolation(self, method="spline"):
312315

313316
def __update_interpolation_coefficients(self, method):
314317
"""Update interpolation coefficients for the given method."""
315-
# Spline, akima and polynomial need data processing
316-
# Shepard, and linear do not
317-
if method == "polynomial":
318-
self.__interpolate_polynomial__()
319-
self._coeffs = self.__polynomial_coefficients__
318+
if method == "spline" or method is None:
319+
self._interpolator = CubicSpline(
320+
self.x_array, self.y_array, bc_type="natural", extrapolate=True
321+
)
322+
self._coeffs = self._interpolator.c[::-1]
323+
self.__spline_coefficients__ = self._coeffs
324+
elif method == "linear":
325+
self._coeffs = np.diff(self.y_array) / np.diff(self.x_array)
320326
elif method == "akima":
321-
self.__interpolate_akima__()
322-
self._coeffs = self.__akima_coefficients__
323-
elif method == "spline" or method is None:
324-
self.__interpolate_spline__()
325-
self._coeffs = self.__spline_coefficients__
327+
self._interpolator = Akima1DInterpolator(
328+
self.x_array, self.y_array, extrapolate=True
329+
)
330+
self._coeffs = self._interpolator.c[::-1]
331+
self.__akima_coefficients__ = self._coeffs
332+
elif method == "polynomial":
333+
self._interpolator = BarycentricInterpolator(self.x_array, self.y_array)
334+
self._coeffs = []
326335
else:
327336
self._coeffs = []
328337

@@ -361,12 +370,10 @@ def __set_interpolation_func(self): # pylint: disable=too-many-statements
361370
if self.__dom_dim__ == 1:
362371

363372
def linear_interpolation(x, x_min, x_max, x_data, y_data, coeffs): # pylint: disable=unused-argument
364-
x_interval = bisect_left(x_data, x)
373+
x_interval = bisect_left(x_data, x, lo=1, hi=len(x_data) - 1)
365374
x_left = x_data[x_interval - 1]
366375
y_left = y_data[x_interval - 1]
367-
dx = float(x_data[x_interval] - x_left)
368-
dy = float(y_data[x_interval] - y_left)
369-
return (x - x_left) * (dy / dx) + y_left
376+
return (x - x_left) * coeffs[x_interval - 1] + y_left
370377

371378
else:
372379
interpolator = LinearNDInterpolator(self._domain, self._image)
@@ -379,28 +386,21 @@ def linear_interpolation(x, x_min, x_max, x_data, y_data, coeffs): # pylint: di
379386
elif interpolation == 1: # polynomial
380387

381388
def polynomial_interpolation(x, x_min, x_max, x_data, y_data, coeffs): # pylint: disable=unused-argument
382-
return np.sum(coeffs * x ** np.arange(len(coeffs)))
389+
return self._interpolator(x)
383390

384391
self._interpolation_func = polynomial_interpolation
385392

386393
elif interpolation == 2: # akima
387394

388395
def akima_interpolation(x, x_min, x_max, x_data, y_data, coeffs): # pylint: disable=unused-argument
389-
x_interval = bisect_left(x_data, x)
390-
x_interval = x_interval if x_interval != 0 else 1
391-
a = coeffs[4 * x_interval - 4 : 4 * x_interval]
392-
return a[3] * x**3 + a[2] * x**2 + a[1] * x + a[0]
396+
return self._interpolator(x)
393397

394398
self._interpolation_func = akima_interpolation
395399

396400
elif interpolation == 3: # spline
397401

398402
def spline_interpolation(x, x_min, x_max, x_data, y_data, coeffs): # pylint: disable=unused-argument
399-
x_interval = bisect_left(x_data, x)
400-
x_interval = max(x_interval, 1)
401-
a = coeffs[:, x_interval - 1]
402-
x = x - x_data[x_interval - 1]
403-
return a[3] * x**3 + a[2] * x**2 + a[1] * x + a[0]
403+
return self._interpolator(x)
404404

405405
self._interpolation_func = spline_interpolation
406406

@@ -472,24 +472,17 @@ def natural_extrapolation(x, x_min, x_max, x_data, y_data, coeffs): # pylint: d
472472
elif interpolation == 1: # polynomial
473473

474474
def natural_extrapolation(x, x_min, x_max, x_data, y_data, coeffs): # pylint: disable=unused-argument
475-
return np.sum(coeffs * x ** np.arange(len(coeffs)))
475+
return self._interpolator(x)
476476

477477
elif interpolation == 2: # akima
478478

479479
def natural_extrapolation(x, x_min, x_max, x_data, y_data, coeffs): # pylint: disable=unused-argument
480-
a = coeffs[:4] if x < x_min else coeffs[-4:]
481-
return a[3] * x**3 + a[2] * x**2 + a[1] * x + a[0]
480+
return self._interpolator(x)
482481

483482
elif interpolation == 3: # spline
484483

485484
def natural_extrapolation(x, x_min, x_max, x_data, y_data, coeffs): # pylint: disable=unused-argument
486-
if x < x_min:
487-
a = coeffs[:, 0]
488-
x = x - x_data[0]
489-
else:
490-
a = coeffs[:, -1]
491-
x = x - x_data[-2]
492-
return a[3] * x**3 + a[2] * x**2 + a[1] * x + a[0]
485+
return self._interpolator(x)
493486

494487
elif interpolation == 4: # shepard
495488
# pylint: disable=unused-argument
@@ -1829,85 +1822,6 @@ def compare_plots( # pylint: disable=too-many-statements
18291822
if return_object:
18301823
return fig, ax
18311824

1832-
# Define all interpolation methods
1833-
def __interpolate_polynomial__(self):
1834-
"""Calculate polynomial coefficients that fit the data exactly."""
1835-
# Find the degree of the polynomial interpolation
1836-
degree = self.source.shape[0] - 1
1837-
# Get x and y values for all supplied points.
1838-
x = self.x_array
1839-
y = self.y_array
1840-
# Check if interpolation requires large numbers
1841-
if np.amax(x) ** degree > 1e308:
1842-
warnings.warn(
1843-
"Polynomial interpolation of too many points can't be done."
1844-
" Once the degree is too high, numbers get too large."
1845-
" The process becomes inefficient. Using spline instead."
1846-
)
1847-
return self.set_interpolation("spline")
1848-
# Create coefficient matrix1
1849-
sys_coeffs = np.zeros((degree + 1, degree + 1))
1850-
for i in range(degree + 1):
1851-
sys_coeffs[:, i] = x**i
1852-
# Solve the system and store the resultant coefficients
1853-
self.__polynomial_coefficients__ = np.linalg.solve(sys_coeffs, y)
1854-
1855-
def __interpolate_spline__(self):
1856-
"""Calculate natural spline coefficients that fit the data exactly."""
1857-
# Get x and y values for all supplied points
1858-
x, y = self.x_array, self.y_array
1859-
m_dim = len(x)
1860-
h = np.diff(x)
1861-
# Initialize the matrix
1862-
banded_matrix = np.zeros((3, m_dim))
1863-
banded_matrix[1, 0] = banded_matrix[1, m_dim - 1] = 1
1864-
# Construct the Ab banded matrix and B vector
1865-
vector_b = [0]
1866-
banded_matrix[2, :-2] = h[:-1]
1867-
banded_matrix[1, 1:-1] = 2 * (h[:-1] + h[1:])
1868-
banded_matrix[0, 2:] = h[1:]
1869-
vector_b.extend(3 * ((y[2:] - y[1:-1]) / h[1:] - (y[1:-1] - y[:-2]) / h[:-1]))
1870-
vector_b.append(0)
1871-
# Solve the system for c coefficients
1872-
c = linalg.solve_banded(
1873-
(1, 1), banded_matrix, vector_b, overwrite_ab=True, overwrite_b=True
1874-
)
1875-
# Calculate other coefficients
1876-
b = (y[1:] - y[:-1]) / h - h * (2 * c[:-1] + c[1:]) / 3
1877-
d = (c[1:] - c[:-1]) / (3 * h)
1878-
# Store coefficients
1879-
self.__spline_coefficients__ = np.vstack([y[:-1], b, c[:-1], d])
1880-
1881-
def __interpolate_akima__(self):
1882-
"""Calculate akima spline coefficients that fit the data exactly"""
1883-
# Get x and y values for all supplied points
1884-
x, y = self.x_array, self.y_array
1885-
# Estimate derivatives at each point
1886-
d = [0] * len(x)
1887-
d[0] = (y[1] - y[0]) / (x[1] - x[0])
1888-
d[-1] = (y[-1] - y[-2]) / (x[-1] - x[-2])
1889-
for i in range(1, len(x) - 1):
1890-
w1, w2 = (x[i] - x[i - 1]), (x[i + 1] - x[i])
1891-
d1, d2 = ((y[i] - y[i - 1]) / w1), ((y[i + 1] - y[i]) / w2)
1892-
d[i] = (w1 * d2 + w2 * d1) / (w1 + w2)
1893-
# Calculate coefficients for each interval with system already solved
1894-
coeffs = [0] * 4 * (len(x) - 1)
1895-
for i in range(len(x) - 1):
1896-
xl, xr = x[i], x[i + 1]
1897-
yl, yr = y[i], y[i + 1]
1898-
dl, dr = d[i], d[i + 1]
1899-
matrix = np.array(
1900-
[
1901-
[1, xl, xl**2, xl**3],
1902-
[1, xr, xr**2, xr**3],
1903-
[0, 1, 2 * xl, 3 * xl**2],
1904-
[0, 1, 2 * xr, 3 * xr**2],
1905-
]
1906-
)
1907-
result = np.array([yl, yr, dl, dr]).T
1908-
coeffs[4 * i : 4 * i + 4] = np.linalg.solve(matrix, result)
1909-
self.__akima_coefficients__ = coeffs
1910-
19111825
def __neg__(self):
19121826
"""Negates the Function object. The result has the same effect as
19131827
multiplying the Function by -1.
@@ -3273,6 +3187,17 @@ def __validate_source(self, source): # pylint: disable=too-many-statements
32733187
"Could not read the csv or txt file to create Function source."
32743188
) from e
32753189

3190+
if isinstance(source, NUMERICAL_TYPES) or self.__is_single_element_array(
3191+
source
3192+
):
3193+
# Convert number source into vectorized lambda function
3194+
temp = 1 * source
3195+
3196+
def source_function(_):
3197+
return temp
3198+
3199+
return source_function
3200+
32763201
if isinstance(source, (list, np.ndarray)):
32773202
# Triggers an error if source is not a list of numbers
32783203
source = np.array(source, dtype=np.float64)
@@ -3293,15 +3218,6 @@ def __validate_source(self, source): # pylint: disable=too-many-statements
32933218

32943219
return source
32953220

3296-
if isinstance(source, NUMERICAL_TYPES):
3297-
# Convert number source into vectorized lambda function
3298-
temp = 1 * source
3299-
3300-
def source_function(_):
3301-
return temp
3302-
3303-
return source_function
3304-
33053221
# If source is a callable function
33063222
return source
33073223

tests/unit/test_environment.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ def test_environment_export_environment_exports_valid_environment_json(
212212
assert os.path.isfile("environment.json")
213213

214214
# Check file content
215-
assert exported_env["gravity"] == env.gravity(env.elevation)
215+
assert exported_env["gravity"] == str(float(env.gravity(env.elevation)))
216216
assert exported_env["date"] == [
217217
env.datetime_date.year,
218218
env.datetime_date.month,

0 commit comments

Comments
 (0)