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.
@@ -0,0 +1,444 @@
1
+ import numpy as np
2
+
3
+ from interpolatepy.b_spline import BSpline
4
+ from interpolatepy.tridiagonal_inv import solve_tridiagonal
5
+
6
+
7
+ class CubicBSplineInterpolation(BSpline):
8
+ """
9
+ A class for cubic B-spline interpolation of a set of points.
10
+
11
+ This class implements a global interpolation algorithm for creating a cubic
12
+ B-spline curve that passes through all specified points with C² continuity.
13
+ It inherits from the BSpline class and extends it with interpolation capabilities.
14
+
15
+ Parameters
16
+ ----------
17
+ points : array_like
18
+ The points to interpolate. Should be a 2D array of shape (n, d) where n is
19
+ the number of points and d is the dimension, or a 1D array for single-dimensional points.
20
+ v0 : array_like, optional
21
+ Initial endpoint derivative vector. If None and auto_derivatives is True,
22
+ it will be calculated as (q₁-q₀)/(ū₁-ū₀). If None and auto_derivatives
23
+ is False, zero derivative is used. Default is None.
24
+ vn : array_like, optional
25
+ Final endpoint derivative vector. If None and auto_derivatives is True,
26
+ it will be calculated as (qₙ-qₙ₋₁)/(ūₙ-ūₙ₋₁). If None and auto_derivatives
27
+ is False, zero derivative is used. Default is None.
28
+ method : {'equally_spaced', 'chord_length', 'centripetal'}, optional
29
+ Method for calculating the parameters ūₖ. Default is 'chord_length'.
30
+ auto_derivatives : bool, optional
31
+ Whether to automatically calculate derivatives when not provided.
32
+ Default is False.
33
+
34
+ Attributes
35
+ ----------
36
+ interpolation_points : ndarray
37
+ The points used for interpolation.
38
+ n_interpolation_points : int
39
+ The number of interpolation points.
40
+ u_bars : ndarray
41
+ The parameters ūₖ calculated using the specified method.
42
+ v0 : ndarray
43
+ The initial endpoint derivative vector.
44
+ vn : ndarray
45
+ The final endpoint derivative vector.
46
+
47
+ Notes
48
+ -----
49
+ The implementation follows the global interpolation algorithm described in
50
+ Section 8.4.2 of "The NURBS Book" by Piegl and Tiller.
51
+
52
+ See Also
53
+ --------
54
+ BSpline : Parent class for basic B-spline functionality.
55
+ """
56
+
57
+ # Define constants
58
+ PARAM_DIFF_THRESHOLD = 1e-10
59
+ MIN_POINTS_FOR_TRIDIAGONAL = 2
60
+
61
+ def __init__(
62
+ self,
63
+ points: list | np.ndarray,
64
+ v0: list | np.ndarray | None = None,
65
+ vn: list | np.ndarray | None = None,
66
+ method: str = "chord_length",
67
+ auto_derivatives: bool = False,
68
+ ) -> None:
69
+ """
70
+ Initialize a cubic B-spline interpolation of a set of points.
71
+
72
+ Parameters
73
+ ----------
74
+ points : array_like
75
+ The points to interpolate. Should be a 2D array of shape (n, d) where n is
76
+ the number of points and d is the dimension, or a 1D array for single-dimensional points
77
+ v0 : array_like, optional
78
+ Initial endpoint derivative vector. If None and auto_derivatives is True,
79
+ it will be calculated as (q₁-q₀)/(ū₁-ū₀). If None and auto_derivatives
80
+ is False, zero derivative is used. Default is None.
81
+ vn : array_like, optional
82
+ Final endpoint derivative vector. If None and auto_derivatives is True,
83
+ it will be calculated as (qₙ-qₙ₋₁)/(ūₙ-ūₙ₋₁). If None and auto_derivatives
84
+ is False, zero derivative is used. Default is None.
85
+ method : {'equally_spaced', 'chord_length', 'centripetal'}, optional
86
+ Method for calculating the parameters ūₖ. Default is 'chord_length'.
87
+ auto_derivatives : bool, optional
88
+ Whether to automatically calculate derivatives when not provided.
89
+ Default is False.
90
+
91
+ Returns
92
+ -------
93
+ None
94
+
95
+ Notes
96
+ -----
97
+ This initializer preprocesses the input points and endpoint derivatives,
98
+ calculates the parameters ūₖ, the knot vector, and the control points,
99
+ then initializes the parent BSpline class with the computed values.
100
+ """
101
+ # Convert points to numpy array and ensure correct format
102
+ if not isinstance(points, np.ndarray):
103
+ points = np.array(points, dtype=np.float64)
104
+ else:
105
+ points = points.astype(np.float64)
106
+
107
+ # Get the number of points and the dimension
108
+ n_points = len(points)
109
+ if points.ndim == 1:
110
+ dimension = 1
111
+ # Reshape for a single point
112
+ points = points.reshape(-1, 1)
113
+ else:
114
+ dimension = points.shape[1]
115
+
116
+ # Store the interpolation-specific properties
117
+ self.interpolation_points = points
118
+ self.n_interpolation_points = n_points
119
+
120
+ # Calculate the parameters ūₖ
121
+ self.u_bars = self._calculate_parameters(method)
122
+
123
+ # Process endpoint derivatives
124
+ n = self.n_interpolation_points - 1 # Index of the last point
125
+
126
+ # If v0 is None, calculate it or set to zero vector
127
+ if v0 is None:
128
+ if auto_derivatives and n > 0:
129
+ # Calculate v0 = (q₁ - q₀) / (ū₁ - ū₀)
130
+ u_diff = self.u_bars[1] - self.u_bars[0]
131
+ if abs(u_diff) > self.PARAM_DIFF_THRESHOLD: # Avoid division by zero
132
+ self.v0 = (points[1] - points[0]) / u_diff
133
+ else:
134
+ self.v0 = np.zeros(dimension, dtype=np.float64)
135
+ else:
136
+ self.v0 = np.zeros(dimension, dtype=np.float64)
137
+ else:
138
+ # Convert to numpy array if it's not already
139
+ if not isinstance(v0, np.ndarray):
140
+ v0 = np.array(v0, dtype=np.float64)
141
+ else:
142
+ v0 = v0.astype(np.float64)
143
+
144
+ # Ensure correct shape
145
+ if v0.ndim == 0: # scalar
146
+ self.v0 = np.zeros(dimension, dtype=np.float64)
147
+ elif v0.ndim == 1 and len(v0) == dimension:
148
+ self.v0 = v0
149
+ else:
150
+ raise ValueError(f"v0 must be a vector of dimension {dimension}")
151
+
152
+ # If vn is None, calculate it or set to zero vector
153
+ if vn is None:
154
+ if auto_derivatives and n > 0:
155
+ # Calculate vn = (qₙ - qₙ₋₁) / (ūₙ - ūₙ₋₁)
156
+ u_diff = self.u_bars[n] - self.u_bars[n - 1]
157
+ if abs(u_diff) > self.PARAM_DIFF_THRESHOLD: # Avoid division by zero
158
+ self.vn = (points[n] - points[n - 1]) / u_diff
159
+ else:
160
+ self.vn = np.zeros(dimension, dtype=np.float64)
161
+ else:
162
+ self.vn = np.zeros(dimension, dtype=np.float64)
163
+ else:
164
+ # Convert to numpy array if it's not already
165
+ if not isinstance(vn, np.ndarray):
166
+ vn = np.array(vn, dtype=np.float64)
167
+ else:
168
+ vn = vn.astype(np.float64)
169
+
170
+ # Ensure correct shape
171
+ if vn.ndim == 0: # scalar
172
+ self.vn = np.zeros(dimension, dtype=np.float64)
173
+ elif vn.ndim == 1 and len(vn) == dimension:
174
+ self.vn = vn
175
+ else:
176
+ raise ValueError(f"vn must be a vector of dimension {dimension}")
177
+
178
+ # Calculate the knot vector
179
+ knots = self._calculate_knot_vector()
180
+
181
+ # Calculate the control points
182
+ control_points = self._calculate_control_points(knots)
183
+
184
+ # Initialize the parent BSpline with cubic degree (3)
185
+ super().__init__(3, knots, control_points)
186
+
187
+ # The rest of the class implementation remains unchanged
188
+ def _calculate_parameters(self, method: str) -> np.ndarray:
189
+ """
190
+ Calculate the parameters ūₖ for each point.
191
+
192
+ Parameters
193
+ ----------
194
+ method : {'equally_spaced', 'chord_length', 'centripetal'}
195
+ Method for calculating the parameters:
196
+ - 'equally_spaced': Parameters are evenly spaced between 0 and 1.
197
+ - 'chord_length': Parameters are proportional to the cumulative chord length.
198
+ - 'centripetal': Parameters are proportional to the cumulative chord length
199
+ raised to the power of mu (0.5).
200
+
201
+ Returns
202
+ -------
203
+ ndarray
204
+ The parameters ūₖ with shape (n,) where n is the number of interpolation points.
205
+
206
+ Notes
207
+ -----
208
+ The endpoints are always set to ū₀ = 0 and ūₙ = 1.
209
+
210
+ For 'chord_length' method, the parameter spacing is proportional to the distance
211
+ between interpolation points.
212
+
213
+ For 'centripetal' method, a value of mu = 0.5 is used as recommended in the literature
214
+ for better shape preservation with non-uniform data.
215
+
216
+ Raises
217
+ ------
218
+ ValueError
219
+ If an unknown method is provided.
220
+ """
221
+ n = self.n_interpolation_points - 1 # Index of the last point
222
+
223
+ # Initialize the parameters
224
+ u_bars = np.zeros(self.n_interpolation_points, dtype=np.float64)
225
+
226
+ # Set the endpoints (equation 8.12)
227
+ u_bars[0] = 0.0
228
+ u_bars[n] = 1.0
229
+
230
+ if method == "equally_spaced":
231
+ # Equally spaced parameters (equation 8.12)
232
+ for k in range(1, n):
233
+ u_bars[k] = k / n
234
+
235
+ elif method == "chord_length":
236
+ # Chord length distribution (equation 8.13)
237
+ # Calculate total chord length
238
+ total_length = 0.0
239
+ for k in range(1, n + 1):
240
+ total_length += np.linalg.norm(
241
+ self.interpolation_points[k] - self.interpolation_points[k - 1]
242
+ )
243
+
244
+ # Calculate parameters
245
+ for k in range(1, n):
246
+ u_bars[k] = (
247
+ u_bars[k - 1]
248
+ + np.linalg.norm(
249
+ self.interpolation_points[k] - self.interpolation_points[k - 1]
250
+ )
251
+ / total_length
252
+ )
253
+
254
+ elif method == "centripetal":
255
+ # Centripetal distribution (equation 8.14)
256
+ mu = 0.5 # As recommended in the document
257
+
258
+ # Calculate total "centripetal" length
259
+ total_length = 0.0
260
+ for k in range(1, n + 1):
261
+ total_length += (
262
+ np.linalg.norm(self.interpolation_points[k] - self.interpolation_points[k - 1])
263
+ ** mu
264
+ )
265
+
266
+ # Calculate parameters
267
+ for k in range(1, n):
268
+ u_bars[k] = (
269
+ u_bars[k - 1]
270
+ + np.linalg.norm(
271
+ self.interpolation_points[k] - self.interpolation_points[k - 1]
272
+ )
273
+ ** mu
274
+ / total_length
275
+ )
276
+
277
+ else:
278
+ raise ValueError(
279
+ f"Unknown method: {method}. Options are 'equally_spaced', 'chord_length', "
280
+ f"or 'centripetal'."
281
+ )
282
+
283
+ return u_bars
284
+
285
+ def _calculate_knot_vector(self) -> np.ndarray:
286
+ """
287
+ Calculate the knot vector based on the parameters ūₖ.
288
+
289
+ Returns
290
+ -------
291
+ ndarray
292
+ The knot vector with shape (n+7,) where n is the number of interpolation
293
+ points minus 1. The first 3 knots are equal to ū₀, the last 3 knots
294
+ are equal to ūₙ, and the middle knots are set to the parameters ūⱼ for
295
+ j = 0, ..., n.
296
+
297
+ Notes
298
+ -----
299
+ This follows equation (8.15) from "The NURBS Book":
300
+ - t₀ = t₁ = t₂ = ū₀
301
+ - tⱼ₊₃ = ūⱼ for j = 0,...,n
302
+ - tₙ₊₄ = tₙ₊₅ = tₙ₊₆ = ūₙ
303
+
304
+ This knot vector ensures that the cubic B-spline curve passes through the
305
+ interpolation points.
306
+ """
307
+ n = self.n_interpolation_points - 1 # Index of the last point
308
+
309
+ # Create the knot vector with n+7 elements (as per equation 8.15)
310
+ knots = np.zeros(n + 7, dtype=np.float64)
311
+
312
+ # Set the first 3 knots to ū₀ (equation 8.15)
313
+ knots[0:3] = self.u_bars[0]
314
+
315
+ # Set the last 3 knots to ūₙ (equation 8.15)
316
+ knots[-3:] = self.u_bars[n]
317
+
318
+ # Set the middle knots to ūⱼ for j = 0, ..., n (equation 8.15)
319
+ for j in range(n + 1):
320
+ knots[j + 3] = self.u_bars[j]
321
+
322
+ return knots
323
+
324
+ def _calculate_control_points(self, knots: np.ndarray) -> np.ndarray:
325
+ """
326
+ Calculate the control points by solving a system of equations.
327
+
328
+ Parameters
329
+ ----------
330
+ knots : ndarray
331
+ The knot vector with shape (n+7,) where n is the number of interpolation
332
+ points minus 1.
333
+
334
+ Returns
335
+ -------
336
+ ndarray
337
+ The control points with shape (n+3, d) where n is the number of
338
+ interpolation points minus 1 and d is the dimension.
339
+
340
+ Notes
341
+ -----
342
+ This follows the algorithm from "The NURBS Book" section 8.4.2:
343
+
344
+ First, the first and last two control points are calculated directly:
345
+ - p₀ = q₀ (first interpolation point)
346
+ - p₁ = q₀ + (t₄/3) * v₀ (using first derivative)
347
+ - pₙ₊₁ = qₙ - ((1-tₙ₊₂)/3) * vₙ (using last derivative)
348
+ - pₙ₊₂ = qₙ (last interpolation point)
349
+
350
+ Then, for the remaining control points p₂, p₃, ..., pₙ, a tridiagonal system
351
+ is solved based on the requirement that the curve passes through all
352
+ interpolation points.
353
+
354
+ If there are only two interpolation points, only the directly calculated
355
+ control points are needed.
356
+ """
357
+ n = self.n_interpolation_points - 1 # Index of the last point
358
+ dimension = self.interpolation_points.shape[1]
359
+
360
+ # Initialize the control points array with n+3 elements (p₀ to pₙ₊₂)
361
+ control_points = np.zeros((n + 3, dimension), dtype=np.float64)
362
+
363
+ # Calculate p₀, p₁, pₙ₊₁, and pₙ₊₂ directly from equation (8.16)
364
+ control_points[0] = self.interpolation_points[0]
365
+
366
+ # Calculate p₁ using v0 (scaled by the knot spacing)
367
+ control_points[1] = self.interpolation_points[0] + (knots[4] / 3.0) * self.v0
368
+
369
+ # Calculate pₙ₊₁ using vn (scaled by the knot spacing)
370
+ control_points[n + 1] = (
371
+ self.interpolation_points[n] - ((1.0 - knots[n + 2]) / 3.0) * self.vn
372
+ )
373
+
374
+ control_points[n + 2] = self.interpolation_points[n]
375
+
376
+ # If there are only two points to interpolate, we're done
377
+ if n < self.MIN_POINTS_FOR_TRIDIAGONAL:
378
+ return control_points
379
+
380
+ # For more than two points, solve the tridiagonal system for the remaining control points
381
+ lower_diagonal = np.zeros(n - 1, dtype=np.float64)
382
+ main_diagonal = np.zeros(n - 1, dtype=np.float64)
383
+ upper_diagonal = np.zeros(n - 1, dtype=np.float64)
384
+ right_hand_side = np.zeros((n - 1, dimension), dtype=np.float64)
385
+
386
+ # Create a temporary BSpline for calculating basis functions
387
+ temp_control = np.zeros((n + 3, dimension), dtype=np.float64)
388
+ temp_bs = BSpline(3, knots, temp_control)
389
+
390
+ # Fill the tridiagonal matrix and right-hand side
391
+ for i in range(n - 1):
392
+ k = i + 1 # Point index (from 1 to n-1)
393
+
394
+ # The parameter ūₖ
395
+ u_bar = self.u_bars[k]
396
+
397
+ # Find the knot span for ūₖ
398
+ span = temp_bs.find_knot_span(u_bar)
399
+
400
+ # Calculate the basis functions at ūₖ
401
+ basis_vals = temp_bs.basis_functions(u_bar, span)
402
+
403
+ # The basis function values we need
404
+ b3_k = basis_vals[0]
405
+ b3_k1 = basis_vals[1]
406
+ b3_k2 = basis_vals[2]
407
+
408
+ # Fill the tridiagonal matrix
409
+ if k == 1:
410
+ # First row
411
+ main_diagonal[0] = b3_k1
412
+ upper_diagonal[0] = b3_k2
413
+ right_hand_side[0] = self.interpolation_points[k] - b3_k * control_points[1]
414
+ elif k == n - 1:
415
+ # Last row
416
+ lower_diagonal[k - 2] = b3_k
417
+ main_diagonal[k - 1] = b3_k1
418
+ right_hand_side[k - 1] = (
419
+ self.interpolation_points[k] - b3_k2 * control_points[n + 1]
420
+ )
421
+ else:
422
+ # Middle rows
423
+ lower_diagonal[k - 2] = b3_k
424
+ main_diagonal[k - 1] = b3_k1
425
+ upper_diagonal[k - 1] = b3_k2
426
+ right_hand_side[k - 1] = self.interpolation_points[k]
427
+
428
+ # Solve the tridiagonal system for each dimension
429
+ for d in range(dimension):
430
+ # Extract the right-hand side for this dimension
431
+ rhs = right_hand_side[:, d]
432
+
433
+ # Adjust the lower_diagonal for the tridiagonal solver
434
+ l_diag = np.zeros(n - 1, dtype=np.float64)
435
+ if n - 1 > 1:
436
+ l_diag[1:] = lower_diagonal[:-1]
437
+
438
+ # Solve the system
439
+ solution = solve_tridiagonal(l_diag, main_diagonal, upper_diagonal, rhs)
440
+
441
+ # Set the control points
442
+ control_points[2 : n + 1, d] = solution
443
+
444
+ return control_points