Spline interpolation¶
This tutorial covers scalar cubic splines and vector-valued B-spline curves. The distinction matters: scalar splines accept NumPy arrays of times directly, while general B-spline curves evaluate one parameter at a time.
Interpolate scalar waypoints¶
import numpy as np
from interpolatepy import CubicSpline
t_points = [0.0, 1.0, 2.5, 4.0]
q_points = [0.0, 1.0, -0.5, 2.0]
spline = CubicSpline(t_points, q_points, v0=0.0, vn=0.0)
t = np.linspace(t_points[0], t_points[-1], 250)
q = spline.evaluate(t)
qd = spline.evaluate_velocity(t)
qdd = spline.evaluate_acceleration(t)
assert np.allclose(spline.evaluate(np.asarray(t_points)), q_points)
assert np.isclose(spline.evaluate_velocity(t_points[0]), 0.0)
assert np.isclose(spline.evaluate_velocity(t_points[-1]), 0.0)
v0 and vn are endpoint velocities. Their default value is zero, which
creates a clamped rest-to-rest spline. A natural spline instead constrains
endpoint acceleration; CubicSpline does not use that convention.
At an interior knot, position, velocity, and acceleration are continuous. Jerk is piecewise constant and may jump.
Smooth noisy scalar samples¶
mu controls the fit/roughness trade-off. One gives exact interpolation;
smaller positive values smooth more strongly.
import numpy as np
from interpolatepy import CubicSmoothingSpline
rng = np.random.default_rng(42)
t_points = np.linspace(0.0, 6.0, 31)
q_points = np.sin(t_points) + 0.12 * rng.standard_normal(t_points.size)
spline = CubicSmoothingSpline(
t_points.tolist(),
q_points.tolist(),
mu=0.2,
v0=0.0,
vn=0.0,
)
t = np.linspace(0.0, 6.0, 200)
smoothed = spline.evaluate(t)
assert smoothed.shape == t.shape
Weights are relative penalties on sample error. Use np.inf to pin a sample:
import numpy as np
from interpolatepy import CubicSmoothingSpline
t_points = [0.0, 1.0, 2.0, 3.0]
q_points = [0.0, 1.2, 0.8, 2.0]
weights = [np.inf, 1.0, 1.0, np.inf]
spline = CubicSmoothingSpline(t_points, q_points, mu=0.1, weights=weights)
assert np.isclose(spline.evaluate(0.0), q_points[0])
assert np.isclose(spline.evaluate(3.0), q_points[-1])
Select smoothing by error tolerance¶
When the requirement is a maximum deviation rather than a chosen mu, use the
binary-search helper:
import numpy as np
from interpolatepy import SplineConfig
from interpolatepy import smoothing_spline_with_tolerance
t_points = np.array([0.0, 1.0, 2.0, 3.0, 4.0])
q_points = np.array([0.0, 1.1, 0.1, 1.9, 1.0])
config = SplineConfig(max_iterations=40)
spline, mu, max_error, iterations = smoothing_spline_with_tolerance(
t_points,
q_points,
tolerance=0.25,
config=config,
)
assert 0.0 < mu <= 1.0
assert iterations <= config.max_iterations
assert max_error <= 0.25 + 1e-6
The returned error is measured at the supplied samples, not over every point of the continuous curve.
Constrain endpoint acceleration¶
Method 1 takes derivative values directly:
import numpy as np
from interpolatepy import CubicSplineWithAcceleration1
spline = CubicSplineWithAcceleration1(
[0.0, 1.0, 2.0, 3.0],
[0.0, 1.0, -0.5, 0.0],
v0=0.0,
vn=0.0,
a0=0.5,
an=-0.5,
)
assert np.isclose(spline.evaluate_acceleration(0.0), 0.5)
assert np.isclose(spline.evaluate_acceleration(3.0), -0.5)
Method 2 uses SplineParameters:
import numpy as np
from interpolatepy import CubicSplineWithAcceleration2
from interpolatepy import SplineParameters
params = SplineParameters(v0=0.0, vn=0.0, a0=0.5, an=-0.5)
spline = CubicSplineWithAcceleration2(
[0.0, 1.0, 2.0, 3.0],
[0.0, 1.0, -0.5, 0.0],
params,
)
assert np.isclose(spline.evaluate_acceleration(0.0), params.a0)
assert np.isclose(spline.evaluate_acceleration(3.0), params.an)
The constructions differ between samples. Sample velocity and acceleration densely before choosing one for a constrained mechanism.
Construct a B-spline from controls¶
For degree p, the knot vector has
len(control_points) + p + 1 entries:
import numpy as np
from interpolatepy import BSpline
degree = 3
control_points = np.array(
[[0.0, 0.0], [1.0, 2.0], [2.0, -1.0], [3.0, 1.0], [4.0, 0.0]]
)
knots = BSpline.create_uniform_knots(degree, len(control_points))
curve = BSpline(degree, knots, control_points)
u = np.linspace(curve.u_min, curve.u_max, 150)
points = np.array([curve.evaluate(value) for value in u])
tangents = np.array([curve.evaluate_derivative(value, 1) for value in u])
assert points.shape == (150, 2)
assert tangents.shape == (150, 2)
Interpolate B-spline samples¶
BSplineInterpolator supports degree 3, 4, or 5, optional times, endpoint
derivatives, and cyclic constraints. As of 3.2.0, two samples are sufficient at
all valid degrees.
import numpy as np
from interpolatepy import BSplineInterpolator
points = np.array([[0.0, 0.0], [2.0, 1.0]])
times = np.array([0.0, 2.0])
curve = BSplineInterpolator(
degree=5,
points=points,
times=times,
initial_velocity=np.array([1.0, 0.0]),
)
assert np.allclose(curve.evaluate(0.0), points[0])
assert np.allclose(curve.evaluate(2.0), points[1])
assert np.allclose(curve.evaluate_derivative(0.0, 1), [1.0, 0.0])
Without explicit times, the class generates a parameter sequence. Use
curve.u_min and curve.u_max instead of assuming [0, 1].
Approximate or smooth vector data¶
Approximation reduces a point set to a selected number of controls:
import numpy as np
from interpolatepy import ApproximationBSpline
x = np.linspace(0.0, 2.0 * np.pi, 40)
points = np.column_stack((x, np.sin(x)))
curve = ApproximationBSpline(points, num_control_points=10, degree=3)
parameters, fitted = curve.generate_curve_points(120)
assert fitted.shape == (120, 2)
assert parameters.shape == (120,)
B-spline smoothing uses a configuration object:
import numpy as np
from interpolatepy import BSplineParams
from interpolatepy import SmoothingCubicBSpline
points = np.array(
[[0.0, 0.0], [1.0, 1.2], [2.0, 0.7], [3.0, 2.1], [4.0, 2.0]]
)
params = BSplineParams(mu=0.8, enforce_endpoints=True, auto_derivatives=True)
curve = SmoothingCubicBSpline(points, params)
errors = curve.calculate_approximation_error()
assert errors.shape == (len(points),)
Plotting¶
Scalar spline classes provide plot(). B-spline classes provide plot_2d()
and plot_3d() according to control-point dimension. These functions create or
populate Matplotlib axes; application code controls when to call plt.show().
Common mistakes¶
- Calling a zero-velocity cubic spline “natural.”
- Passing
a0andandirectly to method 2 instead of usingSplineParameters. - Sending an array to
BSpline.evaluate(); sample scalar parameters in a loop. - Evaluating outside
[u_min, u_max]. - Expecting approximation or smoothing curves to pass through every sample.
- Treating a curve derivative with respect to
uas a physical time derivative without applying the chain rule.