InterpolatePy 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.
- interpolatepy/__init__.py +8 -0
- interpolatepy/b_spline.py +737 -0
- interpolatepy/b_spline_approx.py +544 -0
- interpolatepy/b_spline_cubic.py +444 -0
- interpolatepy/b_spline_interpolate.py +515 -0
- interpolatepy/b_spline_smooth.py +639 -0
- interpolatepy/c_s_smoot_search.py +177 -0
- interpolatepy/c_s_smoothing.py +611 -0
- interpolatepy/c_s_with_acc1.py +643 -0
- interpolatepy/c_s_with_acc2.py +494 -0
- interpolatepy/cubic_spline.py +486 -0
- interpolatepy/double_s.py +580 -0
- interpolatepy/frenet_frame.py +245 -0
- interpolatepy/linear.py +107 -0
- interpolatepy/polynomials.py +451 -0
- interpolatepy/simple_paths.py +281 -0
- interpolatepy/trapezoidal.py +613 -0
- interpolatepy/tridiagonal_inv.py +96 -0
- interpolatepy/version.py +5 -0
- interpolatepy-1.0.0.dist-info/METADATA +415 -0
- interpolatepy-1.0.0.dist-info/RECORD +24 -0
- interpolatepy-1.0.0.dist-info/WHEEL +5 -0
- interpolatepy-1.0.0.dist-info/licenses/LICENSE +21 -0
- interpolatepy-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from interpolatepy.b_spline import BSpline
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# Define constant to replace magic number
|
|
9
|
+
EPSILON = 1e-10
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class BSplineParams:
|
|
14
|
+
"""Parameters for initializing a SmoothingCubicBSpline.
|
|
15
|
+
|
|
16
|
+
Attributes
|
|
17
|
+
----------
|
|
18
|
+
mu : float, default=0.5
|
|
19
|
+
Smoothing parameter between 0 and 1, where higher values give more importance to
|
|
20
|
+
fitting the data points exactly.
|
|
21
|
+
weights : array_like or None, default=None
|
|
22
|
+
Weights for each data point.
|
|
23
|
+
v0 : array_like or None, default=None
|
|
24
|
+
Tangent vector at the start point.
|
|
25
|
+
vn : array_like or None, default=None
|
|
26
|
+
Tangent vector at the end point.
|
|
27
|
+
method : str, default="chord_length"
|
|
28
|
+
Method for parameter calculation ('equally_spaced', 'chord_length', or 'centripetal').
|
|
29
|
+
enforce_endpoints : bool, default=False
|
|
30
|
+
Whether to enforce interpolation at the endpoints.
|
|
31
|
+
auto_derivatives : bool, default=False
|
|
32
|
+
Whether to automatically calculate derivatives at endpoints.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
mu: float = 0.5
|
|
36
|
+
weights: list | np.ndarray | None = None
|
|
37
|
+
v0: list | np.ndarray | None = None
|
|
38
|
+
vn: list | np.ndarray | None = None
|
|
39
|
+
method: str = "chord_length"
|
|
40
|
+
enforce_endpoints: bool = False
|
|
41
|
+
auto_derivatives: bool = False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SmoothingCubicBSpline(BSpline):
|
|
45
|
+
"""A class for creating smoothing cubic B-splines that approximate a set of points.
|
|
46
|
+
|
|
47
|
+
This class inherits from BSpline and implements the smoothing algorithm
|
|
48
|
+
described in Section 8.7 of the document. It creates a cubic B-spline
|
|
49
|
+
curve that balances between fitting the given points and maintaining smoothness.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
points: list | np.ndarray,
|
|
55
|
+
params: BSplineParams | None = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Initialize a smoothing cubic B-spline to approximate a set of points.
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
points : array_like
|
|
62
|
+
The points to approximate.
|
|
63
|
+
params : BSplineParams, optional
|
|
64
|
+
Configuration parameters. If None, default parameters will be used.
|
|
65
|
+
"""
|
|
66
|
+
# Initialize with default parameters if not provided
|
|
67
|
+
if params is None:
|
|
68
|
+
params = BSplineParams()
|
|
69
|
+
|
|
70
|
+
# Convert points to numpy array and ensure correct format
|
|
71
|
+
if not isinstance(points, np.ndarray):
|
|
72
|
+
points = np.array(points, dtype=np.float64)
|
|
73
|
+
else:
|
|
74
|
+
points = points.astype(np.float64)
|
|
75
|
+
|
|
76
|
+
# Get the number of points and the dimension
|
|
77
|
+
n_points = len(points)
|
|
78
|
+
if points.ndim == 1:
|
|
79
|
+
dimension = 1
|
|
80
|
+
# Reshape for a single point
|
|
81
|
+
points = points.reshape(-1, 1)
|
|
82
|
+
else:
|
|
83
|
+
dimension = points.shape[1]
|
|
84
|
+
|
|
85
|
+
# Store the points to approximate and related properties
|
|
86
|
+
self.approximation_points = points
|
|
87
|
+
self.n_approximation_points = n_points
|
|
88
|
+
self.dimension = dimension
|
|
89
|
+
self.mu = np.clip(params.mu, 0.0, 1.0) # Ensure mu is in [0, 1]
|
|
90
|
+
self.lambda_param = (1 - self.mu) / (6 * self.mu) if self.mu > 0 else float("inf")
|
|
91
|
+
|
|
92
|
+
# Set weights
|
|
93
|
+
if params.weights is None:
|
|
94
|
+
self.weights = np.ones(n_points, dtype=np.float64)
|
|
95
|
+
else:
|
|
96
|
+
if len(params.weights) != n_points:
|
|
97
|
+
raise ValueError(
|
|
98
|
+
f"Length of weights ({len(params.weights)}) must match "
|
|
99
|
+
f"the number of points ({n_points})"
|
|
100
|
+
)
|
|
101
|
+
self.weights = np.array(params.weights, dtype=np.float64)
|
|
102
|
+
|
|
103
|
+
# Calculate the parameters ūₖ
|
|
104
|
+
self.u_bars = self._calculate_parameters(params.method)
|
|
105
|
+
|
|
106
|
+
# Calculate the knot vector based on ūₖ as per Section 8.7
|
|
107
|
+
self.knots = self._calculate_knot_vector()
|
|
108
|
+
|
|
109
|
+
# Process endpoint derivatives for endpoint interpolation case
|
|
110
|
+
n = self.n_approximation_points - 1 # Index of the last point
|
|
111
|
+
self.enforce_endpoints = params.enforce_endpoints
|
|
112
|
+
self.auto_derivatives = params.auto_derivatives
|
|
113
|
+
|
|
114
|
+
if params.enforce_endpoints:
|
|
115
|
+
# Process v0
|
|
116
|
+
if params.v0 is None:
|
|
117
|
+
if params.auto_derivatives and n > 0:
|
|
118
|
+
# Calculate v0 = (q₁ - q₀) / (ū₁ - ū₀)
|
|
119
|
+
u_diff = self.u_bars[1] - self.u_bars[0]
|
|
120
|
+
if abs(u_diff) > EPSILON: # Avoid division by zero
|
|
121
|
+
self.v0 = (points[1] - points[0]) / u_diff
|
|
122
|
+
else:
|
|
123
|
+
self.v0 = np.zeros(dimension, dtype=np.float64)
|
|
124
|
+
else:
|
|
125
|
+
self.v0 = np.zeros(dimension, dtype=np.float64)
|
|
126
|
+
else:
|
|
127
|
+
# Convert to numpy array if it's not already
|
|
128
|
+
if not isinstance(params.v0, np.ndarray):
|
|
129
|
+
v0 = np.array(params.v0, dtype=np.float64)
|
|
130
|
+
else:
|
|
131
|
+
v0 = params.v0.astype(np.float64)
|
|
132
|
+
|
|
133
|
+
# Ensure correct shape
|
|
134
|
+
if v0.ndim == 0: # scalar
|
|
135
|
+
self.v0 = np.zeros(dimension, dtype=np.float64)
|
|
136
|
+
elif v0.ndim == 1 and len(v0) == dimension:
|
|
137
|
+
self.v0 = v0
|
|
138
|
+
else:
|
|
139
|
+
raise ValueError(f"v0 must be a vector of dimension {dimension}")
|
|
140
|
+
|
|
141
|
+
# Process vn
|
|
142
|
+
if params.vn is None:
|
|
143
|
+
if params.auto_derivatives and n > 0:
|
|
144
|
+
# Calculate vn = (qₙ - qₙ₋₁) / (ūₙ - ūₙ₋₁)
|
|
145
|
+
u_diff = self.u_bars[n] - self.u_bars[n - 1]
|
|
146
|
+
if abs(u_diff) > EPSILON: # Avoid division by zero
|
|
147
|
+
self.vn = (points[n] - points[n - 1]) / u_diff
|
|
148
|
+
else:
|
|
149
|
+
self.vn = np.zeros(dimension, dtype=np.float64)
|
|
150
|
+
else:
|
|
151
|
+
self.vn = np.zeros(dimension, dtype=np.float64)
|
|
152
|
+
else:
|
|
153
|
+
# Convert to numpy array if it's not already
|
|
154
|
+
if not isinstance(params.vn, np.ndarray):
|
|
155
|
+
vn = np.array(params.vn, dtype=np.float64)
|
|
156
|
+
else:
|
|
157
|
+
vn = params.vn.astype(np.float64)
|
|
158
|
+
|
|
159
|
+
# Ensure correct shape
|
|
160
|
+
if vn.ndim == 0: # scalar
|
|
161
|
+
self.vn = np.zeros(dimension, dtype=np.float64)
|
|
162
|
+
elif vn.ndim == 1 and len(vn) == dimension:
|
|
163
|
+
self.vn = vn
|
|
164
|
+
else:
|
|
165
|
+
raise ValueError(f"vn must be a vector of dimension {dimension}")
|
|
166
|
+
|
|
167
|
+
# First initialize parent BSpline with dummy control points
|
|
168
|
+
# We'll calculate real control points after initialization
|
|
169
|
+
n_control_points = n + 3 # For a cubic smoothing B-spline
|
|
170
|
+
dummy_control_points = np.zeros((n_control_points, dimension), dtype=np.float64)
|
|
171
|
+
super().__init__(3, self.knots, dummy_control_points)
|
|
172
|
+
|
|
173
|
+
# Calculate the actual control points according to Section 8.7
|
|
174
|
+
self.control_points = self._calculate_control_points()
|
|
175
|
+
|
|
176
|
+
def _calculate_parameters(self, method: str) -> np.ndarray:
|
|
177
|
+
"""Calculate the parameters ūₖ for each point using one of three methods.
|
|
178
|
+
|
|
179
|
+
Parameters
|
|
180
|
+
----------
|
|
181
|
+
method : str
|
|
182
|
+
Method for calculating the parameters.
|
|
183
|
+
Options are 'equally_spaced', 'chord_length', or 'centripetal'.
|
|
184
|
+
|
|
185
|
+
Returns
|
|
186
|
+
-------
|
|
187
|
+
np.ndarray
|
|
188
|
+
The parameters ūₖ.
|
|
189
|
+
|
|
190
|
+
Raises
|
|
191
|
+
------
|
|
192
|
+
ValueError
|
|
193
|
+
If an unknown method is provided.
|
|
194
|
+
"""
|
|
195
|
+
n = self.n_approximation_points - 1 # Index of the last point
|
|
196
|
+
|
|
197
|
+
# Initialize the parameters
|
|
198
|
+
u_bars = np.zeros(self.n_approximation_points, dtype=np.float64)
|
|
199
|
+
|
|
200
|
+
# Set the endpoints (equation 8.12)
|
|
201
|
+
u_bars[0] = 0.0
|
|
202
|
+
u_bars[n] = 1.0
|
|
203
|
+
|
|
204
|
+
if method == "equally_spaced":
|
|
205
|
+
# Equally spaced parameters (equation 8.12)
|
|
206
|
+
for k in range(1, n):
|
|
207
|
+
u_bars[k] = k / n
|
|
208
|
+
|
|
209
|
+
elif method == "chord_length":
|
|
210
|
+
# Chord length distribution (equation 8.13)
|
|
211
|
+
# Calculate total chord length
|
|
212
|
+
total_length = 0.0
|
|
213
|
+
for k in range(1, n + 1):
|
|
214
|
+
total_length += np.linalg.norm(
|
|
215
|
+
self.approximation_points[k] - self.approximation_points[k - 1]
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Calculate parameters
|
|
219
|
+
accumulated_length = 0.0
|
|
220
|
+
for k in range(1, n):
|
|
221
|
+
accumulated_length += np.linalg.norm(
|
|
222
|
+
self.approximation_points[k] - self.approximation_points[k - 1]
|
|
223
|
+
)
|
|
224
|
+
u_bars[k] = accumulated_length / total_length
|
|
225
|
+
|
|
226
|
+
elif method == "centripetal":
|
|
227
|
+
# Centripetal distribution (equation 8.14)
|
|
228
|
+
mu = 0.5 # As recommended in the document
|
|
229
|
+
|
|
230
|
+
# Calculate total "centripetal" length
|
|
231
|
+
total_length = 0.0
|
|
232
|
+
for k in range(1, n + 1):
|
|
233
|
+
total_length += (
|
|
234
|
+
np.linalg.norm(self.approximation_points[k] - self.approximation_points[k - 1])
|
|
235
|
+
** mu
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Calculate parameters
|
|
239
|
+
accumulated_length = 0.0
|
|
240
|
+
for k in range(1, n):
|
|
241
|
+
accumulated_length += (
|
|
242
|
+
np.linalg.norm(self.approximation_points[k] - self.approximation_points[k - 1])
|
|
243
|
+
** mu
|
|
244
|
+
)
|
|
245
|
+
u_bars[k] = accumulated_length / total_length
|
|
246
|
+
|
|
247
|
+
else:
|
|
248
|
+
raise ValueError(
|
|
249
|
+
f"Unknown method: {method}. Options are 'equally_spaced', "
|
|
250
|
+
f"'chord_length', or 'centripetal'."
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return u_bars
|
|
254
|
+
|
|
255
|
+
def _calculate_knot_vector(self) -> np.ndarray:
|
|
256
|
+
"""Calculate the knot vector based on the parameters ūₖ according to Section 8.7.
|
|
257
|
+
|
|
258
|
+
As described in the document:
|
|
259
|
+
u0 = ... = u2 = ū0, un+4 = ... = un+6 = ūn, uj+3 = ūj for j = 0, ..., n
|
|
260
|
+
|
|
261
|
+
Returns
|
|
262
|
+
-------
|
|
263
|
+
np.ndarray
|
|
264
|
+
The knot vector.
|
|
265
|
+
"""
|
|
266
|
+
n = self.n_approximation_points - 1 # Index of the last point
|
|
267
|
+
|
|
268
|
+
# Create the knot vector with n+7 elements (as described in Section 8.7)
|
|
269
|
+
knots = np.zeros(n + 7, dtype=np.float64)
|
|
270
|
+
|
|
271
|
+
# Set the first 3 knots to ū₀
|
|
272
|
+
knots[0:3] = self.u_bars[0]
|
|
273
|
+
|
|
274
|
+
# Set the last 3 knots to ūₙ
|
|
275
|
+
knots[-(3):] = self.u_bars[n]
|
|
276
|
+
|
|
277
|
+
# Set the middle knots to ūⱼ for j = 0, ..., n
|
|
278
|
+
for j in range(n + 1):
|
|
279
|
+
knots[j + 3] = self.u_bars[j]
|
|
280
|
+
|
|
281
|
+
return knots
|
|
282
|
+
|
|
283
|
+
def _construct_b_matrix(self) -> np.ndarray:
|
|
284
|
+
"""Construct the B matrix for the smoothing functional.
|
|
285
|
+
|
|
286
|
+
B contains the basis function values at each parameter ūₖ.
|
|
287
|
+
|
|
288
|
+
Returns
|
|
289
|
+
-------
|
|
290
|
+
np.ndarray
|
|
291
|
+
The B matrix.
|
|
292
|
+
"""
|
|
293
|
+
n = self.n_approximation_points - 1 # Index of the last point
|
|
294
|
+
m = n + 2 # Last index of control points (total of m+1 = n+3 points)
|
|
295
|
+
|
|
296
|
+
# Construct the B matrix as per Section 8.7
|
|
297
|
+
b_matrix = np.zeros((n + 1, m + 1), dtype=np.float64)
|
|
298
|
+
|
|
299
|
+
for k in range(n + 1):
|
|
300
|
+
u = self.u_bars[k]
|
|
301
|
+
try:
|
|
302
|
+
# Find the span and calculate basis functions
|
|
303
|
+
span = self.find_knot_span(u)
|
|
304
|
+
basis_vals = self.basis_functions(u, span)
|
|
305
|
+
|
|
306
|
+
# Fill in the B matrix row
|
|
307
|
+
for j in range(self.degree + 1):
|
|
308
|
+
b_matrix[k, span - self.degree + j] = basis_vals[j]
|
|
309
|
+
except Exception as e:
|
|
310
|
+
print(f"Error calculating basis functions at u={u}: {e}")
|
|
311
|
+
raise
|
|
312
|
+
|
|
313
|
+
return b_matrix
|
|
314
|
+
|
|
315
|
+
def _construct_a_matrix(self) -> np.ndarray:
|
|
316
|
+
"""Construct the A matrix as defined in equation (8.34).
|
|
317
|
+
|
|
318
|
+
This matrix is used for the smoothness term in the functional.
|
|
319
|
+
|
|
320
|
+
Returns
|
|
321
|
+
-------
|
|
322
|
+
np.ndarray
|
|
323
|
+
The A matrix.
|
|
324
|
+
"""
|
|
325
|
+
n = self.n_approximation_points - 1 # Index of the last point
|
|
326
|
+
size = n + 1 # Size of the A matrix for r₀, r₁, ..., rₙ
|
|
327
|
+
|
|
328
|
+
a_matrix = np.zeros((size, size), dtype=np.float64)
|
|
329
|
+
|
|
330
|
+
# Using the structure from equation (8.34)
|
|
331
|
+
# The diagonal terms 2(ui+3,i+2 + ui+4,i+3) and off-diagonal terms ui+4,i+3
|
|
332
|
+
for i in range(size):
|
|
333
|
+
# Calculate indices for proper knot differences
|
|
334
|
+
idx3 = i + 3 # u_{i+3}
|
|
335
|
+
idx2 = i + 2 # u_{i+2}
|
|
336
|
+
idx4 = i + 4 # u_{i+4}
|
|
337
|
+
|
|
338
|
+
# Ensure indices are within bounds
|
|
339
|
+
if idx4 < len(self.knots):
|
|
340
|
+
# Main diagonal term: 2(u_{i+3,i+2} + u_{i+4,i+3})
|
|
341
|
+
ui3_i2 = self.knots[idx3] - self.knots[idx2]
|
|
342
|
+
ui4_i3 = self.knots[idx4] - self.knots[idx3]
|
|
343
|
+
|
|
344
|
+
a_matrix[i, i] = 2 * (ui3_i2 + ui4_i3)
|
|
345
|
+
|
|
346
|
+
# Upper diagonal term: u_{i+4,i+3}
|
|
347
|
+
if i < size - 1:
|
|
348
|
+
a_matrix[i, i + 1] = ui4_i3
|
|
349
|
+
|
|
350
|
+
# Lower diagonal term: u_{i+3,i+2}
|
|
351
|
+
if i > 0:
|
|
352
|
+
a_matrix[i, i - 1] = ui3_i2
|
|
353
|
+
|
|
354
|
+
return a_matrix
|
|
355
|
+
|
|
356
|
+
def _construct_c_matrix(self) -> np.ndarray:
|
|
357
|
+
"""Construct the C matrix as defined in equations (8.35) and (8.36).
|
|
358
|
+
|
|
359
|
+
This matrix relates the control points p_j to the 2nd derivative control points r_j.
|
|
360
|
+
|
|
361
|
+
Returns
|
|
362
|
+
-------
|
|
363
|
+
np.ndarray
|
|
364
|
+
The C matrix.
|
|
365
|
+
"""
|
|
366
|
+
n = self.n_approximation_points - 1 # Index of the last point
|
|
367
|
+
size_r = n + 1 # Number of r₀, r₁, ..., rₙ
|
|
368
|
+
size_p = n + 3 # Number of p₀, p₁, ..., pₙ₊₂
|
|
369
|
+
|
|
370
|
+
c_matrix = np.zeros((size_r, size_p), dtype=np.float64)
|
|
371
|
+
|
|
372
|
+
# Using the coefficient definitions from equation (8.36)
|
|
373
|
+
for k in range(size_r):
|
|
374
|
+
# Calculate the knot indices
|
|
375
|
+
k1 = k + 1 # u_{k+1}
|
|
376
|
+
k2 = k + 2 # u_{k+2}
|
|
377
|
+
k4 = k + 4 # u_{k+4}
|
|
378
|
+
k5 = k + 5 # u_{k+5}
|
|
379
|
+
|
|
380
|
+
# Ensure indices are within bounds
|
|
381
|
+
if k4 < len(self.knots) and k5 < len(self.knots):
|
|
382
|
+
# Calculate knot differences
|
|
383
|
+
uk4_k2 = self.knots[k4] - self.knots[k2]
|
|
384
|
+
uk4_k1 = self.knots[k4] - self.knots[k1]
|
|
385
|
+
uk5_k2 = self.knots[k5] - self.knots[k2]
|
|
386
|
+
|
|
387
|
+
# Avoid division by zero
|
|
388
|
+
if abs(uk4_k2) > EPSILON and abs(uk4_k1) > EPSILON and abs(uk5_k2) > EPSILON:
|
|
389
|
+
# c_k,1 coefficient (for p_k)
|
|
390
|
+
if k < size_p:
|
|
391
|
+
c_matrix[k, k] = 6 / (uk4_k2 * uk4_k1)
|
|
392
|
+
|
|
393
|
+
# c_k,2 coefficient (for p_k+1)
|
|
394
|
+
if k + 1 < size_p:
|
|
395
|
+
c_matrix[k, k + 1] = -6 / uk4_k2 * (1 / uk4_k1 + 1 / uk5_k2)
|
|
396
|
+
|
|
397
|
+
# c_k,3 coefficient (for p_k+2)
|
|
398
|
+
if k + 2 < size_p:
|
|
399
|
+
c_matrix[k, k + 2] = 6 / (uk4_k2 * uk5_k2)
|
|
400
|
+
|
|
401
|
+
return c_matrix
|
|
402
|
+
|
|
403
|
+
def _calculate_control_points(self) -> np.ndarray:
|
|
404
|
+
"""Calculate the control points by minimizing the smoothing functional L.
|
|
405
|
+
|
|
406
|
+
Minimizes the smoothing functional as described in Section 8.7 of the document.
|
|
407
|
+
|
|
408
|
+
Returns
|
|
409
|
+
-------
|
|
410
|
+
np.ndarray
|
|
411
|
+
The control points.
|
|
412
|
+
"""
|
|
413
|
+
if self.enforce_endpoints:
|
|
414
|
+
return self._calculate_control_points_with_endpoints()
|
|
415
|
+
|
|
416
|
+
n = self.n_approximation_points - 1 # Index of the last point
|
|
417
|
+
|
|
418
|
+
# Construct the B matrix
|
|
419
|
+
b_matrix = self._construct_b_matrix()
|
|
420
|
+
|
|
421
|
+
# Construct the weight matrix W
|
|
422
|
+
w_matrix = np.diag(self.weights)
|
|
423
|
+
|
|
424
|
+
# Construct the A matrix for the smoothness term
|
|
425
|
+
a_matrix = self._construct_a_matrix()
|
|
426
|
+
|
|
427
|
+
# Construct the C matrix (relates control points to 2nd derivative)
|
|
428
|
+
c_matrix = self._construct_c_matrix()
|
|
429
|
+
|
|
430
|
+
# Calculate CTAC term as in equation (8.33)
|
|
431
|
+
ctac = c_matrix.T @ a_matrix @ c_matrix
|
|
432
|
+
|
|
433
|
+
# Solve the linear system to find the control points
|
|
434
|
+
# From equation (8.37): (BTW B + λ CTAC) P = BTW Q
|
|
435
|
+
left_side = b_matrix.T @ w_matrix @ b_matrix + self.lambda_param * ctac
|
|
436
|
+
right_side = b_matrix.T @ w_matrix @ self.approximation_points
|
|
437
|
+
|
|
438
|
+
# Solve the system for each dimension
|
|
439
|
+
control_points = np.zeros((n + 3, self.dimension), dtype=np.float64)
|
|
440
|
+
|
|
441
|
+
try:
|
|
442
|
+
for d in range(self.dimension):
|
|
443
|
+
control_points[:, d] = np.linalg.solve(left_side, right_side[:, d])
|
|
444
|
+
except np.linalg.LinAlgError as e:
|
|
445
|
+
print(f"Linear algebra error: {e}")
|
|
446
|
+
# Fall back to least squares solution
|
|
447
|
+
for d in range(self.dimension):
|
|
448
|
+
control_points[:, d] = np.linalg.lstsq(left_side, right_side[:, d], rcond=None)[0]
|
|
449
|
+
print("Using least squares solution instead of direct solve")
|
|
450
|
+
|
|
451
|
+
return control_points
|
|
452
|
+
|
|
453
|
+
def _calculate_control_points_with_endpoints(self) -> np.ndarray:
|
|
454
|
+
"""Calculate the control points when enforcing interpolation of endpoints.
|
|
455
|
+
|
|
456
|
+
Enforces interpolation of endpoints and their tangent directions,
|
|
457
|
+
as described in Section 8.7.1.
|
|
458
|
+
|
|
459
|
+
Returns
|
|
460
|
+
-------
|
|
461
|
+
np.ndarray
|
|
462
|
+
The control points.
|
|
463
|
+
"""
|
|
464
|
+
n = self.n_approximation_points - 1 # Index of the last point
|
|
465
|
+
|
|
466
|
+
# Initialize control points
|
|
467
|
+
control_points = np.zeros((n + 3, self.dimension), dtype=np.float64)
|
|
468
|
+
|
|
469
|
+
# Set p₀ and pₙ₊₂ directly from endpoints (equation at beginning of 8.7.1)
|
|
470
|
+
control_points[0] = self.approximation_points[0] # p₀ = q₀
|
|
471
|
+
control_points[n + 2] = self.approximation_points[n] # pₙ₊₂ = qₙ
|
|
472
|
+
|
|
473
|
+
# Calculate p₁ and pₙ₊₁ from tangent directions
|
|
474
|
+
# -p₀ + p₁ = (u₄/3) * d₁
|
|
475
|
+
u4 = self.knots[4]
|
|
476
|
+
control_points[1] = control_points[0] + (u4 / 3.0) * self.v0
|
|
477
|
+
|
|
478
|
+
# -pₙ₊₁ + pₙ₊₂ = ((1-uₙ₊₂)/3) * dₙ
|
|
479
|
+
un2 = self.knots[n + 2]
|
|
480
|
+
control_points[n + 1] = control_points[n + 2] - ((1.0 - un2) / 3.0) * self.vn
|
|
481
|
+
|
|
482
|
+
# If there are only 3 control points (p₀, p₁, p₂), we're done
|
|
483
|
+
if n <= 0:
|
|
484
|
+
return control_points
|
|
485
|
+
|
|
486
|
+
# For more control points, solve the reduced system for p₂, ..., pₙ
|
|
487
|
+
# as described in Section 8.7.1
|
|
488
|
+
|
|
489
|
+
# Construct the reduced Q vector
|
|
490
|
+
q_reduced = np.zeros((n - 1, self.dimension), dtype=np.float64)
|
|
491
|
+
for k in range(1, n):
|
|
492
|
+
q_reduced[k - 1] = self.approximation_points[k]
|
|
493
|
+
|
|
494
|
+
# Construct the reduced B matrix
|
|
495
|
+
b_reduced = np.zeros((n - 1, n - 1), dtype=np.float64)
|
|
496
|
+
for k in range(1, n):
|
|
497
|
+
u = self.u_bars[k]
|
|
498
|
+
try:
|
|
499
|
+
span = self.find_knot_span(u)
|
|
500
|
+
basis_vals = self.basis_functions(u, span)
|
|
501
|
+
|
|
502
|
+
# Subtract contribution of p₀ and p₁ from Q
|
|
503
|
+
if span - self.degree <= 0: # Basis function for p₀ is non-zero
|
|
504
|
+
q_reduced[k - 1] -= basis_vals[0] * control_points[0]
|
|
505
|
+
if span - self.degree + 1 <= 1: # Basis function for p₁ is non-zero
|
|
506
|
+
q_reduced[k - 1] -= basis_vals[1] * control_points[1]
|
|
507
|
+
|
|
508
|
+
# Subtract contribution of pₙ₊₁ and pₙ₊₂ from Q
|
|
509
|
+
if span >= n: # Basis function for pₙ₊₁ is non-zero
|
|
510
|
+
basis_idx = n + 1 - (span - self.degree)
|
|
511
|
+
if 0 <= basis_idx < len(basis_vals):
|
|
512
|
+
q_reduced[k - 1] -= basis_vals[basis_idx] * control_points[n + 1]
|
|
513
|
+
if span >= n + 1: # Basis function for pₙ₊₂ is non-zero
|
|
514
|
+
basis_idx = n + 2 - (span - self.degree)
|
|
515
|
+
if 0 <= basis_idx < len(basis_vals):
|
|
516
|
+
q_reduced[k - 1] -= basis_vals[basis_idx] * control_points[n + 2]
|
|
517
|
+
|
|
518
|
+
# Fill B_reduced for interior control points p₂ to pₙ
|
|
519
|
+
for j in range(2, n + 1):
|
|
520
|
+
basis_idx = j - (span - self.degree)
|
|
521
|
+
if 0 <= basis_idx < len(basis_vals):
|
|
522
|
+
col = j - 2 # Index in B_reduced
|
|
523
|
+
if 0 <= col < n - 1:
|
|
524
|
+
b_reduced[k - 1, col] = basis_vals[basis_idx]
|
|
525
|
+
except Exception as e:
|
|
526
|
+
print(f"Error calculating reduced matrices at u={u}: {e}")
|
|
527
|
+
raise
|
|
528
|
+
|
|
529
|
+
# Construct the reduced weight matrix W
|
|
530
|
+
w_reduced = np.diag(self.weights[1:n])
|
|
531
|
+
|
|
532
|
+
# Construct the A and C matrices for the smoothness term
|
|
533
|
+
# For the reduced case, we need to modify these matrices
|
|
534
|
+
a_matrix = self._construct_a_matrix()
|
|
535
|
+
c_matrix = self._construct_c_matrix()
|
|
536
|
+
|
|
537
|
+
# Extract the parts of C that relate to p₂...pₙ
|
|
538
|
+
c_reduced = c_matrix[:, 2 : n + 1]
|
|
539
|
+
|
|
540
|
+
# Calculate PZ vector (from Section 8.7.1)
|
|
541
|
+
pz = np.zeros((n + 1, self.dimension))
|
|
542
|
+
# Add terms for p₀ and p₁
|
|
543
|
+
pz[0] = c_matrix[0, 0] * control_points[0] + c_matrix[0, 1] * control_points[1]
|
|
544
|
+
# Add terms for pₙ₊₁ and pₙ₊₂
|
|
545
|
+
pz[n] = (
|
|
546
|
+
c_matrix[n, n + 1] * control_points[n + 1] + c_matrix[n, n + 2] * control_points[n + 2]
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
# Calculate the CTAC term for reduced system
|
|
550
|
+
ctac_reduced = c_reduced.T @ a_matrix @ c_reduced
|
|
551
|
+
|
|
552
|
+
# Calculate the CTAT*PZ term
|
|
553
|
+
ctat_pz = c_reduced.T @ a_matrix @ pz
|
|
554
|
+
|
|
555
|
+
# Solve the linear system for the interior control points
|
|
556
|
+
# (BTW B + λ CTAC) P = BTW Q - λ CTAT*PZ
|
|
557
|
+
left_side = b_reduced.T @ w_reduced @ b_reduced + self.lambda_param * ctac_reduced
|
|
558
|
+
right_side = b_reduced.T @ w_reduced @ q_reduced - self.lambda_param * ctat_pz
|
|
559
|
+
|
|
560
|
+
try:
|
|
561
|
+
# Solve the system for each dimension
|
|
562
|
+
for d in range(self.dimension):
|
|
563
|
+
interior_controls = np.linalg.solve(left_side, right_side[:, d])
|
|
564
|
+
control_points[2 : n + 1, d] = interior_controls
|
|
565
|
+
except np.linalg.LinAlgError as e:
|
|
566
|
+
print(f"Linear algebra error in reduced system: {e}")
|
|
567
|
+
# Fall back to least squares solution
|
|
568
|
+
for d in range(self.dimension):
|
|
569
|
+
interior_controls = np.linalg.lstsq(left_side, right_side[:, d], rcond=None)[0]
|
|
570
|
+
control_points[2 : n + 1, d] = interior_controls
|
|
571
|
+
print("Using least squares solution for reduced system")
|
|
572
|
+
|
|
573
|
+
return control_points
|
|
574
|
+
|
|
575
|
+
def calculate_approximation_error(self) -> np.ndarray:
|
|
576
|
+
"""Calculate the approximation error for each point.
|
|
577
|
+
|
|
578
|
+
Returns
|
|
579
|
+
-------
|
|
580
|
+
np.ndarray
|
|
581
|
+
Array with error values for each approximation point.
|
|
582
|
+
"""
|
|
583
|
+
errors = np.zeros(self.n_approximation_points, dtype=np.float64)
|
|
584
|
+
|
|
585
|
+
for k in range(self.n_approximation_points):
|
|
586
|
+
# Evaluate the B-spline at the parameter value
|
|
587
|
+
u = self.u_bars[k]
|
|
588
|
+
point = self.evaluate(u)
|
|
589
|
+
|
|
590
|
+
# Calculate the error (Euclidean distance)
|
|
591
|
+
errors[k] = np.linalg.norm(point - self.approximation_points[k])
|
|
592
|
+
|
|
593
|
+
return errors
|
|
594
|
+
|
|
595
|
+
def calculate_total_error(self) -> float:
|
|
596
|
+
"""Calculate the total weighted approximation error.
|
|
597
|
+
|
|
598
|
+
Returns
|
|
599
|
+
-------
|
|
600
|
+
float
|
|
601
|
+
The total weighted error.
|
|
602
|
+
"""
|
|
603
|
+
total_error = 0.0
|
|
604
|
+
|
|
605
|
+
for k in range(self.n_approximation_points):
|
|
606
|
+
# Evaluate the B-spline at the parameter value
|
|
607
|
+
u = self.u_bars[k]
|
|
608
|
+
point = self.evaluate(u)
|
|
609
|
+
|
|
610
|
+
# Calculate the squared error weighted by w_k
|
|
611
|
+
diff = point - self.approximation_points[k]
|
|
612
|
+
total_error += self.weights[k] * np.sum(diff**2)
|
|
613
|
+
|
|
614
|
+
return total_error
|
|
615
|
+
|
|
616
|
+
def calculate_smoothness_measure(self, num_points: int = 100) -> float:
|
|
617
|
+
"""Calculate the smoothness measure (integral of squared second derivative).
|
|
618
|
+
|
|
619
|
+
Parameters
|
|
620
|
+
----------
|
|
621
|
+
num_points : int, default=100
|
|
622
|
+
Number of points to use for numerical integration.
|
|
623
|
+
|
|
624
|
+
Returns
|
|
625
|
+
-------
|
|
626
|
+
float
|
|
627
|
+
The smoothness measure.
|
|
628
|
+
"""
|
|
629
|
+
# Generate parameter values for numerical integration
|
|
630
|
+
u_values = np.linspace(self.u_min, self.u_max, num_points)
|
|
631
|
+
du = (self.u_max - self.u_min) / (num_points - 1)
|
|
632
|
+
|
|
633
|
+
# Calculate the second derivative at each parameter value
|
|
634
|
+
smoothness = 0.0
|
|
635
|
+
for u in u_values:
|
|
636
|
+
d2 = self.evaluate_derivative(u, order=2)
|
|
637
|
+
smoothness += np.sum(d2**2) * du
|
|
638
|
+
|
|
639
|
+
return smoothness
|