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,737 @@
|
|
|
1
|
+
import matplotlib.pyplot as plt
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
from mpl_toolkits.mplot3d import Axes3D # noqa: F401 - Imported for type hinting
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BSpline:
|
|
8
|
+
"""
|
|
9
|
+
A class for representing and evaluating B-spline curves.
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
degree : int
|
|
14
|
+
The degree of the B-spline.
|
|
15
|
+
knots : array_like
|
|
16
|
+
The knot vector.
|
|
17
|
+
control_points : array_like
|
|
18
|
+
The control points defining the B-spline.
|
|
19
|
+
|
|
20
|
+
Attributes
|
|
21
|
+
----------
|
|
22
|
+
degree : int
|
|
23
|
+
The degree of the B-spline.
|
|
24
|
+
knots : ndarray
|
|
25
|
+
The knot vector.
|
|
26
|
+
control_points : ndarray
|
|
27
|
+
The control points defining the B-spline.
|
|
28
|
+
u_min : float
|
|
29
|
+
Minimum valid parameter value.
|
|
30
|
+
u_max : float
|
|
31
|
+
Maximum valid parameter value.
|
|
32
|
+
dimension : int
|
|
33
|
+
The dimension of the control points (2D, 3D, etc.).
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
# Constants for dimension comparisons
|
|
37
|
+
DIM_2 = 2
|
|
38
|
+
DIM_3 = 3
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self, degree: int, knots: list | np.ndarray, control_points: list | np.ndarray
|
|
42
|
+
) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Initialize a B-spline curve.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
degree : int
|
|
49
|
+
The degree of the B-spline (must be positive).
|
|
50
|
+
knots : array_like
|
|
51
|
+
The knot vector (must be non-decreasing).
|
|
52
|
+
control_points : array_like
|
|
53
|
+
The control points defining the B-spline.
|
|
54
|
+
Can be multidimensional (e.g., 2D or 3D points).
|
|
55
|
+
|
|
56
|
+
Raises
|
|
57
|
+
------
|
|
58
|
+
ValueError
|
|
59
|
+
If the inputs do not satisfy B-spline requirements.
|
|
60
|
+
"""
|
|
61
|
+
# Convert inputs to numpy arrays if they're not already
|
|
62
|
+
if not isinstance(knots, np.ndarray):
|
|
63
|
+
knots = np.array(knots, dtype=np.float64)
|
|
64
|
+
else:
|
|
65
|
+
# Ensure knots are float64 for precision
|
|
66
|
+
knots = knots.astype(np.float64)
|
|
67
|
+
|
|
68
|
+
if not isinstance(control_points, np.ndarray):
|
|
69
|
+
control_points = np.array(control_points, dtype=np.float64)
|
|
70
|
+
else:
|
|
71
|
+
# Ensure control points are float64 for precision
|
|
72
|
+
control_points = control_points.astype(np.float64)
|
|
73
|
+
|
|
74
|
+
# Validate inputs
|
|
75
|
+
if degree < 0:
|
|
76
|
+
raise ValueError("Degree must be non-negative")
|
|
77
|
+
|
|
78
|
+
if not np.all(np.diff(knots) >= 0):
|
|
79
|
+
raise ValueError("Knot vector must be non-decreasing")
|
|
80
|
+
|
|
81
|
+
n_control_points = len(control_points)
|
|
82
|
+
n_knots = len(knots)
|
|
83
|
+
|
|
84
|
+
# Check relationship between degree, control points, and knots
|
|
85
|
+
if n_knots != n_control_points + degree + 1:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f"Invalid knot vector length for the given degree and number of control points. "
|
|
88
|
+
f"Expected {n_control_points + degree + 1}, got {n_knots}. "
|
|
89
|
+
f"The relationship must satisfy: n_knots = n_control_points + degree + 1"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
self.degree = degree
|
|
93
|
+
self.knots = knots
|
|
94
|
+
self.control_points = control_points
|
|
95
|
+
|
|
96
|
+
# Store min and max parameter values for convenience
|
|
97
|
+
self.u_min = knots[degree]
|
|
98
|
+
self.u_max = knots[-(degree + 1)]
|
|
99
|
+
|
|
100
|
+
# Store the dimension of the control points
|
|
101
|
+
self.dimension = 1 if control_points.ndim == 1 else control_points.shape[1]
|
|
102
|
+
|
|
103
|
+
# Precompute knot spans for common parameter values
|
|
104
|
+
self._cached_spans: dict[float, int] = {}
|
|
105
|
+
|
|
106
|
+
# Tolerance for floating-point comparisons
|
|
107
|
+
self.eps = 1e-10
|
|
108
|
+
|
|
109
|
+
def find_knot_span(self, u: float) -> int:
|
|
110
|
+
"""
|
|
111
|
+
Find the knot span index for a given parameter value u.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
u : float
|
|
116
|
+
The parameter value.
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
int
|
|
121
|
+
The index of the knot span containing u.
|
|
122
|
+
|
|
123
|
+
Raises
|
|
124
|
+
------
|
|
125
|
+
ValueError
|
|
126
|
+
If u is outside the valid parameter range.
|
|
127
|
+
"""
|
|
128
|
+
# Check cache first for frequently used values
|
|
129
|
+
if u in self._cached_spans:
|
|
130
|
+
return self._cached_spans[u]
|
|
131
|
+
|
|
132
|
+
# Validate parameter range
|
|
133
|
+
if u < self.u_min - self.eps or u > self.u_max + self.eps:
|
|
134
|
+
raise ValueError(f"Parameter u={u} outside valid range [{self.u_min}, {self.u_max}]")
|
|
135
|
+
|
|
136
|
+
# Clamp parameter to valid range to handle numerical issues
|
|
137
|
+
u = np.clip(u, self.u_min, self.u_max)
|
|
138
|
+
|
|
139
|
+
# Handle endpoint case more efficiently
|
|
140
|
+
if abs(u - self.u_max) <= self.eps:
|
|
141
|
+
# For maximum parameter value, return the last valid span
|
|
142
|
+
span = len(self.knots) - self.degree - 2
|
|
143
|
+
self._cached_spans[u] = span
|
|
144
|
+
return span
|
|
145
|
+
|
|
146
|
+
# More efficient binary search implementation
|
|
147
|
+
n = len(self.control_points) - 1
|
|
148
|
+
low = self.degree
|
|
149
|
+
high = n + 1
|
|
150
|
+
|
|
151
|
+
# Binary search with simplified condition
|
|
152
|
+
while low < high - 1:
|
|
153
|
+
mid = (low + high) // 2
|
|
154
|
+
if u >= self.knots[mid]:
|
|
155
|
+
low = mid
|
|
156
|
+
else:
|
|
157
|
+
high = mid
|
|
158
|
+
|
|
159
|
+
# Cache the result for future use
|
|
160
|
+
self._cached_spans[u] = low
|
|
161
|
+
return low
|
|
162
|
+
|
|
163
|
+
def basis_functions(self, u: float, span_index: int) -> np.ndarray:
|
|
164
|
+
"""
|
|
165
|
+
Calculate all non-zero basis functions at parameter value u.
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
u : float
|
|
170
|
+
The parameter value.
|
|
171
|
+
span_index : int
|
|
172
|
+
The knot span index containing u.
|
|
173
|
+
|
|
174
|
+
Returns
|
|
175
|
+
-------
|
|
176
|
+
ndarray
|
|
177
|
+
Array of basis function values (p+1 values).
|
|
178
|
+
"""
|
|
179
|
+
# Initialize the basis functions array
|
|
180
|
+
n = np.zeros(self.degree + 1, dtype=np.float64)
|
|
181
|
+
|
|
182
|
+
# Initialize left and right arrays
|
|
183
|
+
left = np.zeros(self.degree + 1, dtype=np.float64)
|
|
184
|
+
right = np.zeros(self.degree + 1, dtype=np.float64)
|
|
185
|
+
|
|
186
|
+
# First basis function is always 1
|
|
187
|
+
n[0] = 1.0
|
|
188
|
+
|
|
189
|
+
# For each degree, update the basis functions
|
|
190
|
+
for d in range(1, self.degree + 1):
|
|
191
|
+
left[d] = u - self.knots[span_index + 1 - d]
|
|
192
|
+
right[d] = self.knots[span_index + d] - u
|
|
193
|
+
|
|
194
|
+
saved = 0.0
|
|
195
|
+
|
|
196
|
+
for r in range(d):
|
|
197
|
+
# Avoid division by zero more robustly
|
|
198
|
+
denominator = right[r + 1] + left[d - r]
|
|
199
|
+
|
|
200
|
+
# Use small epsilon for numerical stability - using ternary operator
|
|
201
|
+
temp = 0.0 if abs(denominator) < self.eps else n[r] / denominator
|
|
202
|
+
|
|
203
|
+
# Update the basis function using the recurrence relation
|
|
204
|
+
n[r] = saved + right[r + 1] * temp
|
|
205
|
+
saved = left[d - r] * temp
|
|
206
|
+
|
|
207
|
+
n[d] = saved
|
|
208
|
+
|
|
209
|
+
return n
|
|
210
|
+
|
|
211
|
+
def evaluate(self, u: float) -> np.ndarray:
|
|
212
|
+
"""
|
|
213
|
+
Evaluate the B-spline curve at parameter value u.
|
|
214
|
+
|
|
215
|
+
Parameters
|
|
216
|
+
----------
|
|
217
|
+
u : float
|
|
218
|
+
The parameter value.
|
|
219
|
+
|
|
220
|
+
Returns
|
|
221
|
+
-------
|
|
222
|
+
ndarray
|
|
223
|
+
The point on the B-spline curve at parameter u.
|
|
224
|
+
"""
|
|
225
|
+
# Handle edge cases exactly at endpoints to avoid numerical issues
|
|
226
|
+
if abs(u - self.u_min) <= self.eps:
|
|
227
|
+
return self.control_points[0].copy()
|
|
228
|
+
if abs(u - self.u_max) <= self.eps:
|
|
229
|
+
return self.control_points[-1].copy()
|
|
230
|
+
|
|
231
|
+
# Clamp parameter to valid range using numpy's clip function
|
|
232
|
+
u = np.clip(u, self.u_min, self.u_max)
|
|
233
|
+
|
|
234
|
+
# Find the knot span
|
|
235
|
+
span = self.find_knot_span(u)
|
|
236
|
+
|
|
237
|
+
# Calculate the basis functions
|
|
238
|
+
n = self.basis_functions(u, span)
|
|
239
|
+
|
|
240
|
+
# Calculate the point by multiplying basis functions with control points
|
|
241
|
+
# Use advanced indexing for efficiency
|
|
242
|
+
relevant_controls = self.control_points[span - self.degree : span + 1]
|
|
243
|
+
|
|
244
|
+
# Handle 1D control points differently
|
|
245
|
+
if self.dimension == 1:
|
|
246
|
+
point = np.dot(n, relevant_controls)
|
|
247
|
+
else:
|
|
248
|
+
# For multi-dimensional control points
|
|
249
|
+
point = np.zeros(self.dimension, dtype=np.float64)
|
|
250
|
+
for i in range(self.degree + 1):
|
|
251
|
+
point += n[i] * relevant_controls[i]
|
|
252
|
+
|
|
253
|
+
return point
|
|
254
|
+
|
|
255
|
+
def basis_function_derivatives(self, u: float, span_index: int, order: int) -> np.ndarray:
|
|
256
|
+
"""
|
|
257
|
+
Calculate derivatives of basis functions up to the specified order.
|
|
258
|
+
|
|
259
|
+
Parameters
|
|
260
|
+
----------
|
|
261
|
+
u : float
|
|
262
|
+
The parameter value.
|
|
263
|
+
span_index : int
|
|
264
|
+
The knot span index containing u.
|
|
265
|
+
order : int
|
|
266
|
+
The maximum order of derivatives to calculate.
|
|
267
|
+
|
|
268
|
+
Returns
|
|
269
|
+
-------
|
|
270
|
+
ndarray
|
|
271
|
+
2D array where ders[k][j] is the k-th derivative of the j-th basis function.
|
|
272
|
+
"""
|
|
273
|
+
# Ensure order doesn't exceed degree
|
|
274
|
+
order = min(order, self.degree)
|
|
275
|
+
|
|
276
|
+
# Initialize the result array
|
|
277
|
+
ders = np.zeros((order + 1, self.degree + 1), dtype=np.float64)
|
|
278
|
+
|
|
279
|
+
# Initialize temporary arrays
|
|
280
|
+
left = np.zeros(self.degree + 1, dtype=np.float64)
|
|
281
|
+
right = np.zeros(self.degree + 1, dtype=np.float64)
|
|
282
|
+
a = np.zeros((2, self.degree + 1), dtype=np.float64)
|
|
283
|
+
|
|
284
|
+
# Initialize the basis function table (ndu)
|
|
285
|
+
ndu = np.zeros((self.degree + 1, self.degree + 1), dtype=np.float64)
|
|
286
|
+
ndu[0, 0] = 1.0
|
|
287
|
+
|
|
288
|
+
for j in range(1, self.degree + 1):
|
|
289
|
+
left[j] = u - self.knots[span_index + 1 - j]
|
|
290
|
+
right[j] = self.knots[span_index + j] - u
|
|
291
|
+
saved = 0.0
|
|
292
|
+
|
|
293
|
+
for r in range(j):
|
|
294
|
+
# Lower triangle
|
|
295
|
+
ndu[j, r] = right[r + 1] + left[j - r]
|
|
296
|
+
|
|
297
|
+
# Avoid division by zero - using ternary operator
|
|
298
|
+
temp = 0.0 if abs(ndu[j, r]) < self.eps else ndu[r, j - 1] / ndu[j, r]
|
|
299
|
+
|
|
300
|
+
# Upper triangle
|
|
301
|
+
ndu[r, j] = saved + right[r + 1] * temp
|
|
302
|
+
saved = left[j - r] * temp
|
|
303
|
+
|
|
304
|
+
ndu[j, j] = saved
|
|
305
|
+
|
|
306
|
+
# Load the basis functions
|
|
307
|
+
for j in range(self.degree + 1):
|
|
308
|
+
ders[0, j] = ndu[j, self.degree]
|
|
309
|
+
|
|
310
|
+
# Calculate the derivatives
|
|
311
|
+
for r in range(self.degree + 1):
|
|
312
|
+
# Index of current column in the table
|
|
313
|
+
s1 = 0
|
|
314
|
+
s2 = 1
|
|
315
|
+
|
|
316
|
+
# Initialize a array
|
|
317
|
+
a[0, 0] = 1.0
|
|
318
|
+
|
|
319
|
+
# Loop to compute k-th derivative
|
|
320
|
+
for k in range(1, order + 1):
|
|
321
|
+
d = 0.0
|
|
322
|
+
rk = r - k
|
|
323
|
+
pk = self.degree - k
|
|
324
|
+
|
|
325
|
+
if r >= k:
|
|
326
|
+
a[s2, 0] = a[s1, 0] / ndu[pk + 1, rk]
|
|
327
|
+
d = a[s2, 0] * ndu[rk, pk]
|
|
328
|
+
|
|
329
|
+
j1 = 1 if rk >= -1 else -rk
|
|
330
|
+
j2 = k - 1 if r - 1 <= pk else self.degree - r
|
|
331
|
+
|
|
332
|
+
for j in range(j1, j2 + 1):
|
|
333
|
+
a[s2, j] = (a[s1, j] - a[s1, j - 1]) / ndu[pk + 1, rk + j]
|
|
334
|
+
d += a[s2, j] * ndu[rk + j, pk]
|
|
335
|
+
|
|
336
|
+
if r <= pk:
|
|
337
|
+
a[s2, k] = -a[s1, k - 1] / ndu[pk + 1, r]
|
|
338
|
+
d += a[s2, k] * ndu[r, pk]
|
|
339
|
+
|
|
340
|
+
ders[k, r] = d
|
|
341
|
+
|
|
342
|
+
# Switch row indices
|
|
343
|
+
j = s1
|
|
344
|
+
s1 = s2
|
|
345
|
+
s2 = j
|
|
346
|
+
|
|
347
|
+
# Multiply by the correct factors (p!/(p-k)!)
|
|
348
|
+
r = self.degree
|
|
349
|
+
for k in range(1, order + 1):
|
|
350
|
+
for j in range(self.degree + 1):
|
|
351
|
+
ders[k, j] *= r
|
|
352
|
+
r *= self.degree - k
|
|
353
|
+
|
|
354
|
+
return ders
|
|
355
|
+
|
|
356
|
+
def evaluate_derivative(self, u: float, order: int = 1) -> np.ndarray:
|
|
357
|
+
"""
|
|
358
|
+
Evaluate the derivative of the B-spline curve at parameter value u.
|
|
359
|
+
|
|
360
|
+
Parameters
|
|
361
|
+
----------
|
|
362
|
+
u : float
|
|
363
|
+
The parameter value.
|
|
364
|
+
order : int, optional
|
|
365
|
+
The order of the derivative. Default is 1.
|
|
366
|
+
|
|
367
|
+
Returns
|
|
368
|
+
-------
|
|
369
|
+
ndarray
|
|
370
|
+
The derivative of the B-spline curve at parameter u.
|
|
371
|
+
|
|
372
|
+
Raises
|
|
373
|
+
------
|
|
374
|
+
ValueError
|
|
375
|
+
If the order is greater than the degree of the B-spline.
|
|
376
|
+
"""
|
|
377
|
+
if order > self.degree:
|
|
378
|
+
raise ValueError(f"Derivative order {order} exceeds B-spline degree {self.degree}")
|
|
379
|
+
|
|
380
|
+
if order == 0:
|
|
381
|
+
return self.evaluate(u)
|
|
382
|
+
|
|
383
|
+
# Clamp parameter to valid range
|
|
384
|
+
u = np.clip(u, self.u_min + self.eps, self.u_max - self.eps)
|
|
385
|
+
|
|
386
|
+
# Find the knot span
|
|
387
|
+
span = self.find_knot_span(u)
|
|
388
|
+
|
|
389
|
+
# Calculate derivatives of basis functions
|
|
390
|
+
ders = self.basis_function_derivatives(u, span, order)
|
|
391
|
+
|
|
392
|
+
# Calculate the derivative of the curve more efficiently
|
|
393
|
+
relevant_controls = self.control_points[span - self.degree : span + 1]
|
|
394
|
+
|
|
395
|
+
# Handle 1D control points differently
|
|
396
|
+
if self.dimension == 1:
|
|
397
|
+
derivative = np.dot(ders[order], relevant_controls)
|
|
398
|
+
else:
|
|
399
|
+
derivative = np.zeros(self.dimension, dtype=np.float64)
|
|
400
|
+
for j in range(self.degree + 1):
|
|
401
|
+
derivative += ders[order, j] * relevant_controls[j]
|
|
402
|
+
|
|
403
|
+
return derivative
|
|
404
|
+
|
|
405
|
+
def generate_curve_points(self, num_points: int = 100) -> tuple[np.ndarray, np.ndarray]:
|
|
406
|
+
"""
|
|
407
|
+
Generate points along the B-spline curve for visualization.
|
|
408
|
+
|
|
409
|
+
Parameters
|
|
410
|
+
----------
|
|
411
|
+
num_points : int, optional
|
|
412
|
+
Number of points to generate. Default is 100.
|
|
413
|
+
|
|
414
|
+
Returns
|
|
415
|
+
-------
|
|
416
|
+
u_values : ndarray
|
|
417
|
+
Parameter values.
|
|
418
|
+
curve_points : ndarray
|
|
419
|
+
Corresponding points on the curve.
|
|
420
|
+
"""
|
|
421
|
+
# Generate parameter values evenly spaced in the valid range
|
|
422
|
+
u_values = np.linspace(self.u_min, self.u_max, num_points)
|
|
423
|
+
|
|
424
|
+
# Vectorize for better performance if curve is 1D or 2D
|
|
425
|
+
if self.dimension <= self.DIM_2:
|
|
426
|
+
# Pre-allocate array for curve points
|
|
427
|
+
curve_points = np.zeros((num_points, self.dimension), dtype=np.float64)
|
|
428
|
+
|
|
429
|
+
# Evaluate the curve at each parameter value
|
|
430
|
+
for i, u in enumerate(u_values):
|
|
431
|
+
curve_points[i] = self.evaluate(u)
|
|
432
|
+
else:
|
|
433
|
+
# For higher dimensions, use list comprehension
|
|
434
|
+
curve_points = np.array([self.evaluate(u) for u in u_values])
|
|
435
|
+
|
|
436
|
+
return u_values, curve_points
|
|
437
|
+
|
|
438
|
+
def plot_2d(
|
|
439
|
+
self,
|
|
440
|
+
num_points: int = 100,
|
|
441
|
+
show_control_polygon: bool = True,
|
|
442
|
+
show_knots: bool = False,
|
|
443
|
+
ax: plt.Axes | None = None,
|
|
444
|
+
) -> plt.Axes:
|
|
445
|
+
"""
|
|
446
|
+
Plot a 2D B-spline curve with customizable styling.
|
|
447
|
+
|
|
448
|
+
Parameters
|
|
449
|
+
----------
|
|
450
|
+
num_points : int, optional
|
|
451
|
+
Number of points to generate for the curve. Default is 100.
|
|
452
|
+
show_control_polygon : bool, optional
|
|
453
|
+
Whether to show the control polygon. Default is True.
|
|
454
|
+
show_knots : bool, optional
|
|
455
|
+
Whether to show the knot points on the curve. Default is False.
|
|
456
|
+
ax : matplotlib.axes.Axes, optional
|
|
457
|
+
Matplotlib axis to use for plotting. If None, a new figure is created.
|
|
458
|
+
curve_style : dict, optional
|
|
459
|
+
Style parameters for the curve.
|
|
460
|
+
control_style : dict, optional
|
|
461
|
+
Style parameters for the control polygon.
|
|
462
|
+
knot_style : dict, optional
|
|
463
|
+
Style parameters for the knot points.
|
|
464
|
+
|
|
465
|
+
Returns
|
|
466
|
+
-------
|
|
467
|
+
matplotlib.axes.Axes
|
|
468
|
+
The matplotlib axis object.
|
|
469
|
+
|
|
470
|
+
Raises
|
|
471
|
+
------
|
|
472
|
+
ValueError
|
|
473
|
+
If the dimension of control points is not 2.
|
|
474
|
+
"""
|
|
475
|
+
if self.dimension != self.DIM_2:
|
|
476
|
+
raise ValueError(
|
|
477
|
+
f"Control points must be 2D for this plot function, got {self.dimension}D"
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
# Default styles
|
|
481
|
+
default_curve_style = {"color": "blue", "linewidth": 2, "label": "B-spline curve"}
|
|
482
|
+
default_control_style = {
|
|
483
|
+
"color": "red",
|
|
484
|
+
"linestyle": "--",
|
|
485
|
+
"marker": "o",
|
|
486
|
+
"linewidth": 1,
|
|
487
|
+
"markersize": 8,
|
|
488
|
+
"label": "Control polygon",
|
|
489
|
+
}
|
|
490
|
+
default_knot_style = {
|
|
491
|
+
"color": "green",
|
|
492
|
+
"marker": "x",
|
|
493
|
+
"markersize": 10,
|
|
494
|
+
"label": "Knot points",
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
# Create a new figure if needed
|
|
498
|
+
if ax is None:
|
|
499
|
+
_, ax = plt.subplots(figsize=(10, 6))
|
|
500
|
+
|
|
501
|
+
# Generate points along the curve
|
|
502
|
+
_, curve_points = self.generate_curve_points(num_points)
|
|
503
|
+
|
|
504
|
+
# Plot the curve
|
|
505
|
+
ax.plot(curve_points[:, 0], curve_points[:, 1], **default_curve_style)
|
|
506
|
+
|
|
507
|
+
# Plot the control points and polygon if requested
|
|
508
|
+
if show_control_polygon:
|
|
509
|
+
ax.plot(self.control_points[:, 0], self.control_points[:, 1], **default_control_style)
|
|
510
|
+
|
|
511
|
+
# Plot the knot points if requested
|
|
512
|
+
if show_knots:
|
|
513
|
+
# Only plot the knots within the valid parameter range
|
|
514
|
+
valid_knots = [k for k in self.knots if self.u_min <= k <= self.u_max]
|
|
515
|
+
unique_knots = np.unique(valid_knots)
|
|
516
|
+
|
|
517
|
+
knot_points = np.array([self.evaluate(k) for k in unique_knots])
|
|
518
|
+
ax.plot(knot_points[:, 0], knot_points[:, 1], **default_knot_style)
|
|
519
|
+
|
|
520
|
+
# Set labels and title
|
|
521
|
+
ax.set_xlabel("X")
|
|
522
|
+
ax.set_ylabel("Y")
|
|
523
|
+
ax.set_title(f"B-spline curve of degree {self.degree}")
|
|
524
|
+
ax.legend()
|
|
525
|
+
ax.grid(True)
|
|
526
|
+
|
|
527
|
+
return ax
|
|
528
|
+
|
|
529
|
+
def plot_3d(
|
|
530
|
+
self,
|
|
531
|
+
num_points: int = 100,
|
|
532
|
+
show_control_polygon: bool = True,
|
|
533
|
+
ax: plt.Axes | None = None,
|
|
534
|
+
) -> plt.Axes:
|
|
535
|
+
"""
|
|
536
|
+
Plot a 3D B-spline curve.
|
|
537
|
+
|
|
538
|
+
Parameters
|
|
539
|
+
----------
|
|
540
|
+
num_points : int, optional
|
|
541
|
+
Number of points to generate for the curve. Default is 100.
|
|
542
|
+
show_control_polygon : bool, optional
|
|
543
|
+
Whether to show the control polygon. Default is True.
|
|
544
|
+
ax : matplotlib.axes.Axes, optional
|
|
545
|
+
Matplotlib 3D axis to use for plotting. If None, a new figure is created.
|
|
546
|
+
curve_style : dict, optional
|
|
547
|
+
Style parameters for the curve.
|
|
548
|
+
control_style : dict, optional
|
|
549
|
+
Style parameters for the control polygon.
|
|
550
|
+
|
|
551
|
+
Returns
|
|
552
|
+
-------
|
|
553
|
+
matplotlib.axes.Axes
|
|
554
|
+
The matplotlib 3D axis object.
|
|
555
|
+
|
|
556
|
+
Raises
|
|
557
|
+
------
|
|
558
|
+
ValueError
|
|
559
|
+
If the dimension of control points is not 3.
|
|
560
|
+
"""
|
|
561
|
+
if self.dimension != self.DIM_3:
|
|
562
|
+
raise ValueError(
|
|
563
|
+
f"Control points must be 3D for this plot function, got {self.dimension}D"
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
# Default styles
|
|
567
|
+
default_curve_style = {"color": "blue", "linewidth": 2, "label": "B-spline curve"}
|
|
568
|
+
default_control_style = {
|
|
569
|
+
"color": "red",
|
|
570
|
+
"linestyle": "--",
|
|
571
|
+
"marker": "o",
|
|
572
|
+
"linewidth": 1,
|
|
573
|
+
"markersize": 8,
|
|
574
|
+
"label": "Control polygon",
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
# Create a new figure if needed
|
|
578
|
+
if ax is None:
|
|
579
|
+
fig = plt.figure(figsize=(10, 8))
|
|
580
|
+
ax = fig.add_subplot(111, projection="3d")
|
|
581
|
+
|
|
582
|
+
# Generate points along the curve
|
|
583
|
+
_, curve_points = self.generate_curve_points(num_points)
|
|
584
|
+
|
|
585
|
+
# Plot the curve
|
|
586
|
+
ax.plot(curve_points[:, 0], curve_points[:, 1], curve_points[:, 2], **default_curve_style)
|
|
587
|
+
|
|
588
|
+
# Plot the control points and polygon if requested
|
|
589
|
+
if show_control_polygon:
|
|
590
|
+
ax.plot(
|
|
591
|
+
self.control_points[:, 0],
|
|
592
|
+
self.control_points[:, 1],
|
|
593
|
+
self.control_points[:, 2],
|
|
594
|
+
**default_control_style,
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
# Set labels and title
|
|
598
|
+
ax.set_xlabel("X")
|
|
599
|
+
ax.set_ylabel("Y")
|
|
600
|
+
ax.set_zlabel("Z")
|
|
601
|
+
ax.set_title(f"3D B-spline curve of degree {self.degree}")
|
|
602
|
+
ax.legend()
|
|
603
|
+
|
|
604
|
+
return ax
|
|
605
|
+
|
|
606
|
+
@staticmethod
|
|
607
|
+
def create_uniform_knots(
|
|
608
|
+
degree: int, num_control_points: int, domain_min: float = 0.0, domain_max: float = 1.0
|
|
609
|
+
) -> np.ndarray:
|
|
610
|
+
"""
|
|
611
|
+
Create a uniform knot vector for a B-spline with appropriate
|
|
612
|
+
multiplicity at endpoints to ensure interpolation.
|
|
613
|
+
|
|
614
|
+
Parameters
|
|
615
|
+
----------
|
|
616
|
+
degree : int
|
|
617
|
+
The degree of the B-spline.
|
|
618
|
+
num_control_points : int
|
|
619
|
+
The number of control points.
|
|
620
|
+
domain_min : float, optional
|
|
621
|
+
Minimum value of the parameter domain. Default is 0.0.
|
|
622
|
+
domain_max : float, optional
|
|
623
|
+
Maximum value of the parameter domain. Default is 1.0.
|
|
624
|
+
|
|
625
|
+
Returns
|
|
626
|
+
-------
|
|
627
|
+
ndarray
|
|
628
|
+
The uniform knot vector with knots in the specified domain.
|
|
629
|
+
|
|
630
|
+
Raises
|
|
631
|
+
------
|
|
632
|
+
ValueError
|
|
633
|
+
If degree is negative or num_control_points is less than or equal to degree.
|
|
634
|
+
"""
|
|
635
|
+
# Input validation
|
|
636
|
+
if degree < 0:
|
|
637
|
+
raise ValueError("Degree must be non-negative")
|
|
638
|
+
if num_control_points <= degree:
|
|
639
|
+
raise ValueError("Number of control points must be greater than the degree")
|
|
640
|
+
|
|
641
|
+
# Calculate total number of knots required by the formula: n = m + p + 1
|
|
642
|
+
# where n is number of knots, m is number of control points, p is degree
|
|
643
|
+
num_knots = num_control_points + degree + 1
|
|
644
|
+
|
|
645
|
+
# Initialize the knot vector with domain_min values (for the first p+1 knots)
|
|
646
|
+
knots = np.zeros(num_knots, dtype=np.float64)
|
|
647
|
+
|
|
648
|
+
# Calculate the number of internal knots needed
|
|
649
|
+
n_internal = num_knots - 2 * (degree + 1)
|
|
650
|
+
|
|
651
|
+
# Set the first degree+1 knots to domain_min
|
|
652
|
+
knots[: degree + 1] = domain_min
|
|
653
|
+
|
|
654
|
+
# Create internal knots uniformly distributed (if any)
|
|
655
|
+
if n_internal >= 0:
|
|
656
|
+
# Generate values between domain_min and domain_max, excluding endpoints
|
|
657
|
+
internal_values = np.linspace(domain_min, domain_max, n_internal + 2)[1:-1]
|
|
658
|
+
knots[degree + 1 : degree + 1 + n_internal] = internal_values
|
|
659
|
+
|
|
660
|
+
# Set end knots to domain_max with multiplicity p+1
|
|
661
|
+
knots[-(degree + 1) :] = domain_max
|
|
662
|
+
|
|
663
|
+
return knots
|
|
664
|
+
|
|
665
|
+
@staticmethod
|
|
666
|
+
def create_periodic_knots(
|
|
667
|
+
degree: int, num_control_points: int, domain_min: float = 0.0, domain_max: float = 1.0
|
|
668
|
+
) -> np.ndarray:
|
|
669
|
+
"""
|
|
670
|
+
Create a periodic (uniform) knot vector for a B-spline.
|
|
671
|
+
|
|
672
|
+
Parameters
|
|
673
|
+
----------
|
|
674
|
+
degree : int
|
|
675
|
+
The degree of the B-spline.
|
|
676
|
+
num_control_points : int
|
|
677
|
+
The number of control points.
|
|
678
|
+
domain_min : float, optional
|
|
679
|
+
Minimum value of the parameter domain. Default is 0.0.
|
|
680
|
+
domain_max : float, optional
|
|
681
|
+
Maximum value of the parameter domain. Default is 1.0.
|
|
682
|
+
|
|
683
|
+
Returns
|
|
684
|
+
-------
|
|
685
|
+
ndarray
|
|
686
|
+
The periodic knot vector.
|
|
687
|
+
|
|
688
|
+
Raises
|
|
689
|
+
------
|
|
690
|
+
ValueError
|
|
691
|
+
If degree is negative or num_control_points is less than degree+1.
|
|
692
|
+
"""
|
|
693
|
+
# Input validation
|
|
694
|
+
if degree < 0:
|
|
695
|
+
raise ValueError("Degree must be non-negative")
|
|
696
|
+
if num_control_points < degree + 1:
|
|
697
|
+
raise ValueError(
|
|
698
|
+
"For a periodic B-spline, number of control points must be at least degree+1"
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
# Calculate total number of knots required
|
|
702
|
+
num_knots = num_control_points + degree + 1
|
|
703
|
+
|
|
704
|
+
# Create uniformly spaced knots
|
|
705
|
+
knots = np.linspace(domain_min, domain_max, num_knots - 2 * degree)
|
|
706
|
+
|
|
707
|
+
# Extend the knot vector for periodicity
|
|
708
|
+
extended_knots = np.zeros(num_knots, dtype=np.float64)
|
|
709
|
+
|
|
710
|
+
# Add degree knots at the beginning (below domain_min)
|
|
711
|
+
step = (domain_max - domain_min) / (num_knots - 2 * degree - 1)
|
|
712
|
+
for i in range(degree):
|
|
713
|
+
extended_knots[i] = domain_min - (degree - i) * step
|
|
714
|
+
|
|
715
|
+
# Add the regular knots
|
|
716
|
+
extended_knots[degree : degree + len(knots)] = knots
|
|
717
|
+
|
|
718
|
+
# Add degree knots at the end (above domain_max)
|
|
719
|
+
for i in range(degree):
|
|
720
|
+
extended_knots[degree + len(knots) + i] = domain_max + (i + 1) * step
|
|
721
|
+
|
|
722
|
+
return extended_knots
|
|
723
|
+
|
|
724
|
+
def __repr__(self) -> str:
|
|
725
|
+
"""
|
|
726
|
+
Return a string representation of the B-spline.
|
|
727
|
+
|
|
728
|
+
Returns
|
|
729
|
+
-------
|
|
730
|
+
str
|
|
731
|
+
String representation.
|
|
732
|
+
"""
|
|
733
|
+
return (
|
|
734
|
+
f"BSpline(degree={self.degree}, "
|
|
735
|
+
f"control_points={len(self.control_points)}, "
|
|
736
|
+
f"dimension={self.dimension})"
|
|
737
|
+
)
|