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,515 @@
|
|
|
1
|
+
import matplotlib.pyplot as plt
|
|
2
|
+
import numpy as np
|
|
3
|
+
from scipy.linalg import solve
|
|
4
|
+
|
|
5
|
+
from interpolatepy.b_spline import BSpline
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
CUBIC_DEGREE = 3
|
|
9
|
+
QUARTIC_DEGREE = 4
|
|
10
|
+
QUINTIC_DEGREE = 5
|
|
11
|
+
VALID_DEGREES = {CUBIC_DEGREE, QUARTIC_DEGREE, QUINTIC_DEGREE}
|
|
12
|
+
MAX_POINTS_FOR_LABELS = 10
|
|
13
|
+
TWO_DIMENSIONAL = 2
|
|
14
|
+
THREE_DIMENSIONAL = 3
|
|
15
|
+
EPS_P = 1e12
|
|
16
|
+
EPS_N = 1e-10
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BSplineInterpolator(BSpline):
|
|
20
|
+
"""A B-spline that interpolates a set of points with specified degrees of continuity.
|
|
21
|
+
|
|
22
|
+
This class inherits from BSpline and computes the knot vector and control points
|
|
23
|
+
required to interpolate given data points at specified times, while maintaining
|
|
24
|
+
desired continuity constraints.
|
|
25
|
+
|
|
26
|
+
The implementation follows section 4.5 of the document, supporting:
|
|
27
|
+
- Cubic splines (degree 3) with C² continuity
|
|
28
|
+
- Quartic splines (degree 4) with C³ continuity (continuous jerk)
|
|
29
|
+
- Quintic splines (degree 5) with C⁴ continuity (continuous snap)
|
|
30
|
+
|
|
31
|
+
Points can be of any dimension, including 2D and 3D.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__( # noqa: PLR0913, PLR0917
|
|
35
|
+
self,
|
|
36
|
+
degree: int,
|
|
37
|
+
points: list | np.ndarray,
|
|
38
|
+
times: list | np.ndarray | None = None,
|
|
39
|
+
initial_velocity: list | np.ndarray | None = None,
|
|
40
|
+
final_velocity: list | np.ndarray | None = None,
|
|
41
|
+
initial_acceleration: list | np.ndarray | None = None,
|
|
42
|
+
final_acceleration: list | np.ndarray | None = None,
|
|
43
|
+
cyclic: bool = False,
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Initialize a B-spline interpolator.
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
degree : int
|
|
50
|
+
The degree of the B-spline (3, 4, or 5).
|
|
51
|
+
points : list or numpy.ndarray
|
|
52
|
+
The points to be interpolated.
|
|
53
|
+
times : list or numpy.ndarray or None, optional
|
|
54
|
+
The time instants for each point. If None, uses uniform spacing.
|
|
55
|
+
initial_velocity : list or numpy.ndarray or None, optional
|
|
56
|
+
Initial velocity constraint.
|
|
57
|
+
final_velocity : list or numpy.ndarray or None, optional
|
|
58
|
+
Final velocity constraint.
|
|
59
|
+
initial_acceleration : list or numpy.ndarray or None, optional
|
|
60
|
+
Initial acceleration constraint.
|
|
61
|
+
final_acceleration : list or numpy.ndarray or None, optional
|
|
62
|
+
Final acceleration constraint.
|
|
63
|
+
cyclic : bool, default=False
|
|
64
|
+
Whether to use cyclic (periodic) conditions.
|
|
65
|
+
|
|
66
|
+
Raises
|
|
67
|
+
------
|
|
68
|
+
ValueError
|
|
69
|
+
If the degree is not 3, 4, or 5.
|
|
70
|
+
ValueError
|
|
71
|
+
If there are not enough points for the specified degree.
|
|
72
|
+
"""
|
|
73
|
+
# Validate inputs
|
|
74
|
+
if degree not in VALID_DEGREES:
|
|
75
|
+
raise ValueError(f"Degree must be 3, 4, or 5, got {degree}")
|
|
76
|
+
|
|
77
|
+
# Convert inputs to numpy arrays
|
|
78
|
+
if not isinstance(points, np.ndarray):
|
|
79
|
+
points = np.array(points, dtype=np.float64)
|
|
80
|
+
|
|
81
|
+
# Ensure points are 2D
|
|
82
|
+
if points.ndim == 1:
|
|
83
|
+
# For 1D points, reshape to column vector
|
|
84
|
+
points = points.reshape(-1, 1)
|
|
85
|
+
|
|
86
|
+
# Validate number of points relative to degree
|
|
87
|
+
num_points = len(points)
|
|
88
|
+
min_points = degree + 1
|
|
89
|
+
if num_points < min_points:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f"Not enough points for degree {degree} B-spline interpolation. "
|
|
92
|
+
f"Need at least {min_points} points, but got {num_points}. "
|
|
93
|
+
f"Either reduce the degree or provide more points."
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Set up time sequence if not provided
|
|
97
|
+
if times is None:
|
|
98
|
+
times = np.arange(len(points), dtype=np.float64)
|
|
99
|
+
elif not isinstance(times, np.ndarray):
|
|
100
|
+
times = np.array(times, dtype=np.float64)
|
|
101
|
+
|
|
102
|
+
# Store attributes used for interpolation
|
|
103
|
+
self.interp_points = points.copy()
|
|
104
|
+
self.times = times
|
|
105
|
+
self.initial_velocity = initial_velocity
|
|
106
|
+
self.final_velocity = final_velocity
|
|
107
|
+
self.initial_acceleration = initial_acceleration
|
|
108
|
+
self.final_acceleration = final_acceleration
|
|
109
|
+
self.cyclic = cyclic
|
|
110
|
+
|
|
111
|
+
# Compute knots and control points for interpolation
|
|
112
|
+
knots = self._create_knot_vector(degree, points, times)
|
|
113
|
+
|
|
114
|
+
# Create a temporary BSpline to use its methods for basis function calculations
|
|
115
|
+
# Use a single control point since we only need it for basis functions
|
|
116
|
+
temp_control_points = np.zeros((len(knots) - degree - 1, 1))
|
|
117
|
+
self.temp_spline = BSpline(degree, knots, temp_control_points)
|
|
118
|
+
|
|
119
|
+
# Compute control points using the temporary BSpline
|
|
120
|
+
control_points = self._compute_control_points(degree, points, times)
|
|
121
|
+
|
|
122
|
+
# Initialize the base BSpline class with computed values
|
|
123
|
+
super().__init__(degree, knots, control_points)
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
def _create_knot_vector(degree: int, points: np.ndarray, times: np.ndarray) -> np.ndarray:
|
|
127
|
+
"""Create the knot vector based on the degree.
|
|
128
|
+
|
|
129
|
+
- For odd degrees (3, 5): knots at interpolation points (eq. 4.42)
|
|
130
|
+
- For even degrees (4): knots at midpoints (eq. 4.43)
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
degree : int
|
|
135
|
+
The degree of the B-spline.
|
|
136
|
+
points : numpy.ndarray
|
|
137
|
+
The points to be interpolated.
|
|
138
|
+
times : numpy.ndarray
|
|
139
|
+
The time instants for each point.
|
|
140
|
+
|
|
141
|
+
Returns
|
|
142
|
+
-------
|
|
143
|
+
numpy.ndarray
|
|
144
|
+
The computed knot vector.
|
|
145
|
+
"""
|
|
146
|
+
n = len(points) - 1 # n segments (n+1 points)
|
|
147
|
+
p = degree
|
|
148
|
+
|
|
149
|
+
if p % 2 == 1: # Odd degree (3, 5): knots at points
|
|
150
|
+
# Using equation 4.42 from the document
|
|
151
|
+
# u = [t0, ..., t0, t1, ..., tn-1, tn, ..., tn]
|
|
152
|
+
# p+1 times p+1 times
|
|
153
|
+
|
|
154
|
+
knots = np.zeros(n + 2 * p + 1) # Total knots: n + 2p + 1
|
|
155
|
+
|
|
156
|
+
# Set first p+1 knots to t0
|
|
157
|
+
knots[: p + 1] = times[0]
|
|
158
|
+
|
|
159
|
+
# Set internal knots to interpolation points
|
|
160
|
+
knots[p + 1 : p + 1 + n - 1] = times[1:-1]
|
|
161
|
+
|
|
162
|
+
# Set last p+1 knots to tn
|
|
163
|
+
knots[p + n :] = times[-1]
|
|
164
|
+
else: # Even degree (4): knots at midpoints
|
|
165
|
+
# Using equation 4.43 from the document
|
|
166
|
+
# u = [t0, ..., t0, (t0+t1)/2, ..., (tn-1+tn)/2, tn, ..., tn]
|
|
167
|
+
# p+1 times p+1 times
|
|
168
|
+
|
|
169
|
+
knots = np.zeros(n + 2 * p + 2) # Total knots: n + 2p + 2
|
|
170
|
+
|
|
171
|
+
# Set first p+1 knots to t0
|
|
172
|
+
knots[: p + 1] = times[0]
|
|
173
|
+
|
|
174
|
+
# Set internal knots to midpoints between interpolation points
|
|
175
|
+
for i in range(n):
|
|
176
|
+
knots[p + 1 + i] = (times[i] + times[i + 1]) / 2.0
|
|
177
|
+
|
|
178
|
+
# Set last p+1 knots to tn
|
|
179
|
+
knots[p + 1 + n :] = times[-1]
|
|
180
|
+
|
|
181
|
+
return knots
|
|
182
|
+
|
|
183
|
+
def _compute_control_points(
|
|
184
|
+
self, degree: int, points: np.ndarray, times: np.ndarray
|
|
185
|
+
) -> np.ndarray:
|
|
186
|
+
"""Compute the control points by solving the linear system.
|
|
187
|
+
|
|
188
|
+
The system includes:
|
|
189
|
+
- Interpolation conditions (curve passes through each point)
|
|
190
|
+
- Boundary conditions (velocity/acceleration or cyclic conditions)
|
|
191
|
+
|
|
192
|
+
Parameters
|
|
193
|
+
----------
|
|
194
|
+
degree : int
|
|
195
|
+
The degree of the B-spline.
|
|
196
|
+
points : numpy.ndarray
|
|
197
|
+
The points to be interpolated.
|
|
198
|
+
times : numpy.ndarray
|
|
199
|
+
The time instants for each point.
|
|
200
|
+
|
|
201
|
+
Returns
|
|
202
|
+
-------
|
|
203
|
+
numpy.ndarray
|
|
204
|
+
The computed control points.
|
|
205
|
+
|
|
206
|
+
Raises
|
|
207
|
+
------
|
|
208
|
+
ValueError
|
|
209
|
+
If the linear system cannot be solved or is ill-conditioned.
|
|
210
|
+
"""
|
|
211
|
+
n = len(points) - 1 # Number of segments
|
|
212
|
+
p = degree
|
|
213
|
+
|
|
214
|
+
# Determine number of control points based on degree
|
|
215
|
+
num_control_points = (n + 1) + p - 1 if p % 2 == 1 else (n + 1) + p
|
|
216
|
+
|
|
217
|
+
# Determine number of additional conditions needed
|
|
218
|
+
num_additional = p if p % 2 == 0 else p - 1
|
|
219
|
+
|
|
220
|
+
# Create the linear system: A * P = b
|
|
221
|
+
a_matrix = np.zeros((n + 1 + num_additional, num_control_points))
|
|
222
|
+
b = np.zeros((n + 1 + num_additional, points.shape[1]))
|
|
223
|
+
|
|
224
|
+
# Fill the interpolation conditions (points must lie on the curve)
|
|
225
|
+
for i in range(n + 1):
|
|
226
|
+
t = times[i]
|
|
227
|
+
|
|
228
|
+
# Find which basis functions are non-zero at this point
|
|
229
|
+
span = self.temp_spline.find_knot_span(t)
|
|
230
|
+
basis_values = self.temp_spline.basis_functions(t, span)
|
|
231
|
+
|
|
232
|
+
for j in range(p + 1):
|
|
233
|
+
col = span - p + j
|
|
234
|
+
if 0 <= col < num_control_points:
|
|
235
|
+
a_matrix[i, col] = basis_values[j]
|
|
236
|
+
|
|
237
|
+
# The right side is the point to interpolate
|
|
238
|
+
b[i] = points[i]
|
|
239
|
+
|
|
240
|
+
# Add boundary conditions
|
|
241
|
+
row = n + 1 # Start adding boundary conditions after interpolation
|
|
242
|
+
|
|
243
|
+
if self.cyclic:
|
|
244
|
+
# Add cyclic conditions: derivatives at start = derivatives at end
|
|
245
|
+
for k in range(1, num_additional + 1):
|
|
246
|
+
t0 = times[0]
|
|
247
|
+
tn = times[-1]
|
|
248
|
+
|
|
249
|
+
span0 = self.temp_spline.find_knot_span(t0)
|
|
250
|
+
spann = self.temp_spline.find_knot_span(tn)
|
|
251
|
+
|
|
252
|
+
# Get derivative basis functions
|
|
253
|
+
ders0 = self.temp_spline.basis_function_derivatives(t0, span0, k)
|
|
254
|
+
dersn = self.temp_spline.basis_function_derivatives(tn, spann, k)
|
|
255
|
+
|
|
256
|
+
# Fill the derivative constraint: s^(k)(t0) - s^(k)(tn) = 0
|
|
257
|
+
for j in range(p + 1):
|
|
258
|
+
col0 = span0 - p + j
|
|
259
|
+
if 0 <= col0 < num_control_points:
|
|
260
|
+
a_matrix[row, col0] = ders0[k, j]
|
|
261
|
+
|
|
262
|
+
coln = spann - p + j
|
|
263
|
+
if 0 <= coln < num_control_points:
|
|
264
|
+
a_matrix[row, coln] = -dersn[k, j]
|
|
265
|
+
|
|
266
|
+
# Right side is zero for cyclic conditions
|
|
267
|
+
# b[row] already initialized to zero
|
|
268
|
+
|
|
269
|
+
row += 1
|
|
270
|
+
if row >= n + 1 + num_additional:
|
|
271
|
+
break
|
|
272
|
+
else:
|
|
273
|
+
# Add velocity constraints if provided
|
|
274
|
+
if self.initial_velocity is not None and row < n + 1 + num_additional:
|
|
275
|
+
t = times[0]
|
|
276
|
+
span = self.temp_spline.find_knot_span(t)
|
|
277
|
+
ders = self.temp_spline.basis_function_derivatives(t, span, 1)
|
|
278
|
+
|
|
279
|
+
for j in range(p + 1):
|
|
280
|
+
col = span - p + j
|
|
281
|
+
if 0 <= col < num_control_points:
|
|
282
|
+
a_matrix[row, col] = ders[1, j]
|
|
283
|
+
|
|
284
|
+
b[row] = self.initial_velocity
|
|
285
|
+
row += 1
|
|
286
|
+
|
|
287
|
+
if self.final_velocity is not None and row < n + 1 + num_additional:
|
|
288
|
+
t = times[-1]
|
|
289
|
+
span = self.temp_spline.find_knot_span(t)
|
|
290
|
+
ders = self.temp_spline.basis_function_derivatives(t, span, 1)
|
|
291
|
+
|
|
292
|
+
for j in range(p + 1):
|
|
293
|
+
col = span - p + j
|
|
294
|
+
if 0 <= col < num_control_points:
|
|
295
|
+
a_matrix[row, col] = ders[1, j]
|
|
296
|
+
|
|
297
|
+
b[row] = self.final_velocity
|
|
298
|
+
row += 1
|
|
299
|
+
|
|
300
|
+
# Add acceleration constraints if provided
|
|
301
|
+
if self.initial_acceleration is not None and row < n + 1 + num_additional:
|
|
302
|
+
t = times[0]
|
|
303
|
+
span = self.temp_spline.find_knot_span(t)
|
|
304
|
+
ders = self.temp_spline.basis_function_derivatives(t, span, 2)
|
|
305
|
+
|
|
306
|
+
for j in range(p + 1):
|
|
307
|
+
col = span - p + j
|
|
308
|
+
if 0 <= col < num_control_points:
|
|
309
|
+
a_matrix[row, col] = ders[2, j]
|
|
310
|
+
|
|
311
|
+
b[row] = self.initial_acceleration
|
|
312
|
+
row += 1
|
|
313
|
+
|
|
314
|
+
if self.final_acceleration is not None and row < n + 1 + num_additional:
|
|
315
|
+
t = times[-1]
|
|
316
|
+
span = self.temp_spline.find_knot_span(t)
|
|
317
|
+
ders = self.temp_spline.basis_function_derivatives(t, span, 2)
|
|
318
|
+
|
|
319
|
+
for j in range(p + 1):
|
|
320
|
+
col = span - p + j
|
|
321
|
+
if 0 <= col < num_control_points:
|
|
322
|
+
a_matrix[row, col] = ders[2, j]
|
|
323
|
+
|
|
324
|
+
b[row] = self.final_acceleration
|
|
325
|
+
row += 1
|
|
326
|
+
|
|
327
|
+
# If we still need more constraints, add natural spline conditions
|
|
328
|
+
# (zero second derivatives at endpoints)
|
|
329
|
+
while row < n + 1 + num_additional:
|
|
330
|
+
# For cubic splines, use zero second derivatives
|
|
331
|
+
# For higher degree, can use higher derivatives
|
|
332
|
+
deriv_order = min(p - 1, 2)
|
|
333
|
+
|
|
334
|
+
# Alternate between initial and final endpoints
|
|
335
|
+
t = times[0] if row % 2 == 0 else times[-1]
|
|
336
|
+
|
|
337
|
+
span = self.temp_spline.find_knot_span(t)
|
|
338
|
+
ders = self.temp_spline.basis_function_derivatives(t, span, deriv_order)
|
|
339
|
+
|
|
340
|
+
for j in range(p + 1):
|
|
341
|
+
col = span - p + j
|
|
342
|
+
if 0 <= col < num_control_points:
|
|
343
|
+
a_matrix[row, col] = ders[deriv_order, j]
|
|
344
|
+
|
|
345
|
+
# Right side is zero (natural spline condition)
|
|
346
|
+
# b[row] already initialized to zero
|
|
347
|
+
|
|
348
|
+
row += 1
|
|
349
|
+
|
|
350
|
+
# Check if the system is well-posed
|
|
351
|
+
if np.linalg.matrix_rank(a_matrix) < min(a_matrix.shape):
|
|
352
|
+
raise ValueError(
|
|
353
|
+
"Linear system is rank-deficient. This typically occurs when there "
|
|
354
|
+
"are too few points for the specified degree and constraints. "
|
|
355
|
+
"Add more points or reduce the polynomial degree."
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
# Solve the linear system for each coordinate
|
|
359
|
+
try:
|
|
360
|
+
# Check if the system is well-conditioned
|
|
361
|
+
condition_number = np.linalg.cond(a_matrix)
|
|
362
|
+
if condition_number > EPS_P:
|
|
363
|
+
print(
|
|
364
|
+
f"Warning: The linear system is ill-conditioned "
|
|
365
|
+
f"(condition number: {condition_number:.2e})"
|
|
366
|
+
)
|
|
367
|
+
print("This may lead to numerical inaccuracies in the spline interpolation.")
|
|
368
|
+
print(
|
|
369
|
+
"Consider adding more points, using a lower degree, "
|
|
370
|
+
"or adjusting the time distribution."
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# Add a small regularization term for stability
|
|
374
|
+
if condition_number > EPS_P:
|
|
375
|
+
epsilon = EPS_N
|
|
376
|
+
a_matrix += epsilon * np.eye(a_matrix.shape[0], a_matrix.shape[1])
|
|
377
|
+
print(
|
|
378
|
+
f"Adding regularization (epsilon={epsilon}) to improve numerical stability."
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
# Solve for control points
|
|
382
|
+
control_points = np.zeros((num_control_points, points.shape[1]))
|
|
383
|
+
for dim in range(points.shape[1]):
|
|
384
|
+
control_points[:, dim] = solve(a_matrix, b[:, dim])
|
|
385
|
+
|
|
386
|
+
return control_points # noqa: TRY300
|
|
387
|
+
|
|
388
|
+
except np.linalg.LinAlgError as e:
|
|
389
|
+
recommended_points = degree + 2 # Safe minimum
|
|
390
|
+
current_points = len(points)
|
|
391
|
+
|
|
392
|
+
error_msg = f"Failed to solve for control points: {e}\n"
|
|
393
|
+
error_msg += "This is likely due to an ill-posed interpolation problem.\n"
|
|
394
|
+
error_msg += (
|
|
395
|
+
f"For degree {degree} B-splines, you should have at least "
|
|
396
|
+
f"{recommended_points} points "
|
|
397
|
+
)
|
|
398
|
+
error_msg += f"(you provided {current_points}).\n"
|
|
399
|
+
|
|
400
|
+
if self.cyclic:
|
|
401
|
+
error_msg += "When using cyclic conditions, you may need even more points.\n"
|
|
402
|
+
|
|
403
|
+
if (
|
|
404
|
+
self.initial_velocity is not None
|
|
405
|
+
or self.final_velocity is not None
|
|
406
|
+
or self.initial_acceleration is not None
|
|
407
|
+
or self.final_acceleration is not None
|
|
408
|
+
):
|
|
409
|
+
error_msg += (
|
|
410
|
+
"When specifying velocity or acceleration constraints, "
|
|
411
|
+
"you may need more points.\n"
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
if degree in {QUARTIC_DEGREE, QUINTIC_DEGREE}:
|
|
415
|
+
error_msg += (
|
|
416
|
+
f"Consider using a lower degree (e.g., degree=3) "
|
|
417
|
+
f"with {current_points} points.\n"
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
raise ValueError(error_msg) from e
|
|
421
|
+
|
|
422
|
+
def plot_with_points(
|
|
423
|
+
self, num_points: int = 100, show_control_polygon: bool = True, ax: plt.Axes | None = None
|
|
424
|
+
) -> plt.Axes:
|
|
425
|
+
"""Plot the 2D B-spline curve along with the interpolation points.
|
|
426
|
+
|
|
427
|
+
Parameters
|
|
428
|
+
----------
|
|
429
|
+
num_points : int, default=100
|
|
430
|
+
Number of points to generate for the curve.
|
|
431
|
+
show_control_polygon : bool, default=True
|
|
432
|
+
Whether to show the control polygon.
|
|
433
|
+
ax : matplotlib.axes.Axes or None, optional
|
|
434
|
+
Optional matplotlib axis to use.
|
|
435
|
+
|
|
436
|
+
Returns
|
|
437
|
+
-------
|
|
438
|
+
matplotlib.axes.Axes
|
|
439
|
+
The matplotlib axis object.
|
|
440
|
+
|
|
441
|
+
Raises
|
|
442
|
+
------
|
|
443
|
+
ValueError
|
|
444
|
+
If points are not 2D.
|
|
445
|
+
"""
|
|
446
|
+
if self.interp_points.shape[1] != TWO_DIMENSIONAL:
|
|
447
|
+
raise ValueError(f"Points must be 2D for this plot, got {self.interp_points.shape[1]}D")
|
|
448
|
+
|
|
449
|
+
# Plot the B-spline using the parent class method
|
|
450
|
+
ax = self.plot_2d(num_points=num_points, show_control_polygon=show_control_polygon, ax=ax)
|
|
451
|
+
|
|
452
|
+
# Add interpolation points
|
|
453
|
+
ax.plot(
|
|
454
|
+
self.interp_points[:, 0],
|
|
455
|
+
self.interp_points[:, 1],
|
|
456
|
+
"go",
|
|
457
|
+
markersize=8,
|
|
458
|
+
label="Interpolation points",
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
# Add time labels if not too many points
|
|
462
|
+
if len(self.interp_points) <= MAX_POINTS_FOR_LABELS:
|
|
463
|
+
for i, (x, y) in enumerate(self.interp_points):
|
|
464
|
+
ax.text(x, y + 0.1, f"t={self.times[i]:.1f}", horizontalalignment="center")
|
|
465
|
+
|
|
466
|
+
ax.legend()
|
|
467
|
+
return ax
|
|
468
|
+
|
|
469
|
+
def plot_with_points_3d(
|
|
470
|
+
self, num_points: int = 100, show_control_polygon: bool = True, ax: plt.Axes | None = None
|
|
471
|
+
) -> plt.Axes:
|
|
472
|
+
"""Plot the 3D B-spline curve along with the interpolation points.
|
|
473
|
+
|
|
474
|
+
Parameters
|
|
475
|
+
----------
|
|
476
|
+
num_points : int, default=100
|
|
477
|
+
Number of points to generate for the curve.
|
|
478
|
+
show_control_polygon : bool, default=True
|
|
479
|
+
Whether to show the control polygon.
|
|
480
|
+
ax : matplotlib.axes.Axes or None, optional
|
|
481
|
+
Optional matplotlib 3D axis to use.
|
|
482
|
+
|
|
483
|
+
Returns
|
|
484
|
+
-------
|
|
485
|
+
matplotlib.axes.Axes
|
|
486
|
+
The matplotlib 3D axis object.
|
|
487
|
+
|
|
488
|
+
Raises
|
|
489
|
+
------
|
|
490
|
+
ValueError
|
|
491
|
+
If points are not 3D.
|
|
492
|
+
"""
|
|
493
|
+
if self.interp_points.shape[1] != THREE_DIMENSIONAL:
|
|
494
|
+
raise ValueError(f"Points must be 3D for this plot, got {self.interp_points.shape[1]}D")
|
|
495
|
+
|
|
496
|
+
# Plot the B-spline using the parent class method
|
|
497
|
+
ax = self.plot_3d(num_points=num_points, show_control_polygon=show_control_polygon, ax=ax)
|
|
498
|
+
|
|
499
|
+
# Add interpolation points
|
|
500
|
+
ax.scatter(
|
|
501
|
+
self.interp_points[:, 0],
|
|
502
|
+
self.interp_points[:, 1],
|
|
503
|
+
self.interp_points[:, 2],
|
|
504
|
+
color="g",
|
|
505
|
+
s=64,
|
|
506
|
+
label="Interpolation points",
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
# Add time labels if not too many points
|
|
510
|
+
if len(self.interp_points) <= MAX_POINTS_FOR_LABELS:
|
|
511
|
+
for i, (x, y, z) in enumerate(self.interp_points):
|
|
512
|
+
ax.text(x, y, z, f"t={self.times[i]:.1f}", horizontalalignment="center")
|
|
513
|
+
|
|
514
|
+
ax.legend()
|
|
515
|
+
return ax
|