Source code for turbo_moc.geometry.spline_tool
"""
turbo_moc.geometry.spline_tool -- B-spline fitting/evaluation used by
turbo_moc.geometry.blade to reduce a dense blade-profile polyline to a small set
of control points (for CAD/optimization use) and reconstruct a smooth
curve from them.
Originally ported from opt_framework/test_2/01_geometry_parametrization/utils/
spline_gen.py (BSplineTool). Basis assembly/evaluation use SciPy's compiled routines;
parameterization, normal-equation fitting, and endpoint conventions are retained.
"""
import numpy as np
from scipy import linalg
from scipy.interpolate import BSpline
__all__ = ["BSplineTool"]
[docs]
class BSplineTool:
def __init__(self, degree=3):
"""
Initializes the B-Spline tool with a specific degree (default k=3).
"""
self.n = degree
def _get_basis(self, i, k, u, knots):
"""
Recursive Cox-de Boor algorithm to compute basis functions.
"""
if k == 0:
return 1.0 if knots[i - 1] <= u < knots[i] else 0.0
denom1 = knots[i + k - 1] - knots[i - 1]
denom2 = knots[i + k] - knots[i]
val = 0
if denom1 != 0:
val += (u - knots[i - 1]) / denom1 * self._get_basis(i, k - 1, u, knots)
if denom2 != 0:
val += (knots[i + k] - u) / denom2 * self._get_basis(i + 1, k - 1, u, knots)
return val
def _get_params_and_knots(self, data_points, num_cp):
P_len = len(data_points)
# Centripetal Parameterization
dists = np.sqrt(np.sum(np.diff(data_points, axis=0) ** 2, axis=1))
w = np.zeros(P_len)
w[1:] = np.cumsum(np.power(dists, 0.5))
w /= w[-1]
# Clamped Knot Sequence (Ensures endpoint interpolation)
knots = np.concatenate([
np.zeros(self.n + 1),
np.linspace(0, 1, num_cp - self.n + 1)[1:-1],
np.ones(self.n + 1)
])
return w, knots
[docs]
def reparameterize_equidistant(self, data_points, num_points=500):
"""
Re-samples data points so they are physically equidistant along the arc length.
Prevents clustered data from distorting CP distribution.
"""
# Calculate cumulative arc length
dists = np.sqrt(np.sum(np.diff(data_points, axis=0) ** 2, axis=1))
arc_length = np.concatenate(([0], np.cumsum(dists)))
# Create a uniform distribution of length
uniform_lengths = np.linspace(0, arc_length[-1], num_points)
# Interpolate to find new equidistant (x, y) coordinates
new_x = np.interp(uniform_lengths, arc_length, data_points[:, 0])
new_y = np.interp(uniform_lengths, arc_length, data_points[:, 1])
return np.vstack((new_x, new_y)).T
[docs]
def backward(self, data_points, num_cp):
"""
Generates control points from data points using Least Squares.
Uses a Uniform Knot Sequence for equispaced control variables.
"""
P = len(data_points)
# 1. Centripetal Parameterization
dists = np.sqrt(np.sum(np.diff(data_points, axis=0) ** 2, axis=1))
w = np.zeros(P)
w[1:] = np.cumsum(np.power(dists, 0.5))
w /= w[-1]
# 2. Knot Sequence
knots = np.concatenate([
np.zeros(self.n + 1),
np.linspace(0, 1, num_cp - self.n + 1)[1:-1],
np.ones(self.n + 1)
])
# 3. Build Linear System A*x = B solved with Cholesky
N_mat = self._basis_matrix(w, knots)
A = N_mat.T @ N_mat
B = N_mat.T @ data_points
L_cho = linalg.cholesky(A, lower=True)
control_points = linalg.cho_solve((L_cho, True), B)
return control_points, knots
# The following employs the backward approach while fixing the start and end points
[docs]
def backward_fixed(self, data_points, num_cp):
w, knots = self._get_params_and_knots(data_points, num_cp)
N_mat = self._basis_matrix(w, knots)
# Force d0 and dL to match first and last data points
d0, dL = data_points[0], data_points[-1]
target_B = data_points - (np.outer(N_mat[:, 0], d0) + np.outer(N_mat[:, -1], dL))
N_inner = N_mat[:, 1:-1]
A_red = N_inner.T @ N_inner
B_red = N_inner.T @ target_B
L_cho = linalg.cholesky(A_red, lower=True)
inner_cps = linalg.cho_solve((L_cho, True), B_red)
control_points = np.vstack([d0, inner_cps, dL])
return control_points, knots
[docs]
def forward(self, control_points, knots, num_points=500):
"""
Generates the curve from control points (Forward Method).
"""
u_range = np.linspace(knots[self.n], knots[-self.n - 1], num_points)
# Preserve the legacy upper-end sampling so CAD endpoints and existing
# fitted contours do not change during this performance refactor.
u_range[u_range == knots[-self.n - 1]] -= 1e-10
return BSpline(knots, np.asarray(control_points), self.n)(u_range)
def _basis_matrix(self, parameters, knots):
"""Compiled basis evaluation with the historical half-open endpoint."""
matrix = BSpline.design_matrix(parameters, knots, self.n).toarray()
# The recursive reference returns zero for all bases at the upper knot.
# Retain that convention in both fit methods (including unconstrained
# fits), rather than changing the least-squares problem accidentally.
matrix[np.asarray(parameters) == knots[-self.n - 1]] = 0.0
return matrix