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,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