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.
- bsplyne/__init__.py +55 -0
- bsplyne/b_spline.py +2464 -0
- bsplyne/b_spline_basis.py +1000 -0
- bsplyne/geometries_in_3D.py +1193 -0
- bsplyne/multi_patch_b_spline.py +1731 -0
- bsplyne/my_wide_product.py +209 -0
- bsplyne/parallel_utils.py +378 -0
- bsplyne/save_utils.py +141 -0
- bsplyne-1.0.0.dist-info/METADATA +91 -0
- bsplyne-1.0.0.dist-info/RECORD +13 -0
- bsplyne-1.0.0.dist-info/WHEEL +5 -0
- bsplyne-1.0.0.dist-info/licenses/LICENSE.txt +70 -0
- bsplyne-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -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)
|