bsplyne 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,1000 @@
1
+ from typing import Iterable, Union
2
+ import json, pickle
3
+ import numpy as np
4
+ import numba as nb
5
+ import scipy.sparse as sps
6
+ from scipy.special import comb
7
+ import matplotlib.pyplot as plt
8
+
9
+
10
+ class BSplineBasis:
11
+ """
12
+ BSpline basis in 1D.
13
+
14
+ A class representing a one-dimensional B-spline basis with functionality for evaluation,
15
+ manipulation and visualization of basis functions. Provides methods for basis function
16
+ evaluation, derivatives computation, knot insertion, order elevation, and integration
17
+ point generation.
18
+
19
+ Attributes
20
+ ----------
21
+ p : int
22
+ Degree of the polynomials composing the basis.
23
+ knot : np.ndarray[np.floating]
24
+ Knot vector defining the B-spline basis. Contains non-decreasing sequence
25
+ of isoparametric coordinates.
26
+ m : int
27
+ Last index of the knot vector (size - 1).
28
+ n : int
29
+ Last index of the basis functions. When evaluated, returns an array of size
30
+ `n + 1`.
31
+ span : tuple[float, float]
32
+ Interval of definition of the basis `(knot[p], knot[m - p])`.
33
+
34
+ Notes
35
+ -----
36
+ The basis functions are defined over the isoparametric space specified by the knot vector.
37
+ Basis function evaluation and manipulation methods use efficient algorithms based on
38
+ Cox-de Boor recursion formulas.
39
+
40
+ See Also
41
+ --------
42
+ `numpy.ndarray` : Array type used for knot vector storage
43
+ `scipy.sparse` : Sparse matrix formats used for basis function evaluations
44
+ """
45
+
46
+ p: int
47
+ knot: np.ndarray[np.floating]
48
+ m: int
49
+ n: int
50
+ span: tuple[float, float]
51
+
52
+ def __init__(self, p: int, knot: Iterable[float]):
53
+ """
54
+ Initialize a B-spline basis with specified degree and knot vector.
55
+
56
+ Parameters
57
+ ----------
58
+ p : int
59
+ Degree of the B-spline polynomials.
60
+ knot : Iterable[float]
61
+ Knot vector defining the B-spline basis. Must be a non-decreasing sequence
62
+ of real numbers.
63
+
64
+ Returns
65
+ -------
66
+ BSplineBasis
67
+ The initialized `BSplineBasis` instance.
68
+
69
+ Notes
70
+ -----
71
+ The knot vector must satisfy these conditions:
72
+ - Size must be at least `p + 2`
73
+ - Must be non-decreasing
74
+ - For non closed B-spline curves, first and last knots must have multiplicity `p + 1`
75
+
76
+ The basis functions are defined over the isoparametric space specified by
77
+ the knot vector. The span of the basis is [`knot[p]`, `knot[m - p]`], where
78
+ `m` is the last index of the knot vector.
79
+
80
+ Examples
81
+ --------
82
+ Create a quadratic B-spline basis with uniform knot vector:
83
+ >>> basis = BSplineBasis(2, [0., 0., 0., 1., 1., 1.])
84
+ """
85
+ self.p = p
86
+ self.knot = np.array(knot, dtype="float")
87
+ self.m = self.knot.size - 1
88
+ self.n = self.m - self.p - 1
89
+ self.span = (self.knot[self.p], self.knot[self.m - self.p])
90
+
91
+ def linspace(self, n_eval_per_elem: int = 10) -> np.ndarray[np.floating]:
92
+ """
93
+ Generate evenly spaced points over the basis span.
94
+
95
+ Creates a set of evaluation points by distributing them uniformly within each knot span
96
+ (element) of the basis. Points are evenly spaced within elements but spacing may vary
97
+ between different elements.
98
+
99
+ Parameters
100
+ ----------
101
+ n_eval_per_elem : int, optional
102
+ Number of evaluation points per element. By default, 10.
103
+
104
+ Returns
105
+ -------
106
+ xi : np.ndarray[np.floating]
107
+ Array of evenly spaced points in isoparametric coordinates over the basis span.
108
+
109
+ Notes
110
+ -----
111
+ The method:
112
+ 1. Identifies unique knot spans (elements) in the isoparametric space
113
+ 2. Distributes points evenly within each element
114
+ 3. Combines points from all elements into a single array
115
+
116
+ Examples
117
+ --------
118
+ >>> basis = BSplineBasis(2, [0., 0., 0., 1., 1., 1.])
119
+ >>> basis.linspace(5)
120
+ array([0. , 0.2, 0.4, 0.6, 0.8, 1. ])
121
+ """
122
+ knot_uniq = np.unique(
123
+ self.knot[
124
+ np.logical_and(self.knot >= self.span[0], self.knot <= self.span[1])
125
+ ]
126
+ )
127
+ xi = np.linspace(knot_uniq[-2], knot_uniq[-1], n_eval_per_elem + 1)
128
+ for i in range(knot_uniq.size - 2, 0, -1):
129
+ xi = np.append(
130
+ np.linspace(
131
+ knot_uniq[i - 1], knot_uniq[i], n_eval_per_elem, endpoint=False
132
+ ),
133
+ xi,
134
+ )
135
+ return xi
136
+
137
+ def linspace_for_integration(
138
+ self,
139
+ n_eval_per_elem: int = 10,
140
+ bounding_box: Union[tuple[float, float], None] = None,
141
+ ) -> tuple[np.ndarray[np.floating], np.ndarray[np.floating]]:
142
+ """
143
+ Generate points and weights for numerical integration over knot spans in the
144
+ isoparametric space. Points are evenly distributed within each element (knot span),
145
+ though spacing may vary between different elements.
146
+
147
+ Parameters
148
+ ----------
149
+ n_eval_per_elem : int, optional
150
+ Number of evaluation points per element. By default, 10.
151
+ bounding_box : Union[tuple[float, float], None], optional
152
+ Lower and upper bounds for integration. If `None`, uses the span of the basis.
153
+ By default, None.
154
+
155
+ Returns
156
+ -------
157
+ xi : np.ndarray[np.floating]
158
+ Array of integration points in isoparametric coordinates, evenly spaced
159
+ within each element.
160
+ dxi : np.ndarray[np.floating]
161
+ Array of corresponding integration weights, which may vary between elements
162
+
163
+ Notes
164
+ -----
165
+ The method generates integration points by:
166
+ 1. Identifying unique knot spans (elements) in the isoparametric space
167
+ 2. Distributing points evenly within each element
168
+ 3. Computing appropriate weights for each point based on the element size
169
+
170
+ When `bounding_box` is provided, integration is restricted to that interval,
171
+ and elements are adjusted accordingly.
172
+
173
+ Examples
174
+ --------
175
+ >>> basis = BSplineBasis(2, [0, 0, 0, 1, 1, 1])
176
+ >>> xi, dxi = basis.linspace_for_integration(5)
177
+ """
178
+ if bounding_box is None:
179
+ lower, upper = self.span
180
+ else:
181
+ lower, upper = bounding_box
182
+ knot_uniq = np.unique(
183
+ self.knot[
184
+ np.logical_and(self.knot >= self.span[0], self.knot <= self.span[1])
185
+ ]
186
+ )
187
+ xi = []
188
+ dxi = []
189
+ for i in range(knot_uniq.size - 1):
190
+ a = knot_uniq[i]
191
+ b = knot_uniq[i + 1]
192
+ if a < upper and b > lower:
193
+ if a < lower and b > upper:
194
+ dxi_i_l = (upper - lower) / n_eval_per_elem
195
+ if (lower - 0.5 * dxi_i_l) < a:
196
+ dxi_i_u = (upper - a) / n_eval_per_elem
197
+ if (upper + 0.5 * dxi_i_u) > b:
198
+ dxi_i = (b - a) / n_eval_per_elem
199
+ else:
200
+ b = upper + 0.5 * dxi_i_u
201
+ dxi_i = dxi_i_u
202
+ else:
203
+ a = lower - 0.5 * dxi_i_l
204
+ dxi_i_u = dxi_i_l
205
+ if (upper + 0.5 * dxi_i_u) > b:
206
+ dxi_i = (b - lower) / n_eval_per_elem
207
+ else:
208
+ dxi_i = dxi_i_u
209
+ b = upper + 0.5 * dxi_i_u
210
+ elif a < lower and b > lower:
211
+ dxi_i_l = (b - lower) / n_eval_per_elem
212
+ if (lower - 0.5 * dxi_i_l) < a:
213
+ dxi_i = (b - a) / n_eval_per_elem
214
+ else:
215
+ a = lower - 0.5 * dxi_i_l
216
+ dxi_i = dxi_i_l
217
+ elif a < upper and b > upper:
218
+ dxi_i_u = (upper - a) / n_eval_per_elem
219
+ if (upper + 0.5 * dxi_i_u) > b:
220
+ dxi_i = (b - a) / n_eval_per_elem
221
+ else:
222
+ b = upper + 0.5 * dxi_i_u
223
+ dxi_i = dxi_i_u
224
+ else:
225
+ dxi_i = (b - a) / n_eval_per_elem
226
+ xi.append(
227
+ np.linspace(a + 0.5 * dxi_i, b - 0.5 * dxi_i, n_eval_per_elem)
228
+ )
229
+ dxi.append(dxi_i * np.ones(n_eval_per_elem))
230
+ xi = np.hstack(xi)
231
+ dxi = np.hstack(dxi)
232
+ return xi, dxi
233
+
234
+ def gauss_legendre_for_integration(
235
+ self,
236
+ n_eval_per_elem: Union[int, None] = None,
237
+ bounding_box: Union[tuple[float, float], None] = None,
238
+ ) -> tuple[np.ndarray[np.floating], np.ndarray[np.floating]]:
239
+ """
240
+ Generate Gauss-Legendre quadrature points and weights for numerical integration over the B-spline basis.
241
+
242
+ Parameters
243
+ ----------
244
+ n_eval_per_elem : Union[int, None], optional
245
+ Number of evaluation points per element. If `None`, takes the value `self.p//2 + 1`.
246
+ By default, None.
247
+ bounding_box : Union[tuple[float, float], None], optional
248
+ Lower and upper bounds for integration. If `None`, uses the span of the basis.
249
+ By default, None.
250
+
251
+ Returns
252
+ -------
253
+ xi : np.ndarray[np.floating]
254
+ Array of Gauss-Legendre quadrature points in isoparametric coordinates.
255
+ dxi : np.ndarray[np.floating]
256
+ Array of corresponding integration weights.
257
+
258
+ Notes
259
+ -----
260
+ The method generates integration points and weights by:
261
+ 1. Identifying unique knot spans (elements) in the isoparametric space
262
+ 2. Computing Gauss-Legendre points and weights for each element
263
+ 3. Transforming points and weights to account for element size
264
+
265
+ When `bounding_box` is provided, integration is restricted to that interval.
266
+
267
+ Examples
268
+ --------
269
+ >>> basis = BSplineBasis(2, [0, 0, 0, 1, 1, 1])
270
+ >>> xi, dxi = basis.gauss_legendre_for_integration(3)
271
+ >>> xi # Gauss-Legendre points
272
+ array([0.11270167, 0.5 , 0.88729833])
273
+ >>> dxi # Integration weights
274
+ array([0.27777778, 0.44444444, 0.27777778])
275
+ """
276
+ if n_eval_per_elem is None:
277
+ n_eval_per_elem = self.p // 2 + 1
278
+ if bounding_box is None:
279
+ lower, upper = self.span
280
+ else:
281
+ lower, upper = bounding_box
282
+ knot_uniq = np.hstack(
283
+ (
284
+ [lower],
285
+ np.unique(
286
+ self.knot[np.logical_and(self.knot > lower, self.knot < upper)]
287
+ ),
288
+ [upper],
289
+ )
290
+ )
291
+ points, wheights = np.polynomial.legendre.leggauss(n_eval_per_elem)
292
+ xi = np.hstack(
293
+ [
294
+ (b - a) / 2 * points + (b + a) / 2
295
+ for a, b in zip(knot_uniq[:-1], knot_uniq[1:])
296
+ ]
297
+ )
298
+ dxi = np.hstack(
299
+ [(b - a) / 2 * wheights for a, b in zip(knot_uniq[:-1], knot_uniq[1:])]
300
+ )
301
+ return xi, dxi
302
+
303
+ def normalize_knots(self):
304
+ """
305
+ Normalize the knot vector to the interval [0, 1].
306
+
307
+ Maps the knot vector to the unit interval by applying an affine transformation that
308
+ preserves the relative spacing between knots. Updates both the knot vector and span
309
+ attributes.
310
+
311
+ Examples
312
+ --------
313
+ >>> basis = BSplineBasis(2, [0., 0., 0., 2., 2., 2.])
314
+ >>> basis.normalize_knots()
315
+ >>> basis.knot
316
+ array([0., 0., 0., 1., 1., 1.])
317
+ >>> basis.span
318
+ (0, 1)
319
+ """
320
+ a, b = self.span
321
+ self.knot = (self.knot - a) / (b - a)
322
+ self.span = (0, 1)
323
+
324
+ def N(self, XI: np.ndarray[np.floating], k: int = 0) -> sps.coo_matrix:
325
+ """
326
+ Compute the k-th derivative of the B-spline basis functions at specified points.
327
+
328
+ Parameters
329
+ ----------
330
+ XI : np.ndarray[np.floating]
331
+ Points in the isoparametric space at which to evaluate the basis functions.
332
+ k : int, optional
333
+ Order of the derivative to compute. By default, 0.
334
+
335
+ Returns
336
+ -------
337
+ DN : sps.coo_matrix
338
+ Sparse matrix containing the k-th derivative values. Each row corresponds to an
339
+ evaluation point, each column to a basis function. Shape is (`XI.size`, `n + 1`).
340
+
341
+ Notes
342
+ -----
343
+ Uses Cox-de Boor recursion formulas to compute basis function derivatives.
344
+ Returns values in sparse matrix format for efficient storage and computation.
345
+
346
+ Examples
347
+ --------
348
+ >>> basis = BSplineBasis(2, [0., 0., 0., 1., 1., 1.])
349
+ >>> basis.N([0., 0.5, 1.]).A # Evaluate basis functions
350
+ array([[1. , 0. , 0. ],
351
+ [0.25, 0.5 , 0.25],
352
+ [0. , 0. , 1. ]])
353
+ >>> basis.N([0., 0.5, 1.], k=1).A # Evaluate first derivatives
354
+ array([[-2., 2., 0.],
355
+ [-1., 0., 1.],
356
+ [ 0., -2., 2.]])
357
+ """
358
+ vals, row, col = _DN(
359
+ self.p, self.m, self.n, self.knot, np.asarray(XI, dtype=np.float64), k
360
+ )
361
+ DN = sps.coo_matrix((vals, (row, col)), shape=(XI.size, self.n + 1))
362
+ return DN
363
+
364
+ def to_dict(self) -> dict:
365
+ """
366
+ Returns a dictionary representation of the BSplineBasis object.
367
+ """
368
+ return {
369
+ "p": self.p,
370
+ "knot": self.knot.tolist(),
371
+ "m": self.m,
372
+ "n": self.n,
373
+ "span": self.span,
374
+ }
375
+
376
+ @classmethod
377
+ def from_dict(cls, data: dict) -> "BSplineBasis":
378
+ """
379
+ Creates a BSplineBasis object from a dictionary representation.
380
+ """
381
+ this = cls(data["p"], data["knot"])
382
+ this.m = data["m"]
383
+ this.n = data["n"]
384
+ this.span = data["span"]
385
+ return this
386
+
387
+ def save(self, filepath: str) -> None:
388
+ """
389
+ Save the BSplineBasis object to a file.
390
+ Control points are optional.
391
+ Supported extensions: json, pkl
392
+ """
393
+ data = self.to_dict()
394
+ ext = filepath.split(".")[-1]
395
+ if ext == "json":
396
+ with open(filepath, "w") as f:
397
+ json.dump(data, f, indent=2)
398
+ elif ext == "pkl":
399
+ with open(filepath, "wb") as f:
400
+ pickle.dump(data, f)
401
+ else:
402
+ raise ValueError(
403
+ f"Unknown extension {ext}. Supported extensions: json, pkl."
404
+ )
405
+
406
+ @classmethod
407
+ def load(cls, filepath: str) -> "BSplineBasis":
408
+ """
409
+ Load a BSplineBasis object from a file.
410
+ May return control points if the file contains them.
411
+ Supported extensions: json, pkl
412
+ """
413
+ ext = filepath.split(".")[-1]
414
+ if ext == "json":
415
+ with open(filepath, "r") as f:
416
+ data = json.load(f)
417
+ elif ext == "pkl":
418
+ with open(filepath, "rb") as f:
419
+ data = pickle.load(f)
420
+ else:
421
+ raise ValueError(
422
+ f"Unknown extension {ext}. Supported extensions: json, pkl."
423
+ )
424
+ this = cls.from_dict(data)
425
+ return this
426
+
427
+ def plotN(self, k: int = 0, show: bool = True):
428
+ """
429
+ Plot the B-spline basis functions or their derivatives over the span.
430
+
431
+ Visualizes each basis function N_i(ξ) or its k-th derivative over its support interval
432
+ using matplotlib. The plot includes proper LaTeX labels and a legend if there are 10 or
433
+ fewer basis functions.
434
+
435
+ Parameters
436
+ ----------
437
+ k : int, optional
438
+ Order of derivative to plot. By default, 0 (plots the basis functions themselves).
439
+ show : bool, optional
440
+ Whether to display the plot immediately. Can be useful to add more stuff to the plot.
441
+ By default, True.
442
+
443
+ Notes
444
+ -----
445
+ - Uses adaptive sampling with points only in regions where basis functions are non-zero
446
+ - Plots each basis function in a different color with LaTeX-formatted labels
447
+ - Legend is automatically hidden if there are more than 10 basis functions
448
+ - The x-axis represents the isoparametric coordinate ξ
449
+
450
+ Examples
451
+ --------
452
+ >>> basis = BSplineBasis(2, [0., 0., 0., 1., 1., 1.])
453
+ >>> basis.plotN() # Plot basis functions
454
+ >>> basis.plotN(k=1) # Plot first derivatives
455
+ """
456
+ n_eval_per_elem = 500 // np.unique(self.knot).size
457
+ for idx in range(self.n + 1):
458
+ XI = np.empty(0, dtype="float")
459
+ for i in range(idx, idx + self.p + 1):
460
+ a = self.knot[i]
461
+ b = self.knot[i + 1]
462
+ if a != b:
463
+ b -= np.finfo("float").eps
464
+ XI = np.append(XI, np.linspace(a, b, n_eval_per_elem))
465
+ DN_idx = np.empty(0, dtype="float")
466
+ for ind in range(XI.size):
467
+ DN_idx_ind = _funcDNElemOneXi(idx, self.p, self.knot, XI[ind], k)
468
+ DN_idx = np.append(DN_idx, DN_idx_ind)
469
+ label = "$N_{" + str(idx) + "}" + ("'" * k) + "(\\xi)$"
470
+ plt.plot(XI, DN_idx, label=label)
471
+ plt.xlabel("$\\xi$")
472
+ unique_knots, counts = np.unique(self.knot, return_counts=True)
473
+ if unique_knots.size <= 10:
474
+ ylim = plt.ylim()
475
+ y_text = ylim[1] + 0.05 * (ylim[1] - ylim[0])
476
+ id = 0
477
+ for xi, n in zip(unique_knots, counts):
478
+ plt.axvline(xi, color="gray", linestyle=":", linewidth=0.8)
479
+ if n == 1:
480
+ plt.text(
481
+ xi,
482
+ y_text,
483
+ f"$\\xi_{{{id}}}$",
484
+ ha="center",
485
+ va="bottom",
486
+ fontsize=10,
487
+ )
488
+ else:
489
+ plt.text(
490
+ xi,
491
+ y_text,
492
+ f"$\\xi_{{{id}-{id + n - 1}}}$",
493
+ ha="center",
494
+ va="bottom",
495
+ fontsize=10,
496
+ )
497
+ id += n
498
+ plt.ylim(ylim[0], y_text + 0.05 * (ylim[1] - ylim[0]))
499
+ if self.n + 1 <= 10:
500
+ plt.legend(loc="best")
501
+ if show:
502
+ plt.show()
503
+
504
+ def _funcDElem(self, i, j, new_knot, p):
505
+ """
506
+ Compute the ij value of the knot insertion matrix D.
507
+
508
+ Parameters
509
+ ----------
510
+ i : int
511
+ Row index of D.
512
+ j : int
513
+ Column index of D.
514
+ new_knot : numpy.array of float
515
+ New knot vector to use.
516
+ p : int
517
+ Degree of the BSpline.
518
+
519
+ Returns
520
+ -------
521
+ D_ij : float
522
+ Value of D at the index ij.
523
+
524
+ """
525
+ if p == 0:
526
+ return int(new_knot[i] >= self.knot[j] and new_knot[i] < self.knot[j + 1])
527
+ if self.knot[j + p] != self.knot[j]:
528
+ rec_p = (new_knot[i + p] - self.knot[j]) / (self.knot[j + p] - self.knot[j])
529
+ rec_p *= self._funcDElem(i, j, new_knot, p - 1)
530
+ else:
531
+ rec_p = 0
532
+ if self.knot[j + p + 1] != self.knot[j + 1]:
533
+ rec_j = (self.knot[j + p + 1] - new_knot[i + p]) / (
534
+ self.knot[j + p + 1] - self.knot[j + 1]
535
+ )
536
+ rec_j *= self._funcDElem(i, j + 1, new_knot, p - 1)
537
+ else:
538
+ rec_j = 0
539
+ D_ij = rec_p + rec_j
540
+ return D_ij
541
+
542
+ def _D(self, new_knot):
543
+ """
544
+ Compute the `D` matrix used to determine the position of the new control
545
+ points for the knot insertion process. The instance of `BSplineBasis`
546
+ won't be modified here.
547
+
548
+ Parameters
549
+ ----------
550
+ new_knot : numpy.array of float
551
+ The new knot vector for the knot insertion.
552
+
553
+ Returns
554
+ -------
555
+ D : scipy.sparse.coo_matrix of float
556
+ The matrix `D` such that :
557
+ newCtrlPtsCoordinate = `D` @ ancientCtrlPtsCoordinate.
558
+
559
+ """
560
+ new_m = new_knot.size - 1
561
+ new_n = new_m - self.p - 1
562
+ loop1 = new_n + 1
563
+ loop2 = self.p + 1
564
+ nb_val_max = loop1 * loop2
565
+ vals = np.empty(nb_val_max, dtype="float")
566
+ row = np.empty(nb_val_max, dtype="int")
567
+ col = np.empty(nb_val_max, dtype="int")
568
+ nb_not_put = 0
569
+ for ind1 in range(loop1):
570
+ sparse_ind1 = ind1
571
+ i = ind1
572
+ new_knot_i = new_knot[i]
573
+ # find {elem} so that new_knot_i \in [knot_{elem}, knot_{{elem} + 1}[
574
+ elem = _findElem(self.p, self.m, self.n, self.knot, new_knot_i)
575
+ # determine D_ij(new_knot_i) for the values of j where we know D_ij(new_knot_i) not equal to 0
576
+ for ind2 in range(loop2):
577
+ sparse_ind2 = sparse_ind1 * loop2 + ind2
578
+ j = ind2 + elem - self.p
579
+ if j < 0 or j > elem:
580
+ nb_not_put += 1
581
+ else:
582
+ sparse_ind = sparse_ind2 - nb_not_put
583
+ vals[sparse_ind] = self._funcDElem(i, j, new_knot, self.p)
584
+ row[sparse_ind] = i
585
+ col[sparse_ind] = j
586
+ if nb_not_put != 0:
587
+ vals = vals[:-nb_not_put]
588
+ row = row[:-nb_not_put]
589
+ col = col[:-nb_not_put]
590
+ D = sps.coo_matrix((vals, (row, col)), shape=(new_n + 1, self.n + 1))
591
+ return D
592
+
593
+ def knotInsertion(self, knots_to_add: np.ndarray[np.floating]) -> sps.coo_matrix:
594
+ """
595
+ Insert knots into the B-spline basis and return the transformation matrix.
596
+
597
+ Parameters
598
+ ----------
599
+ knots_to_add : np.ndarray[np.floating]
600
+ Array of knots to insert into the knot vector.
601
+
602
+ Returns
603
+ -------
604
+ D : sps.coo_matrix
605
+ Transformation matrix such that new control points = `D` @ old control points.
606
+
607
+ Notes
608
+ -----
609
+ Updates the basis by:
610
+ - Inserting new knots into the knot vector
611
+ - Incrementing `m` and `n` by the number of inserted knots
612
+ - Computing transformation matrix `D` for control points update
613
+
614
+ Examples
615
+ --------
616
+ >>> basis = BSplineBasis(2, np.array([0, 0, 0, 1, 1, 1], dtype='float'))
617
+ >>> basis.knotInsertion(np.array([0.33, 0.67], dtype='float')).A
618
+ array([[1. , 0. , 0. ],
619
+ [0.67 , 0.33 , 0. ],
620
+ [0.2211, 0.5578, 0.2211],
621
+ [0. , 0.33 , 0.67 ],
622
+ [0. , 0. , 1. ]])
623
+
624
+ The knot vector is modified (as well as n and m) :
625
+ >>> basis.knot
626
+ array([0. , 0. , 0. , 0.33, 0.67, 1. , 1. , 1. ])
627
+ """
628
+ k = knots_to_add.size
629
+ new_knot = np.sort(np.concatenate((self.knot, knots_to_add), dtype="float"))
630
+ D = self._D(new_knot)
631
+ self.m += k
632
+ self.n += k
633
+ self.knot = new_knot
634
+ return D
635
+
636
+ def orderElevation(self, t: int) -> sps.coo_matrix:
637
+ """
638
+ Elevate the polynomial degree of the B-spline basis and return the transformation matrix.
639
+
640
+ Parameters
641
+ ----------
642
+ t : int
643
+ Amount by which to increase the basis degree. New degree will be current degree plus `t`.
644
+
645
+ Returns
646
+ -------
647
+ STD : sps.coo_matrix
648
+ Transformation matrix for control points such that:
649
+ new_control_points = `STD` @ old_control_points
650
+
651
+ Notes
652
+ -----
653
+ The method:
654
+ 1. Separates B-spline into Bézier segments via knot insertion
655
+ 2. Elevates degree of each Bézier segment
656
+ 3. Recombines segments into elevated B-spline via knot removal
657
+ 4. Updates basis degree, knot vector and other attributes
658
+
659
+ Examples
660
+ --------
661
+ Elevate quadratic basis to cubic:
662
+ >>> basis = BSplineBasis(2, np.array([0, 0, 0, 1, 1, 1], dtype='float'))
663
+ >>> basis.orderElevation(1).A
664
+ array([[1. , 0. , 0. ],
665
+ [0.33333333, 0.66666667, 0. ],
666
+ [0. , 0.66666667, 0.33333333],
667
+ [0. , 0. , 1. ]])
668
+
669
+ The knot vector and the degree are modified (as well as n and m) :
670
+ >>> basis.knot
671
+ array([0., 0., 0., 0., 1., 1., 1., 1.])
672
+ >>> basis.p
673
+ 3
674
+ """
675
+ no_dup, counts = np.unique(self.knot, return_counts=True)
676
+ missed = self.p + 1 - counts
677
+ p0 = self.p
678
+ p1 = p0
679
+ p2 = p1 + t
680
+ p3 = p2
681
+ knot0 = self.knot
682
+ knot1 = np.sort(np.concatenate((knot0, np.repeat(no_dup, missed)), axis=0))
683
+ knot2 = np.sort(np.repeat(no_dup, p2 + 1))
684
+ knot3 = np.sort(np.concatenate((knot0, np.repeat(no_dup, t)), axis=0))
685
+ # step 1 : separate the B-spline in bezier curves by knot insertion
686
+ D = self._D(knot1)
687
+ # step 2 : perform the order elevation on every bezier curve
688
+ num_bezier = no_dup.size - 1
689
+ loop1 = num_bezier
690
+ loop2 = p2 + 1
691
+ loop3 = p1 + 1
692
+ nb_val_max = loop1 * loop2 * loop3
693
+ vals = np.empty(nb_val_max, dtype="float")
694
+ row = np.empty(nb_val_max, dtype="int")
695
+ col = np.empty(nb_val_max, dtype="int")
696
+ nb_not_put = 0
697
+ i_offset = 0
698
+ j_offset = 0
699
+ for ind1 in range(loop1):
700
+ sparse_ind1 = ind1
701
+ for ind2 in range(loop2):
702
+ sparse_ind2 = sparse_ind1 * loop2 + ind2
703
+ i = ind2
704
+ inv_denom = 1 / comb(p2, i) # type: ignore
705
+ for ind3 in range(loop3):
706
+ sparse_ind3 = sparse_ind2 * loop3 + ind3
707
+ j = ind3
708
+ if j < (i - t) or j > i:
709
+ nb_not_put += 1
710
+ else:
711
+ sparse_ind = sparse_ind3 - nb_not_put
712
+ vals[sparse_ind] = comb(p1, j) * comb(t, i - j) * inv_denom # type: ignore
713
+ row[sparse_ind] = i_offset + i
714
+ col[sparse_ind] = j_offset + j
715
+ i_offset += p2 + 1
716
+ j_offset += p1 + 1
717
+ if nb_not_put != 0:
718
+ vals = vals[:-nb_not_put]
719
+ row = row[:-nb_not_put]
720
+ col = col[:-nb_not_put]
721
+ T = sps.coo_matrix(
722
+ (vals, (row, col)), shape=((p2 + 1) * num_bezier, (p1 + 1) * num_bezier)
723
+ )
724
+ # step 3 : come back to B-spline by removing useless knots
725
+ self.__init__(p2, knot2)
726
+ S = self._D(knot3)
727
+ self.__init__(p3, knot3)
728
+ STD = S @ T @ D
729
+ return STD
730
+
731
+ def greville_abscissa(
732
+ self, return_weights: bool = False
733
+ ) -> Union[
734
+ np.ndarray[np.floating], tuple[np.ndarray[np.floating], np.ndarray[np.floating]]
735
+ ]:
736
+ r"""
737
+ Compute the Greville abscissa and optionally their weights for this 1D B-spline basis.
738
+
739
+ The Greville abscissa represent the parametric coordinates associated with each
740
+ control point. They are defined as the average of `p` consecutive internal knots.
741
+
742
+ Parameters
743
+ ----------
744
+ return_weights : bool, optional
745
+ If `True`, also returns the weights (support lengths) associated with each basis function.
746
+ By default, False.
747
+
748
+ Returns
749
+ -------
750
+ greville : np.ndarray[np.floating]
751
+ Array containing the Greville abscissa of size `n + 1`, where `n` is the last index
752
+ of the basis functions in this 1D basis.
753
+
754
+ weight : np.ndarray[np.floating], optional
755
+ Only returned if `return_weights` is `True`.
756
+ Array of the same size as `greville`, containing the length of the support of
757
+ each basis function (difference between the end and start knots of its support).
758
+
759
+ Notes
760
+ -----
761
+ - The Greville abscissa are computed as the average of `p` consecutive knots:
762
+ for the i-th basis function, its abscissa is
763
+ (knot[i+1] + knot[i+2] + ... + knot[i+p]) / p
764
+ - The weights represent the length of the support of each basis function,
765
+ computed as knot[i+p+1] - knot[i].
766
+ - The number of abscissa equals the number of control points.
767
+
768
+ Examples
769
+ --------
770
+ >>> degree = 2
771
+ >>> knot = np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float')
772
+ >>> basis = BSplineBasis(degree, knot)
773
+ >>> greville = basis.greville_abscissa()
774
+ >>> greville
775
+ array([0. , 0.25, 0.75, 1. ])
776
+
777
+ Compute both abscissa and weights:
778
+ >>> greville, weight = basis.greville_abscissa(return_weights=True)
779
+ >>> weight
780
+ array([0.5, 1. , 1. , 0.5])
781
+ """
782
+ greville = (
783
+ np.convolve(self.knot[1:-1], np.ones(self.p, dtype=int), "valid") / self.p
784
+ )
785
+ if return_weights:
786
+ weight = self.knot[(self.p + 1) :] - self.knot[: -(self.p + 1)]
787
+ return greville, weight
788
+ return greville
789
+
790
+
791
+ # %% fast functions for evaluation
792
+
793
+
794
+ @nb.njit(nb.float64(nb.int64, nb.int64, nb.float64[:], nb.float64), cache=True)
795
+ def _funcNElemOneXi(i, p, knot, xi):
796
+ """
797
+ Evaluate the basis function N_i^p(xi) of the BSpline.
798
+
799
+ Parameters
800
+ ----------
801
+ i : int
802
+ Index of the basis function wanted.
803
+ p : int
804
+ Degree of the BSpline evaluated.
805
+ knot : numpy.array of float
806
+ Knot vector of the BSpline basis.
807
+ xi : float
808
+ Value in the parametric space at which the BSpline is evaluated.
809
+
810
+ Returns
811
+ -------
812
+ N_i : float
813
+ Value of the BSpline basis function N_i^p(xi).
814
+
815
+ """
816
+ if p == 0:
817
+ return int(
818
+ (xi >= knot[i] and xi < knot[i + 1])
819
+ or (knot[i + 1] == knot[-1] and xi == knot[i + 1])
820
+ )
821
+ if knot[i + p] != knot[i]:
822
+ rec_p = (xi - knot[i]) / (knot[i + p] - knot[i])
823
+ rec_p *= _funcNElemOneXi(i, p - 1, knot, xi)
824
+ else:
825
+ rec_p = 0
826
+ if knot[i + p + 1] != knot[i + 1]:
827
+ rec_i = (knot[i + p + 1] - xi) / (knot[i + p + 1] - knot[i + 1])
828
+ rec_i *= _funcNElemOneXi(i + 1, p - 1, knot, xi)
829
+ else:
830
+ rec_i = 0
831
+ N_i = rec_p + rec_i
832
+ return N_i
833
+
834
+
835
+ @nb.njit(
836
+ nb.float64(nb.int64, nb.int64, nb.float64[:], nb.float64, nb.int64), cache=True
837
+ )
838
+ def _funcDNElemOneXi(i, p, knot, xi, k):
839
+ """
840
+ Evaluate the `k`-th derivative of the basis function N_i^p(xi) of the
841
+ BSpline.
842
+
843
+ Parameters
844
+ ----------
845
+ i : int
846
+ Index of the basis function wanted.
847
+ p : int
848
+ Degree of the BSpline evaluated.
849
+ knot : numpy.array of float
850
+ Knot vector of the BSpline basis.
851
+ xi : float
852
+ Value in the parametric space at which the BSpline is evaluated.
853
+ k : int
854
+ `k`-th derivative of the BSpline evaluated.
855
+
856
+ Raises
857
+ ------
858
+ ValueError
859
+ Can't compute the `k`-th derivative of a B-spline of degree strictly
860
+ less than `k` or if `k`<0.
861
+
862
+ Returns
863
+ -------
864
+ DN_i : float
865
+ Value of the `k`-th derivative of the BSpline basis function
866
+ N_i^p(xi).
867
+
868
+ """
869
+ if k == 0:
870
+ return _funcNElemOneXi(i, p, knot, xi)
871
+ if p == 0:
872
+ if k >= 0:
873
+ raise ValueError(
874
+ "Impossible to determine the k-th derivative of a B-spline of degree strictly less than k !"
875
+ )
876
+ raise ValueError(
877
+ "Impossible to determine the k-th derivative of a B-spline if k<0 !"
878
+ )
879
+ if knot[i + p] != knot[i]:
880
+ rec_p = p / (knot[i + p] - knot[i])
881
+ rec_p *= _funcDNElemOneXi(i, p - 1, knot, xi, k - 1)
882
+ else:
883
+ rec_p = 0
884
+ if knot[i + p + 1] != knot[i + 1]:
885
+ rec_i = p / (knot[i + p + 1] - knot[i + 1])
886
+ rec_i *= _funcDNElemOneXi(i + 1, p - 1, knot, xi, k - 1)
887
+ else:
888
+ rec_i = 0
889
+ N_i = rec_p - rec_i
890
+ return N_i
891
+
892
+
893
+ @nb.njit(nb.int64(nb.int64, nb.int64, nb.int64, nb.float64[:], nb.float64), cache=True)
894
+ def _findElem(p, m, n, knot, xi):
895
+ """
896
+ Find `i` so that `xi` belongs to
897
+ [ `knot`[`i`], `knot`[`i` + 1] [.
898
+
899
+ Parameters
900
+ ----------
901
+ p : int
902
+ Degree of the polynomials composing the basis.
903
+ m : int
904
+ Last index of the knot vector.
905
+ n : int
906
+ Last index of the basis.
907
+ knot : numpy.array of float
908
+ Knot vector of the BSpline basis.
909
+ xi : float
910
+ Value in the parametric space.
911
+
912
+ Raises
913
+ ------
914
+ ValueError
915
+ If the value of `xi` is outside the definition interval
916
+ of the spline.
917
+
918
+ Returns
919
+ -------
920
+ i : int
921
+ Index of the first knot of the interval in which `xi` is bounded.
922
+
923
+ """
924
+ if xi == knot[m - p]:
925
+ return n
926
+ i = 0
927
+ pastrouve = True
928
+ while i <= n and pastrouve:
929
+ pastrouve = xi < knot[i] or xi >= knot[i + 1]
930
+ i += 1
931
+ if pastrouve:
932
+ raise ValueError("xi is outside the definition interval of the spline !")
933
+ # print("ValueError : xi=", xi, " is outside the definition interval [", knot[p], ", ", knot[m - p], "] of the spline !")
934
+ # return None
935
+ i -= 1
936
+ return i
937
+
938
+
939
+ @nb.njit(
940
+ nb.types.UniTuple.from_types((nb.float64[:], nb.int64[:], nb.int64[:]))(
941
+ nb.int64, nb.int64, nb.int64, nb.float64[:], nb.float64[:], nb.int64
942
+ ),
943
+ cache=True,
944
+ )
945
+ def _DN(p, m, n, knot, XI, k):
946
+ """
947
+ Compute the `k`-th derivative of the BSpline basis functions for a set
948
+ of values in the parametric space.
949
+
950
+ Parameters
951
+ ----------
952
+ p : int
953
+ Degree of the polynomials composing the basis.
954
+ m : int
955
+ Last index of the knot vector.
956
+ n : int
957
+ Last index of the basis.
958
+ knot : numpy.array of float
959
+ Knot vector of the BSpline basis.
960
+ XI : numpy.array of float
961
+ Values in the parametric space at which the BSpline is evaluated.
962
+ k : int
963
+ `k`-th derivative of the BSpline evaluated. The default is 0.
964
+
965
+ Returns
966
+ -------
967
+ (vals, row, col) : (numpy.array of float, numpy.array of int, numpy.array of int)
968
+ Values and indices of the `k`-th derivative matrix of the BSpline
969
+ basis functions in the columns for each value of `XI` in the rows.
970
+
971
+ """
972
+ loop1 = XI.size
973
+ loop2 = p + 1
974
+ nb_val_max = loop1 * loop2
975
+ vals = np.empty(nb_val_max, dtype="float")
976
+ row = np.empty(nb_val_max, dtype="int")
977
+ col = np.empty(nb_val_max, dtype="int")
978
+ nb_not_put = 0
979
+ for ind1 in range(loop1): # nb.p
980
+ sparse_ind1 = ind1
981
+ i_xi = ind1
982
+ xi = XI.flat[i_xi]
983
+ # find {elem} so that \xi \in [\xi_{elem}, \xi_{{elem} + 1}[
984
+ elem = _findElem(p, m, n, knot, xi)
985
+ # determine DN_i(\xi) for the values of i where we know DN_i(\xi) not equal to 0
986
+ for ind2 in range(loop2):
987
+ sparse_ind2 = sparse_ind1 * loop2 + ind2
988
+ i = ind2 + elem - p
989
+ if i < 0:
990
+ nb_not_put += 1
991
+ else:
992
+ sparse_ind = sparse_ind2 - nb_not_put
993
+ vals[sparse_ind] = _funcDNElemOneXi(i, p, knot, xi, k)
994
+ row[sparse_ind] = i_xi
995
+ col[sparse_ind] = i
996
+ if nb_not_put != 0:
997
+ vals = vals[:-nb_not_put]
998
+ row = row[:-nb_not_put]
999
+ col = col[:-nb_not_put]
1000
+ return (vals, row, col)