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,544 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from interpolatepy.b_spline import BSpline
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ApproximationBSpline(BSpline):
|
|
7
|
+
"""A class for B-spline curve approximation of a set of points.
|
|
8
|
+
|
|
9
|
+
Inherits from BSpline class.
|
|
10
|
+
|
|
11
|
+
The approximation follows the theory described in Section 8.5 of the reference:
|
|
12
|
+
- The end points are exactly interpolated
|
|
13
|
+
- The internal points are approximated in the least squares sense
|
|
14
|
+
- Degree 3 (cubic) is typically used to ensure C2 continuity
|
|
15
|
+
|
|
16
|
+
Attributes
|
|
17
|
+
----------
|
|
18
|
+
original_points : np.ndarray
|
|
19
|
+
The original points being approximated.
|
|
20
|
+
original_parameters : np.ndarray
|
|
21
|
+
The parameter values corresponding to original points.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__( # noqa: PLR0913
|
|
25
|
+
self,
|
|
26
|
+
points: list | np.ndarray,
|
|
27
|
+
num_control_points: int,
|
|
28
|
+
*, # Make remaining parameters keyword-only
|
|
29
|
+
degree: int = 3,
|
|
30
|
+
weights: list | np.ndarray | None = None,
|
|
31
|
+
method: str = "chord_length",
|
|
32
|
+
debug: bool = False,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Initialize an approximation B-spline.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
points : list or np.ndarray
|
|
39
|
+
The points to approximate.
|
|
40
|
+
num_control_points : int
|
|
41
|
+
The number of control points to use.
|
|
42
|
+
degree : int, default=3
|
|
43
|
+
The degree of the B-spline. Defaults to 3 for cubic.
|
|
44
|
+
weights : list or np.ndarray or None, default=None
|
|
45
|
+
Weights for points in approximation. If None, uniform weighting is used.
|
|
46
|
+
method : str, default="chord_length"
|
|
47
|
+
Method for parameter calculation. Options are 'equally_spaced',
|
|
48
|
+
'chord_length', or 'centripetal'.
|
|
49
|
+
debug : bool, default=False
|
|
50
|
+
Whether to print debug information.
|
|
51
|
+
|
|
52
|
+
Raises
|
|
53
|
+
------
|
|
54
|
+
ValueError
|
|
55
|
+
If inputs do not satisfy approximation requirements.
|
|
56
|
+
"""
|
|
57
|
+
# Validate inputs
|
|
58
|
+
if degree < 1:
|
|
59
|
+
raise ValueError("Degree must be at least 1")
|
|
60
|
+
if num_control_points <= degree:
|
|
61
|
+
raise ValueError("Number of control points must be greater than the degree")
|
|
62
|
+
if len(points) <= num_control_points:
|
|
63
|
+
raise ValueError("Number of points must be greater than number of control points")
|
|
64
|
+
|
|
65
|
+
# Store debug flag
|
|
66
|
+
self.debug = debug
|
|
67
|
+
|
|
68
|
+
if self.debug:
|
|
69
|
+
print("\n" + "=" * 50)
|
|
70
|
+
print("INITIALIZING APPROXIMATION B-SPLINE")
|
|
71
|
+
print(f"Degree: {degree}")
|
|
72
|
+
print(f"Number of control points: {num_control_points}")
|
|
73
|
+
print(f"Number of points to approximate: {len(points)}")
|
|
74
|
+
print(f"Parameterization method: {method}")
|
|
75
|
+
print("=" * 50)
|
|
76
|
+
|
|
77
|
+
# Convert points to numpy array if needed
|
|
78
|
+
if not isinstance(points, np.ndarray):
|
|
79
|
+
points = np.array(points, dtype=np.float64)
|
|
80
|
+
|
|
81
|
+
# Set default weights if not provided (uniform weights)
|
|
82
|
+
n = len(points) - 1 # Number of points minus 1
|
|
83
|
+
if weights is None:
|
|
84
|
+
weights = np.ones(n - 1) # Exclude first and last points
|
|
85
|
+
elif not isinstance(weights, np.ndarray):
|
|
86
|
+
weights = np.array(weights, dtype=np.float64)
|
|
87
|
+
|
|
88
|
+
# Calculate parameter values for the points
|
|
89
|
+
u_bar = self._compute_parameters(points, method)
|
|
90
|
+
|
|
91
|
+
# Calculate knot vector
|
|
92
|
+
knots = self._compute_knots(degree, num_control_points, len(points), u_bar)
|
|
93
|
+
|
|
94
|
+
# Calculate control points using least squares approximation
|
|
95
|
+
control_points = self._approximate_control_points(
|
|
96
|
+
points, degree, knots, u_bar, num_control_points, weights
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Initialize parent class with calculated values
|
|
100
|
+
super().__init__(degree, knots, control_points)
|
|
101
|
+
|
|
102
|
+
# Store the original points and parameter values for reference
|
|
103
|
+
self.original_points = points
|
|
104
|
+
self.original_parameters = u_bar
|
|
105
|
+
|
|
106
|
+
if self.debug:
|
|
107
|
+
print("\nFINAL RESULTS:")
|
|
108
|
+
print(f"Degree: {degree}")
|
|
109
|
+
print(f"Number of control points: {len(control_points)}")
|
|
110
|
+
print(f"Knot vector: {knots}")
|
|
111
|
+
print("\nControl points:")
|
|
112
|
+
for i, cp in enumerate(control_points):
|
|
113
|
+
print(f" P{i}: {cp}")
|
|
114
|
+
|
|
115
|
+
def _compute_parameters(self, points: np.ndarray, method: str = "chord_length") -> np.ndarray:
|
|
116
|
+
"""Calculate parameter values using one of three methods.
|
|
117
|
+
|
|
118
|
+
Parameters
|
|
119
|
+
----------
|
|
120
|
+
points : np.ndarray
|
|
121
|
+
The points to approximate.
|
|
122
|
+
method : str, default="chord_length"
|
|
123
|
+
Method for calculating the parameters. Options are 'equally_spaced',
|
|
124
|
+
'chord_length', or 'centripetal'.
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
np.ndarray
|
|
129
|
+
Parameter values for each point, normalized to [0, 1].
|
|
130
|
+
|
|
131
|
+
Raises
|
|
132
|
+
------
|
|
133
|
+
ValueError
|
|
134
|
+
If an unknown method is provided.
|
|
135
|
+
"""
|
|
136
|
+
n = len(points) - 1 # Index of the last point
|
|
137
|
+
|
|
138
|
+
# Initialize the parameters
|
|
139
|
+
u_bar = np.zeros(n + 1, dtype=np.float64)
|
|
140
|
+
|
|
141
|
+
# Set the endpoints
|
|
142
|
+
u_bar[0] = 0.0
|
|
143
|
+
u_bar[n] = 1.0
|
|
144
|
+
|
|
145
|
+
if method == "equally_spaced":
|
|
146
|
+
# Equally spaced parameters
|
|
147
|
+
for k in range(1, n):
|
|
148
|
+
u_bar[k] = k / n
|
|
149
|
+
|
|
150
|
+
elif method == "chord_length":
|
|
151
|
+
# Chord length distribution
|
|
152
|
+
# Calculate total chord length
|
|
153
|
+
total_length = 0.0
|
|
154
|
+
for k in range(1, n + 1):
|
|
155
|
+
total_length += np.linalg.norm(points[k] - points[k - 1])
|
|
156
|
+
|
|
157
|
+
# Calculate parameters
|
|
158
|
+
for k in range(1, n):
|
|
159
|
+
u_bar[k] = u_bar[k - 1] + np.linalg.norm(points[k] - points[k - 1]) / total_length
|
|
160
|
+
|
|
161
|
+
elif method == "centripetal":
|
|
162
|
+
# Centripetal distribution
|
|
163
|
+
mu = 0.5 # As recommended in the document
|
|
164
|
+
|
|
165
|
+
# Calculate total "centripetal" length
|
|
166
|
+
total_length = 0.0
|
|
167
|
+
for k in range(1, n + 1):
|
|
168
|
+
total_length += np.linalg.norm(points[k] - points[k - 1]) ** mu
|
|
169
|
+
|
|
170
|
+
# Calculate parameters
|
|
171
|
+
for k in range(1, n):
|
|
172
|
+
u_bar[k] = (
|
|
173
|
+
u_bar[k - 1] + np.linalg.norm(points[k] - points[k - 1]) ** mu / total_length
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
else:
|
|
177
|
+
raise ValueError(
|
|
178
|
+
f"Unknown method: {method}. Options are 'equally_spaced', "
|
|
179
|
+
f"'chord_length', or 'centripetal'."
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if hasattr(self, "debug") and self.debug:
|
|
183
|
+
print(f"\nPARAMETER VALUES (using '{method}' method):")
|
|
184
|
+
for i, u in enumerate(u_bar):
|
|
185
|
+
print(f" u_bar[{i}] = {u:.6f}")
|
|
186
|
+
|
|
187
|
+
return u_bar
|
|
188
|
+
|
|
189
|
+
def _compute_knots(
|
|
190
|
+
self, degree: int, num_control_points: int, num_points: int, u_bar: np.ndarray
|
|
191
|
+
) -> np.ndarray:
|
|
192
|
+
"""Compute knot vector following the algorithm in Section 8.5.1.
|
|
193
|
+
|
|
194
|
+
Parameters
|
|
195
|
+
----------
|
|
196
|
+
degree : int
|
|
197
|
+
The degree of the B-spline.
|
|
198
|
+
num_control_points : int
|
|
199
|
+
The number of control points.
|
|
200
|
+
num_points : int
|
|
201
|
+
The number of points to approximate.
|
|
202
|
+
u_bar : np.ndarray
|
|
203
|
+
Parameter values for the points.
|
|
204
|
+
|
|
205
|
+
Returns
|
|
206
|
+
-------
|
|
207
|
+
np.ndarray
|
|
208
|
+
The knot vector.
|
|
209
|
+
"""
|
|
210
|
+
# Total number of knots
|
|
211
|
+
num_knots = num_control_points + degree + 1
|
|
212
|
+
|
|
213
|
+
if hasattr(self, "debug") and self.debug:
|
|
214
|
+
print("\nKNOT VECTOR CALCULATION:")
|
|
215
|
+
print(f" Number of knots needed: {num_knots}")
|
|
216
|
+
|
|
217
|
+
# Initialize knot vector
|
|
218
|
+
knots = np.zeros(num_knots)
|
|
219
|
+
|
|
220
|
+
# Set the first and last knots with multiplicity p+1 to ensure interpolation
|
|
221
|
+
# of end points as specified in the document
|
|
222
|
+
knots[: degree + 1] = u_bar[0]
|
|
223
|
+
knots[-(degree + 1) :] = u_bar[-1]
|
|
224
|
+
|
|
225
|
+
# Compute internal knots using the method from Section 8.5.1
|
|
226
|
+
n = num_points - 1 # Number of points minus 1
|
|
227
|
+
m = num_control_points - 1 # Number of control points minus 1
|
|
228
|
+
|
|
229
|
+
# Formula from the document: d = (n+1)/(m-p+1)
|
|
230
|
+
d = (n + 1) / (m - degree + 1)
|
|
231
|
+
|
|
232
|
+
if hasattr(self, "debug") and self.debug:
|
|
233
|
+
print(f" d = (n+1)/(m-p+1) = ({n + 1})/({m}-{degree}+1) = {d:.6f}")
|
|
234
|
+
|
|
235
|
+
# Compute internal knots using the algorithm from the document
|
|
236
|
+
# For j=1,...,m-p compute:
|
|
237
|
+
# i = floor(j*d)
|
|
238
|
+
# a = j*d - i
|
|
239
|
+
# u_(j+p) = (1-a)ū_(i-1) + aū_i
|
|
240
|
+
for j in range(1, m - degree + 1):
|
|
241
|
+
i = int(j * d) # floor(j*d)
|
|
242
|
+
alpha = j * d - i # j*d - floor(j*d)
|
|
243
|
+
knots[j + degree] = (1 - alpha) * u_bar[i - 1] + alpha * u_bar[i]
|
|
244
|
+
|
|
245
|
+
if hasattr(self, "debug") and self.debug:
|
|
246
|
+
print(f" j={j}, i=floor({j}*{d:.6f})={i}, a={alpha:.6f}")
|
|
247
|
+
print(
|
|
248
|
+
f" u_{j + degree} = (1-{alpha:.6f})*{u_bar[i - 1]:.6f} + "
|
|
249
|
+
f"{alpha:.6f}*{u_bar[i]:.6f} = {knots[j + degree]:.6f}"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
if hasattr(self, "debug") and self.debug:
|
|
253
|
+
print("\nFINAL KNOT VECTOR:")
|
|
254
|
+
knot_str = " ["
|
|
255
|
+
for k in knots:
|
|
256
|
+
knot_str += f"{k:.6f}, "
|
|
257
|
+
knot_str = knot_str[:-2] + "]"
|
|
258
|
+
print(knot_str)
|
|
259
|
+
|
|
260
|
+
return knots
|
|
261
|
+
|
|
262
|
+
def _approximate_control_points( # noqa: PLR0917, PLR0913
|
|
263
|
+
self,
|
|
264
|
+
points: np.ndarray,
|
|
265
|
+
degree: int,
|
|
266
|
+
knots: np.ndarray,
|
|
267
|
+
u_bar: np.ndarray,
|
|
268
|
+
num_control_points: int,
|
|
269
|
+
weights: np.ndarray,
|
|
270
|
+
) -> np.ndarray:
|
|
271
|
+
"""Compute control points using least squares approximation.
|
|
272
|
+
|
|
273
|
+
Uses the approach described in Section 8.5.
|
|
274
|
+
|
|
275
|
+
Parameters
|
|
276
|
+
----------
|
|
277
|
+
points : np.ndarray
|
|
278
|
+
The points to approximate.
|
|
279
|
+
degree : int
|
|
280
|
+
The degree of the B-spline.
|
|
281
|
+
knots : np.ndarray
|
|
282
|
+
The knot vector.
|
|
283
|
+
u_bar : np.ndarray
|
|
284
|
+
Parameter values for the points.
|
|
285
|
+
num_control_points : int
|
|
286
|
+
The number of control points.
|
|
287
|
+
weights : np.ndarray
|
|
288
|
+
Weights for points in approximation.
|
|
289
|
+
|
|
290
|
+
Returns
|
|
291
|
+
-------
|
|
292
|
+
np.ndarray
|
|
293
|
+
The control points.
|
|
294
|
+
"""
|
|
295
|
+
n = len(points) - 1 # Number of points minus 1
|
|
296
|
+
m = num_control_points - 1 # Number of control points minus 1
|
|
297
|
+
|
|
298
|
+
if hasattr(self, "debug") and self.debug:
|
|
299
|
+
print("\nCONTROL POINTS CALCULATION:")
|
|
300
|
+
print(f" n = {n} (number of points minus 1)")
|
|
301
|
+
print(f" m = {m} (number of control points minus 1)")
|
|
302
|
+
|
|
303
|
+
# Initialize control points
|
|
304
|
+
control_points = np.zeros((num_control_points, points.shape[1]))
|
|
305
|
+
|
|
306
|
+
# First and last control points are fixed to first and last data points
|
|
307
|
+
# as per condition 1 in Section 8.5
|
|
308
|
+
control_points[0] = points[0]
|
|
309
|
+
control_points[m] = points[n]
|
|
310
|
+
|
|
311
|
+
if hasattr(self, "debug") and self.debug:
|
|
312
|
+
print(" Fixed control points:")
|
|
313
|
+
print(f" P_0 = {points[0]}")
|
|
314
|
+
print(f" P_{m} = {points[n]}")
|
|
315
|
+
|
|
316
|
+
# Handle case where we only have 2 control points
|
|
317
|
+
if m <= 1:
|
|
318
|
+
if hasattr(self, "debug") and self.debug:
|
|
319
|
+
print(" Only two control points needed, returning interpolated curve.")
|
|
320
|
+
return control_points
|
|
321
|
+
|
|
322
|
+
# Create a temporary B-spline for basis function calculation
|
|
323
|
+
temp_control_points = np.zeros((num_control_points, points.shape[1]))
|
|
324
|
+
temp_bspline = BSpline(degree, knots, temp_control_points)
|
|
325
|
+
|
|
326
|
+
# Initialize matrices for internal points
|
|
327
|
+
# B is (n-1) x (m-1) matrix as in equation (8.21)
|
|
328
|
+
b_matrix = np.zeros((n - 1, m - 1))
|
|
329
|
+
# R is (n-1) x d matrix where d is the dimension of points
|
|
330
|
+
r_matrix = np.zeros((n - 1, points.shape[1]))
|
|
331
|
+
|
|
332
|
+
if hasattr(self, "debug") and self.debug:
|
|
333
|
+
print(f" B matrix shape: {b_matrix.shape}")
|
|
334
|
+
print(f" R matrix shape: {r_matrix.shape}")
|
|
335
|
+
|
|
336
|
+
# For each internal parameter value (k=1 to n-1)
|
|
337
|
+
for k in range(1, n):
|
|
338
|
+
u = u_bar[k]
|
|
339
|
+
|
|
340
|
+
if hasattr(self, "debug") and self.debug:
|
|
341
|
+
print(f"\n Processing point {k} at parameter u = {u:.6f}")
|
|
342
|
+
|
|
343
|
+
# Calculate all basis function values at parameter u
|
|
344
|
+
# This evaluates all basis functions B_j^p(u_k) for j=0,...,m
|
|
345
|
+
all_basis = np.zeros(m + 1)
|
|
346
|
+
|
|
347
|
+
# Find the knot span that contains u
|
|
348
|
+
span = temp_bspline.find_knot_span(u)
|
|
349
|
+
|
|
350
|
+
if hasattr(self, "debug") and self.debug:
|
|
351
|
+
print(f" Knot span for u = {u:.6f} is {span}")
|
|
352
|
+
|
|
353
|
+
# Get non-zero basis functions at this parameter
|
|
354
|
+
basis_values = temp_bspline.basis_functions(u, span)
|
|
355
|
+
|
|
356
|
+
if hasattr(self, "debug") and self.debug:
|
|
357
|
+
print(f" Non-zero basis values: {basis_values}")
|
|
358
|
+
|
|
359
|
+
# Map the basis functions to the correct indices in all_basis
|
|
360
|
+
# Only p+1 basis functions are non-zero at any parameter value
|
|
361
|
+
for j in range(degree + 1):
|
|
362
|
+
idx = span - degree + j
|
|
363
|
+
if 0 <= idx <= m:
|
|
364
|
+
all_basis[idx] = basis_values[j]
|
|
365
|
+
|
|
366
|
+
if hasattr(self, "debug") and self.debug:
|
|
367
|
+
print(" All basis function values:")
|
|
368
|
+
for j, val in enumerate(all_basis):
|
|
369
|
+
print(f" B_{j}^{degree}({u:.6f}) = {val:.6f}")
|
|
370
|
+
|
|
371
|
+
# Fill the k-th row of matrix B with values for internal control points (j=1 to m-1)
|
|
372
|
+
# Exactly as in Equation (8.21)
|
|
373
|
+
for j in range(1, m):
|
|
374
|
+
b_matrix[k - 1, j - 1] = all_basis[j]
|
|
375
|
+
|
|
376
|
+
# Calculate the k-th row of matrix R
|
|
377
|
+
# R_k = q_k - B_0^p(u_k)q_0 - B_m^p(u_k)q_m
|
|
378
|
+
# This follows directly from the equation after (8.20) in the document
|
|
379
|
+
r_matrix[k - 1] = points[k] - all_basis[0] * points[0] - all_basis[m] * points[n]
|
|
380
|
+
|
|
381
|
+
if hasattr(self, "debug") and self.debug:
|
|
382
|
+
print(
|
|
383
|
+
f" Row {k - 1} of R matrix = q_{k} - B_0^{degree}({u:.6f})*q_0 - "
|
|
384
|
+
f"B_{m}^{degree}({u:.6f})*q_{n}"
|
|
385
|
+
)
|
|
386
|
+
print(
|
|
387
|
+
f" = {points[k]} - {all_basis[0]:.6f}*{points[0]} - "
|
|
388
|
+
f"{all_basis[m]:.6f}*{points[n]}"
|
|
389
|
+
)
|
|
390
|
+
print(f" = {r_matrix[k - 1]}")
|
|
391
|
+
|
|
392
|
+
if hasattr(self, "debug") and self.debug:
|
|
393
|
+
print("\n B matrix:")
|
|
394
|
+
for i in range(b_matrix.shape[0]):
|
|
395
|
+
row = " ["
|
|
396
|
+
for j in range(b_matrix.shape[1]):
|
|
397
|
+
row += f"{b_matrix[i, j]:.6f}, "
|
|
398
|
+
row = row[:-2] + "]"
|
|
399
|
+
print(row)
|
|
400
|
+
|
|
401
|
+
print("\n R matrix (first dimension):")
|
|
402
|
+
for i in range(r_matrix.shape[0]):
|
|
403
|
+
row = " ["
|
|
404
|
+
# Define max columns to display
|
|
405
|
+
max_cols = 3
|
|
406
|
+
for j in range(min(max_cols, r_matrix.shape[1])):
|
|
407
|
+
row += f"{r_matrix[i, j]:.6f}, "
|
|
408
|
+
if r_matrix.shape[1] > max_cols:
|
|
409
|
+
row += "..."
|
|
410
|
+
else:
|
|
411
|
+
row = row[:-2]
|
|
412
|
+
row += "]"
|
|
413
|
+
print(row)
|
|
414
|
+
|
|
415
|
+
# Apply weights to minimize the weighted least squares functional in equation (8.19)
|
|
416
|
+
w_matrix = np.diag(weights)
|
|
417
|
+
|
|
418
|
+
if hasattr(self, "debug") and self.debug:
|
|
419
|
+
print("\n Weights:")
|
|
420
|
+
print(f" {weights}")
|
|
421
|
+
|
|
422
|
+
# Compute weighted pseudo-inverse solution according to equation (8.25)
|
|
423
|
+
# B† = (B^T W B)^(-1) B^T W
|
|
424
|
+
btw = np.dot(b_matrix.T, w_matrix)
|
|
425
|
+
btwb = np.dot(btw, b_matrix)
|
|
426
|
+
btwr = np.dot(btw, r_matrix)
|
|
427
|
+
|
|
428
|
+
if hasattr(self, "debug") and self.debug:
|
|
429
|
+
print("\n Calculating pseudo-inverse solution:")
|
|
430
|
+
print(f" B^T W shape: {btw.shape}")
|
|
431
|
+
print(f" B^T W B shape: {btwb.shape}")
|
|
432
|
+
print(f" B^T W R shape: {btwr.shape}")
|
|
433
|
+
|
|
434
|
+
# Solve for internal control points: P = B† R
|
|
435
|
+
try:
|
|
436
|
+
# Solve the normal equations for the least squares solution
|
|
437
|
+
internal_control_points = np.linalg.solve(btwb, btwr)
|
|
438
|
+
|
|
439
|
+
if hasattr(self, "debug") and self.debug:
|
|
440
|
+
print(" Used np.linalg.solve (direct solution)")
|
|
441
|
+
except np.linalg.LinAlgError:
|
|
442
|
+
# If matrix is singular or poorly conditioned, use pseudo-inverse
|
|
443
|
+
# This provides the least squares solution that minimizes the norm of P
|
|
444
|
+
internal_control_points = np.dot(np.linalg.pinv(btwb), btwr)
|
|
445
|
+
|
|
446
|
+
if hasattr(self, "debug") and self.debug:
|
|
447
|
+
print(" Used np.linalg.pinv (matrix was singular or poorly conditioned)")
|
|
448
|
+
|
|
449
|
+
# Assign internal control points (p_1 to p_(m-1))
|
|
450
|
+
control_points[1:m] = internal_control_points
|
|
451
|
+
|
|
452
|
+
if hasattr(self, "debug") and self.debug:
|
|
453
|
+
print("\n Calculated internal control points:")
|
|
454
|
+
for i in range(1, m):
|
|
455
|
+
print(f" P_{i} = {control_points[i]}")
|
|
456
|
+
|
|
457
|
+
return control_points
|
|
458
|
+
|
|
459
|
+
def calculate_approximation_error(
|
|
460
|
+
self, points: np.ndarray | None = None, u_bar: np.ndarray | None = None
|
|
461
|
+
) -> float:
|
|
462
|
+
"""Calculate the approximation error as the sum of squared distances.
|
|
463
|
+
|
|
464
|
+
Computes sum of squared distances between the points and the corresponding
|
|
465
|
+
points on the B-spline.
|
|
466
|
+
|
|
467
|
+
Parameters
|
|
468
|
+
----------
|
|
469
|
+
points : np.ndarray or None, default=None
|
|
470
|
+
The points to compare with the B-spline. If None, the original points
|
|
471
|
+
used for approximation are used.
|
|
472
|
+
u_bar : np.ndarray or None, default=None
|
|
473
|
+
Parameter values for the points. If None, the original parameters are
|
|
474
|
+
used for original points, or computed for new points.
|
|
475
|
+
|
|
476
|
+
Returns
|
|
477
|
+
-------
|
|
478
|
+
float
|
|
479
|
+
The sum of squared distances.
|
|
480
|
+
"""
|
|
481
|
+
# Use original points and parameters if not provided
|
|
482
|
+
if points is None:
|
|
483
|
+
points = self.original_points
|
|
484
|
+
u_bar = self.original_parameters
|
|
485
|
+
|
|
486
|
+
# Calculate the sum of squared distances
|
|
487
|
+
sum_squared_dist = 0.0
|
|
488
|
+
for i, point in enumerate(points):
|
|
489
|
+
# Evaluate the B-spline at the parameter value
|
|
490
|
+
spline_point = self.evaluate(u_bar[i]) # type: ignore
|
|
491
|
+
|
|
492
|
+
# Calculate squared distance
|
|
493
|
+
squared_dist = np.sum((point - spline_point) ** 2)
|
|
494
|
+
sum_squared_dist += squared_dist
|
|
495
|
+
|
|
496
|
+
return sum_squared_dist
|
|
497
|
+
|
|
498
|
+
def refine(
|
|
499
|
+
self, max_error: float = 0.1, max_control_points: int = 100
|
|
500
|
+
) -> "ApproximationBSpline":
|
|
501
|
+
"""Refine the approximation by adding more control points.
|
|
502
|
+
|
|
503
|
+
Adds control points until the maximum error is below a threshold or the
|
|
504
|
+
maximum number of control points is reached.
|
|
505
|
+
|
|
506
|
+
Parameters
|
|
507
|
+
----------
|
|
508
|
+
max_error : float, default=0.1
|
|
509
|
+
Maximum acceptable error.
|
|
510
|
+
max_control_points : int, default=100
|
|
511
|
+
Maximum number of control points.
|
|
512
|
+
|
|
513
|
+
Returns
|
|
514
|
+
-------
|
|
515
|
+
ApproximationBSpline
|
|
516
|
+
A refined approximation B-spline.
|
|
517
|
+
"""
|
|
518
|
+
# Start with the current number of control points
|
|
519
|
+
num_control_points = len(self.control_points)
|
|
520
|
+
|
|
521
|
+
# Calculate initial error
|
|
522
|
+
error = self.calculate_approximation_error()
|
|
523
|
+
|
|
524
|
+
while error > max_error and num_control_points < max_control_points:
|
|
525
|
+
# Increase the number of control points
|
|
526
|
+
num_control_points += 1
|
|
527
|
+
|
|
528
|
+
# Create a new approximation B-spline with more control points
|
|
529
|
+
new_spline = ApproximationBSpline(
|
|
530
|
+
self.original_points, num_control_points, degree=self.degree
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
# Calculate the new error
|
|
534
|
+
error = new_spline.calculate_approximation_error()
|
|
535
|
+
|
|
536
|
+
# If error is below threshold, return the new spline
|
|
537
|
+
if error <= max_error:
|
|
538
|
+
return new_spline
|
|
539
|
+
|
|
540
|
+
# If we've reached max_control_points but error is still above threshold,
|
|
541
|
+
# return the best approximation we have
|
|
542
|
+
return ApproximationBSpline(
|
|
543
|
+
self.original_points, min(num_control_points, max_control_points), degree=self.degree
|
|
544
|
+
)
|