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
bsplyne/b_spline.py
ADDED
|
@@ -0,0 +1,2464 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Iterable, Literal, Union
|
|
3
|
+
import json, pickle
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import scipy.sparse as sps
|
|
7
|
+
import meshio as io
|
|
8
|
+
import matplotlib as mpl
|
|
9
|
+
from scipy.interpolate import griddata
|
|
10
|
+
|
|
11
|
+
from bsplyne.b_spline_basis import BSplineBasis
|
|
12
|
+
from bsplyne.my_wide_product import my_wide_product
|
|
13
|
+
from bsplyne.save_utils import writePVD
|
|
14
|
+
|
|
15
|
+
_can_visualize_pyvista = False
|
|
16
|
+
try:
|
|
17
|
+
import pyvista as pv
|
|
18
|
+
|
|
19
|
+
_can_visualize_pyvista = True
|
|
20
|
+
except ImportError:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BSpline:
|
|
25
|
+
"""
|
|
26
|
+
BSpline class for representing and manipulating B-spline curves, surfaces and volumes.
|
|
27
|
+
|
|
28
|
+
A class providing functionality for evaluating, manipulating and visualizing B-splines of arbitrary dimension.
|
|
29
|
+
Supports knot insertion, order elevation, and visualization through Paraview and Matplotlib.
|
|
30
|
+
|
|
31
|
+
Attributes
|
|
32
|
+
----------
|
|
33
|
+
NPa : int
|
|
34
|
+
Dimension of the isoparametric space.
|
|
35
|
+
bases : np.ndarray[BSplineBasis]
|
|
36
|
+
Array containing `BSplineBasis` instances for each isoparametric dimension.
|
|
37
|
+
|
|
38
|
+
Notes
|
|
39
|
+
-----
|
|
40
|
+
- Supports B-splines of arbitrary dimension (curves, surfaces, volumes, etc.)
|
|
41
|
+
- Provides methods for evaluation, derivatives, refinement and visualization
|
|
42
|
+
- Uses Cox-de Boor recursion formulas for efficient basis function evaluation
|
|
43
|
+
- Visualization available through Paraview (VTK) and Matplotlib
|
|
44
|
+
|
|
45
|
+
See Also
|
|
46
|
+
--------
|
|
47
|
+
`BSplineBasis` : Class representing one-dimensional B-spline basis functions
|
|
48
|
+
`numpy.ndarray` : Array type used for control points and evaluations
|
|
49
|
+
`scipy.sparse` : Sparse matrix formats used for basis function evaluations
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
NPa: int
|
|
53
|
+
bases: np.ndarray[BSplineBasis]
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self, degrees: Iterable[int], knots: Iterable[np.ndarray[np.floating]]
|
|
57
|
+
):
|
|
58
|
+
"""
|
|
59
|
+
Initialize a `BSpline` instance with specified degrees and knot vectors.
|
|
60
|
+
|
|
61
|
+
Creates a `BSpline` object by generating basis functions for each isoparametric dimension
|
|
62
|
+
using the provided polynomial degrees and knot vectors.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
degrees : Iterable[int]
|
|
67
|
+
Collection of polynomial degrees for each isoparametric dimension.
|
|
68
|
+
The length determines the dimensionality of the parametric space (`NPa`).
|
|
69
|
+
For example:
|
|
70
|
+
- [p] for a curve
|
|
71
|
+
- [p, q] for a surface
|
|
72
|
+
- [p, q, r] for a volume
|
|
73
|
+
- ...
|
|
74
|
+
|
|
75
|
+
knots : Iterable[np.ndarray[np.floating]]
|
|
76
|
+
Collection of knot vectors for each isoparametric dimension.
|
|
77
|
+
Each knot vector must be a numpy array of `floats`.
|
|
78
|
+
The number of knot vectors must match the number of degrees.
|
|
79
|
+
For a degree `p`, the knot vector must have size `m + 1` where `m>=p`.
|
|
80
|
+
|
|
81
|
+
Notes
|
|
82
|
+
-----
|
|
83
|
+
- The number of control points in each dimension will be `m - p` where `m` is
|
|
84
|
+
the size of the knot vector minus 1 and `p` is the degree
|
|
85
|
+
- Each knot vector must be non-decreasing
|
|
86
|
+
- The multiplicity of each knot must not exceed `p + 1`
|
|
87
|
+
|
|
88
|
+
Examples
|
|
89
|
+
--------
|
|
90
|
+
Create a 2D B-spline surface:
|
|
91
|
+
>>> degrees = [2, 2]
|
|
92
|
+
>>> knots = [np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float'),
|
|
93
|
+
... np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float')]
|
|
94
|
+
>>> spline = BSpline(degrees, knots)
|
|
95
|
+
|
|
96
|
+
Create a 1D B-spline curve:
|
|
97
|
+
>>> degree = [3]
|
|
98
|
+
>>> knot = [np.array([0, 0, 0, 0, 1, 1, 1, 1], dtype='float')]
|
|
99
|
+
>>> curve = BSpline(degree, knot)
|
|
100
|
+
"""
|
|
101
|
+
self.NPa = len(degrees)
|
|
102
|
+
self.bases = np.empty(self.NPa, dtype="object")
|
|
103
|
+
for idx in range(self.NPa):
|
|
104
|
+
p = degrees[idx]
|
|
105
|
+
knot = knots[idx]
|
|
106
|
+
self.bases[idx] = BSplineBasis(p, knot)
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def from_bases(cls, bases: Iterable[BSplineBasis]) -> "BSpline":
|
|
110
|
+
"""
|
|
111
|
+
Create a BSpline instance from an array of `BSplineBasis` objects.
|
|
112
|
+
This is an alternative constructor that allows direct initialization from
|
|
113
|
+
existing basis functions rather than creating new ones from degrees and knot
|
|
114
|
+
vectors.
|
|
115
|
+
|
|
116
|
+
Parameters
|
|
117
|
+
----------
|
|
118
|
+
bases : Iterable[BSplineBasis]
|
|
119
|
+
An iterable (e.g. list, tuple, array) containing `BSplineBasis` instances.
|
|
120
|
+
Each basis represents one parametric dimension of the resulting B-spline.
|
|
121
|
+
The number of bases determines the dimensionality of the parametric space.
|
|
122
|
+
|
|
123
|
+
Returns
|
|
124
|
+
-------
|
|
125
|
+
BSpline
|
|
126
|
+
A new `BSpline` instance with the provided basis functions.
|
|
127
|
+
|
|
128
|
+
Notes
|
|
129
|
+
-----
|
|
130
|
+
- The method initializes a new `BSpline` instance with empty degrees and knots
|
|
131
|
+
- The bases array is populated with the provided `BSplineBasis` objects
|
|
132
|
+
- The dimensionality (`NPa`) is determined by the number of basis functions
|
|
133
|
+
|
|
134
|
+
Examples
|
|
135
|
+
--------
|
|
136
|
+
>>> basis1 = BSplineBasis(2, np.array([0, 0, 0, 1, 1, 1]))
|
|
137
|
+
>>> basis2 = BSplineBasis(2, np.array([0, 0, 0, 0.5, 1, 1, 1]))
|
|
138
|
+
>>> spline = BSpline.from_bases([basis1, basis2])
|
|
139
|
+
"""
|
|
140
|
+
self = cls([], [])
|
|
141
|
+
self.NPa = len(bases)
|
|
142
|
+
self.bases = np.empty(self.NPa, dtype="object")
|
|
143
|
+
self.bases[:] = bases
|
|
144
|
+
return self
|
|
145
|
+
|
|
146
|
+
def getDegrees(self) -> np.ndarray[np.integer]:
|
|
147
|
+
"""
|
|
148
|
+
Returns the polynomial degree of each basis function in the isoparametric space.
|
|
149
|
+
|
|
150
|
+
Returns
|
|
151
|
+
-------
|
|
152
|
+
degrees : np.ndarray[np.integer]
|
|
153
|
+
Array containing the polynomial degrees of the B-spline basis functions.
|
|
154
|
+
The array has length `NPa` (dimension of isoparametric space), where each element
|
|
155
|
+
represents the degree of the corresponding isoparametric dimension.
|
|
156
|
+
|
|
157
|
+
Notes
|
|
158
|
+
-----
|
|
159
|
+
- For a curve (1D), returns [degree_xi]
|
|
160
|
+
- For a surface (2D), returns [degree_xi, degree_eta]
|
|
161
|
+
- For a volume (3D), returns [degree_xi, degree_eta, degree_zeta]
|
|
162
|
+
- ...
|
|
163
|
+
|
|
164
|
+
Examples
|
|
165
|
+
--------
|
|
166
|
+
>>> degrees = np.array([2, 2], dtype='int')
|
|
167
|
+
>>> knots = [np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float'),
|
|
168
|
+
... np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float')]
|
|
169
|
+
>>> spline = BSpline(degrees, knots)
|
|
170
|
+
>>> spline.getDegrees()
|
|
171
|
+
array([2, 2])
|
|
172
|
+
"""
|
|
173
|
+
degrees = np.array([basis.p for basis in self.bases], dtype="int")
|
|
174
|
+
return degrees
|
|
175
|
+
|
|
176
|
+
def getKnots(self) -> list[np.ndarray[np.floating]]:
|
|
177
|
+
"""
|
|
178
|
+
Returns the knot vector of each basis function in the isoparametric space.
|
|
179
|
+
|
|
180
|
+
This method collects all knot vectors from each `BSplineBasis` instance stored
|
|
181
|
+
in the `bases` array. The knot vectors define the isoparametric space partitioning
|
|
182
|
+
and the regularity properties of the B-spline.
|
|
183
|
+
|
|
184
|
+
Returns
|
|
185
|
+
-------
|
|
186
|
+
knots : list[np.ndarray[np.floating]]
|
|
187
|
+
List containing the knot vectors of the B-spline basis functions.
|
|
188
|
+
The list has length `NPa` (dimension of isoparametric space), where each element
|
|
189
|
+
is a `numpy.ndarray` containing the knots for the corresponding isoparametric dimension.
|
|
190
|
+
|
|
191
|
+
Notes
|
|
192
|
+
-----
|
|
193
|
+
- For a curve (1D), returns [`knots_xi`]
|
|
194
|
+
- For a surface (2D), returns [`knots_xi`, `knots_eta`]
|
|
195
|
+
- For a volume (3D), returns [`knots_xi`, `knots_eta`, `knots_zeta`]
|
|
196
|
+
- Each knot vector must be non-decreasing
|
|
197
|
+
- The multiplicity of interior knots determines the continuity at that point
|
|
198
|
+
|
|
199
|
+
Examples
|
|
200
|
+
--------
|
|
201
|
+
>>> degrees = [2, 2]
|
|
202
|
+
>>> knots = [np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float'),
|
|
203
|
+
... np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float')]
|
|
204
|
+
>>> spline = BSpline(degrees, knots)
|
|
205
|
+
>>> spline.getKnots()
|
|
206
|
+
[array([0., 0., 0., 0.5, 1., 1., 1.]),
|
|
207
|
+
array([0., 0., 0., 0.5, 1., 1., 1.])]
|
|
208
|
+
"""
|
|
209
|
+
knots = [basis.knot for basis in self.bases]
|
|
210
|
+
return knots
|
|
211
|
+
|
|
212
|
+
def getCtrlShape(self) -> tuple[int]:
|
|
213
|
+
"""
|
|
214
|
+
Get the shape of the control grid (number of control points per dimension).
|
|
215
|
+
|
|
216
|
+
This method returns a tuple giving, for each isoparametric direction,
|
|
217
|
+
the number of control points associated with the corresponding B-spline basis.
|
|
218
|
+
In each dimension, this number is equal to `n + 1`, where `n` is the highest
|
|
219
|
+
basis function index.
|
|
220
|
+
|
|
221
|
+
Returns
|
|
222
|
+
-------
|
|
223
|
+
tuple of int
|
|
224
|
+
A tuple giving the number of control points in each dimension.
|
|
225
|
+
|
|
226
|
+
Notes
|
|
227
|
+
-----
|
|
228
|
+
- For a curve (1D), returns a single integer (`n1 + 1`,)
|
|
229
|
+
- For a surface (2D), returns (`n1 + 1`, `n2 + 1`)
|
|
230
|
+
- For a volume (3D), returns (`n1 + 1`, `n2 + 1`, `n3 + 1`)
|
|
231
|
+
- The product of these values gives the total number of control points,
|
|
232
|
+
identical to the number of basis functions (`getNbFunc()`).
|
|
233
|
+
|
|
234
|
+
Examples
|
|
235
|
+
--------
|
|
236
|
+
>>> degrees = [2, 2]
|
|
237
|
+
>>> knots = [np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float'),
|
|
238
|
+
... np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float')]
|
|
239
|
+
>>> spline = BSpline(degrees, knots)
|
|
240
|
+
>>> spline.getCtrlShape()
|
|
241
|
+
(4, 4)
|
|
242
|
+
"""
|
|
243
|
+
return tuple(basis.n + 1 for basis in self.bases)
|
|
244
|
+
|
|
245
|
+
def getNbFunc(self) -> int:
|
|
246
|
+
"""
|
|
247
|
+
Compute the total number of basis functions in the B-spline.
|
|
248
|
+
|
|
249
|
+
This method calculates the total number of basis functions by multiplying
|
|
250
|
+
the number of basis functions in each isoparametric dimension (`n + 1` for each dimension).
|
|
251
|
+
|
|
252
|
+
Returns
|
|
253
|
+
-------
|
|
254
|
+
int
|
|
255
|
+
Total number of basis functions in the B-spline. This is equal to
|
|
256
|
+
the product of (`n + 1`) for each basis, where `n` is the last index
|
|
257
|
+
of each basis function.
|
|
258
|
+
|
|
259
|
+
Notes
|
|
260
|
+
-----
|
|
261
|
+
- For a curve (1D), returns (`n + 1`)
|
|
262
|
+
- For a surface (2D), returns (`n1 + 1`) × (`n2 + 1`)
|
|
263
|
+
- For a volume (3D), returns (`n1 + 1`) × (`n2 + 1`) × (`n3 + 1`)
|
|
264
|
+
- The number of basis functions equals the number of control points needed
|
|
265
|
+
|
|
266
|
+
Examples
|
|
267
|
+
--------
|
|
268
|
+
>>> degrees = [2, 2]
|
|
269
|
+
>>> knots = [np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float'),
|
|
270
|
+
... np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float')]
|
|
271
|
+
>>> spline = BSpline(degrees, knots)
|
|
272
|
+
>>> spline.getNbFunc()
|
|
273
|
+
16
|
|
274
|
+
"""
|
|
275
|
+
return np.prod(self.getCtrlShape())
|
|
276
|
+
|
|
277
|
+
def getSpans(self) -> list[tuple[float, float]]:
|
|
278
|
+
"""
|
|
279
|
+
Returns the span of each basis function in the isoparametric space.
|
|
280
|
+
|
|
281
|
+
This method collects the spans (intervals of definition) from each `BSplineBasis`
|
|
282
|
+
instance stored in the `bases` array.
|
|
283
|
+
|
|
284
|
+
Returns
|
|
285
|
+
-------
|
|
286
|
+
spans : list[tuple[float, float]]
|
|
287
|
+
List containing the spans of the B-spline basis functions.
|
|
288
|
+
The list has length `NPa` (dimension of isoparametric space), where each element
|
|
289
|
+
is a tuple (`a`, `b`) containing the lower and upper bounds of the span
|
|
290
|
+
for the corresponding isoparametric dimension.
|
|
291
|
+
|
|
292
|
+
Notes
|
|
293
|
+
-----
|
|
294
|
+
- For a curve (1D), returns [(`xi_min`, `xi_max`)]
|
|
295
|
+
- For a surface (2D), returns [(`xi_min`, `xi_max`), (`eta_min`, `eta_max`)]
|
|
296
|
+
- For a volume (3D), returns [(`xi_min`, `xi_max`), (`eta_min`, `eta_max`), (`zeta_min`, `zeta_max`)]
|
|
297
|
+
- The span represents the interval where the B-spline is defined
|
|
298
|
+
- Each span is determined by the `p`-th and `(m - p)`-th knots, where `p` is the degree
|
|
299
|
+
and `m` is the last index of the knot vector
|
|
300
|
+
|
|
301
|
+
Examples
|
|
302
|
+
--------
|
|
303
|
+
>>> degrees = [2, 2]
|
|
304
|
+
>>> knots = [np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float'),
|
|
305
|
+
... np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float')]
|
|
306
|
+
>>> spline = BSpline(degrees, knots)
|
|
307
|
+
>>> spline.getSpans()
|
|
308
|
+
[(0.0, 1.0), (0.0, 1.0)]
|
|
309
|
+
"""
|
|
310
|
+
spans = [basis.span for basis in self.bases]
|
|
311
|
+
return spans
|
|
312
|
+
|
|
313
|
+
# def get_indices(self, begining=0):
|
|
314
|
+
# """
|
|
315
|
+
# Create an array containing the indices of the control points of
|
|
316
|
+
# the B-spline.
|
|
317
|
+
#
|
|
318
|
+
# Parameters
|
|
319
|
+
# ----------
|
|
320
|
+
# begining : int, optional
|
|
321
|
+
# First index of the arrayof indices, by default 0
|
|
322
|
+
#
|
|
323
|
+
# Returns
|
|
324
|
+
# -------
|
|
325
|
+
# indices : np.array of int
|
|
326
|
+
# Indices of the control points in the same shape as the
|
|
327
|
+
# control points.
|
|
328
|
+
# """
|
|
329
|
+
# indices = np.arange(begining, begining + self.ctrl_pts.size).reshape(self.ctrl_pts.shape)
|
|
330
|
+
# return indices
|
|
331
|
+
|
|
332
|
+
def linspace(
|
|
333
|
+
self, n_eval_per_elem: Union[int, Iterable[int]] = 10
|
|
334
|
+
) -> tuple[np.ndarray[np.floating], ...]:
|
|
335
|
+
"""
|
|
336
|
+
Generate sets of evaluation points over the span of each basis in the isoparametric space.
|
|
337
|
+
|
|
338
|
+
This method creates evenly spaced points for each isoparametric dimension by calling
|
|
339
|
+
`linspace` on each `BSplineBasis` instance stored in the `bases` array.
|
|
340
|
+
|
|
341
|
+
Parameters
|
|
342
|
+
----------
|
|
343
|
+
n_eval_per_elem : Union[int, Iterable[int]], optional
|
|
344
|
+
Number of evaluation points per element for each isoparametric dimension.
|
|
345
|
+
If an `int` is provided, the same number is used for all dimensions.
|
|
346
|
+
If an `Iterable` is provided, each value corresponds to a different dimension.
|
|
347
|
+
By default, 10.
|
|
348
|
+
|
|
349
|
+
Returns
|
|
350
|
+
-------
|
|
351
|
+
XI : tuple[np.ndarray[np.floating], ...]
|
|
352
|
+
Tuple containing arrays of evaluation points for each isoparametric dimension.
|
|
353
|
+
The tuple has length `NPa` (dimension of isoparametric space).
|
|
354
|
+
|
|
355
|
+
Notes
|
|
356
|
+
-----
|
|
357
|
+
- For a curve (1D), returns (`xi` points, )
|
|
358
|
+
- For a surface (2D), returns (`xi` points, `eta` points)
|
|
359
|
+
- For a volume (3D), returns (`xi` points, `eta` points, `zeta` points)
|
|
360
|
+
- The number of points returned for each dimension depends on the number of
|
|
361
|
+
elements in that dimension times the value of `n_eval_per_elem`
|
|
362
|
+
|
|
363
|
+
Examples
|
|
364
|
+
--------
|
|
365
|
+
>>> degrees = [2, 2]
|
|
366
|
+
>>> knots = [np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float'),
|
|
367
|
+
... np.array([0, 0, 0, 1, 1, 1], dtype='float')]
|
|
368
|
+
>>> spline = BSpline(degrees, knots)
|
|
369
|
+
>>> xi, eta = spline.linspace(n_eval_per_elem=2)
|
|
370
|
+
>>> xi
|
|
371
|
+
array([0. , 0.25, 0.5 , 0.75, 1. ])
|
|
372
|
+
>>> eta
|
|
373
|
+
array([0. , 0.5, 1. ])
|
|
374
|
+
"""
|
|
375
|
+
if type(n_eval_per_elem) is int:
|
|
376
|
+
n_eval_per_elem = [n_eval_per_elem] * self.NPa # type: ignore
|
|
377
|
+
XI = tuple([basis.linspace(n) for basis, n in zip(self.bases, n_eval_per_elem)]) # type: ignore
|
|
378
|
+
return XI
|
|
379
|
+
|
|
380
|
+
def linspace_for_integration(
|
|
381
|
+
self,
|
|
382
|
+
n_eval_per_elem: Union[int, Iterable[int]] = 10,
|
|
383
|
+
bounding_box: Union[Iterable, None] = None,
|
|
384
|
+
) -> tuple[
|
|
385
|
+
tuple[np.ndarray[np.floating], ...], tuple[np.ndarray[np.floating], ...]
|
|
386
|
+
]:
|
|
387
|
+
"""
|
|
388
|
+
Generate sets of evaluation points and their integration weights over each basis span.
|
|
389
|
+
|
|
390
|
+
This method creates evenly spaced points and their corresponding integration weights
|
|
391
|
+
for each isoparametric dimension by calling `linspace_for_integration` on each
|
|
392
|
+
`BSplineBasis` instance stored in the `bases` array.
|
|
393
|
+
|
|
394
|
+
Parameters
|
|
395
|
+
----------
|
|
396
|
+
n_eval_per_elem : Union[int, Iterable[int]], optional
|
|
397
|
+
Number of evaluation points per element for each isoparametric dimension.
|
|
398
|
+
If an `int` is provided, the same number is used for all dimensions.
|
|
399
|
+
If an `Iterable` is provided, each value corresponds to a different dimension.
|
|
400
|
+
By default, 10.
|
|
401
|
+
|
|
402
|
+
bounding_box : Union[Iterable[tuple[float, float]], None], optional
|
|
403
|
+
Lower and upper bounds for each isoparametric dimension.
|
|
404
|
+
If `None`, uses the span of each basis.
|
|
405
|
+
Format: [(`xi_min`, `xi_max`), (`eta_min`, `eta_max`), ...].
|
|
406
|
+
By default, None.
|
|
407
|
+
|
|
408
|
+
Returns
|
|
409
|
+
-------
|
|
410
|
+
XI : tuple[np.ndarray[np.floating], ...]
|
|
411
|
+
Tuple containing arrays of evaluation points for each isoparametric dimension.
|
|
412
|
+
The tuple has length `NPa` (dimension of isoparametric space).
|
|
413
|
+
|
|
414
|
+
dXI : tuple[np.ndarray[np.floating], ...]
|
|
415
|
+
Tuple containing arrays of integration weights for each isoparametric dimension.
|
|
416
|
+
The tuple has length `NPa` (dimension of isoparametric space).
|
|
417
|
+
|
|
418
|
+
Notes
|
|
419
|
+
-----
|
|
420
|
+
- For a curve (1D), returns ((`xi` points), (`xi` weights))
|
|
421
|
+
- For a surface (2D), returns ((`xi` points, `eta` points), (`xi` weights, `eta` weights))
|
|
422
|
+
- For a volume (3D), returns ((`xi` points, `eta` points, `zeta` points),
|
|
423
|
+
(`xi` weights, `eta` weights, `zeta` weights))
|
|
424
|
+
- The points are centered in their integration intervals
|
|
425
|
+
- The weights represent the size of the integration intervals
|
|
426
|
+
|
|
427
|
+
Examples
|
|
428
|
+
--------
|
|
429
|
+
>>> degrees = [2, 2]
|
|
430
|
+
>>> knots = [np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float'),
|
|
431
|
+
... np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float')]
|
|
432
|
+
>>> spline = BSpline(degrees, knots)
|
|
433
|
+
>>> (xi, eta), (dxi, deta) = spline.linspace_for_integration(n_eval_per_elem=2)
|
|
434
|
+
>>> xi # xi points
|
|
435
|
+
array([0.125, 0.375, 0.625, 0.875])
|
|
436
|
+
>>> dxi # xi weights
|
|
437
|
+
array([0.25, 0.25, 0.25, 0.25])
|
|
438
|
+
"""
|
|
439
|
+
if type(n_eval_per_elem) is int:
|
|
440
|
+
n_eval_per_elem = [n_eval_per_elem] * self.NPa # type: ignore
|
|
441
|
+
if bounding_box is None:
|
|
442
|
+
bounding_box = [b.span for b in self.bases] # type: ignore
|
|
443
|
+
XI = []
|
|
444
|
+
dXI = []
|
|
445
|
+
for basis, (n, bb) in zip(self.bases, zip(n_eval_per_elem, bounding_box)): # type: ignore
|
|
446
|
+
xi, dxi = basis.linspace_for_integration(n, bb)
|
|
447
|
+
XI.append(xi)
|
|
448
|
+
dXI.append(dxi)
|
|
449
|
+
XI = tuple(XI)
|
|
450
|
+
dXI = tuple(dXI)
|
|
451
|
+
return XI, dXI
|
|
452
|
+
|
|
453
|
+
def gauss_legendre_for_integration(
|
|
454
|
+
self,
|
|
455
|
+
n_eval_per_elem: Union[int, Iterable[int], None] = None,
|
|
456
|
+
bounding_box: Union[Iterable, None] = None,
|
|
457
|
+
) -> tuple[
|
|
458
|
+
tuple[np.ndarray[np.floating], ...], tuple[np.ndarray[np.floating], ...]
|
|
459
|
+
]:
|
|
460
|
+
"""
|
|
461
|
+
Generate sets of evaluation points and their Gauss-Legendre integration weights over each basis span.
|
|
462
|
+
|
|
463
|
+
This method creates Gauss-Legendre quadrature points and their corresponding integration weights
|
|
464
|
+
for each isoparametric dimension by calling `gauss_legendre_for_integration` on each
|
|
465
|
+
`BSplineBasis` instance stored in the `bases` array.
|
|
466
|
+
|
|
467
|
+
Parameters
|
|
468
|
+
----------
|
|
469
|
+
n_eval_per_elem : Union[int, Iterable[int], None], optional
|
|
470
|
+
Number of evaluation points per element for each isoparametric dimension.
|
|
471
|
+
If an `int` is provided, the same number is used for all dimensions.
|
|
472
|
+
If an `Iterable` is provided, each value corresponds to a different dimension.
|
|
473
|
+
If `None`, uses `p//2 + 1` points per element where `p` is the degree of each basis.
|
|
474
|
+
This number of points ensures an exact integration of a `p`-th degree polynomial.
|
|
475
|
+
By default, None.
|
|
476
|
+
|
|
477
|
+
bounding_box : Union[Iterable[tuple[float, float]], None], optional
|
|
478
|
+
Lower and upper bounds for each isoparametric dimension.
|
|
479
|
+
If `None`, uses the span of each basis.
|
|
480
|
+
Format: [(`xi_min`, `xi_max`), (`eta_min`, `eta_max`), ...].
|
|
481
|
+
By default, None.
|
|
482
|
+
|
|
483
|
+
Returns
|
|
484
|
+
-------
|
|
485
|
+
XI : tuple[np.ndarray[np.floating], ...]
|
|
486
|
+
Tuple containing arrays of Gauss-Legendre points for each isoparametric dimension.
|
|
487
|
+
The tuple has length `NPa` (dimension of isoparametric space).
|
|
488
|
+
|
|
489
|
+
dXI : tuple[np.ndarray[np.floating], ...]
|
|
490
|
+
Tuple containing arrays of Gauss-Legendre weights for each isoparametric dimension.
|
|
491
|
+
The tuple has length `NPa` (dimension of isoparametric space).
|
|
492
|
+
|
|
493
|
+
Notes
|
|
494
|
+
-----
|
|
495
|
+
- For a curve (1D), returns ((`xi` points), (`xi` weights))
|
|
496
|
+
- For a surface (2D), returns ((`xi` points, `eta` points), (`xi` weights, `eta` weights))
|
|
497
|
+
- For a volume (3D), returns ((`xi` points, `eta` points, `zeta` points),
|
|
498
|
+
(`xi` weights, `eta` weights, `zeta` weights))
|
|
499
|
+
- The points and weights follow the Gauss-Legendre quadrature rule
|
|
500
|
+
- When `n_eval_per_elem` is `None`, uses `p//2 + 1` points per element for exact
|
|
501
|
+
integration of polynomials up to degree `p`
|
|
502
|
+
|
|
503
|
+
Examples
|
|
504
|
+
--------
|
|
505
|
+
>>> degrees = [2, 2]
|
|
506
|
+
>>> knots = [np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float'),
|
|
507
|
+
... np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float')]
|
|
508
|
+
>>> spline = BSpline(degrees, knots)
|
|
509
|
+
>>> (xi, eta), (dxi, deta) = spline.gauss_legendre_for_integration()
|
|
510
|
+
>>> xi # 2 xi points per element => 4 points in total
|
|
511
|
+
array([0.10566243, 0.39433757, 0.60566243, 0.89433757])
|
|
512
|
+
>>> dxi # xi weights
|
|
513
|
+
array([0.25, 0.25, 0.25, 0.25])
|
|
514
|
+
"""
|
|
515
|
+
if n_eval_per_elem is None:
|
|
516
|
+
n_eval_per_elem = self.getDegrees() // 2 + 1
|
|
517
|
+
if type(n_eval_per_elem) is int:
|
|
518
|
+
n_eval_per_elem = [n_eval_per_elem] * self.NPa # type: ignore
|
|
519
|
+
if bounding_box is None:
|
|
520
|
+
bounding_box = [None] * self.NPa # type: ignore
|
|
521
|
+
XI = []
|
|
522
|
+
dXI = []
|
|
523
|
+
for basis, (n_eval_per_elem_axis, bb) in zip(self.bases, zip(n_eval_per_elem, bounding_box)): # type: ignore
|
|
524
|
+
xi, dxi = basis.gauss_legendre_for_integration(n_eval_per_elem_axis, bb)
|
|
525
|
+
XI.append(xi)
|
|
526
|
+
dXI.append(dxi)
|
|
527
|
+
XI = tuple(XI)
|
|
528
|
+
dXI = tuple(dXI)
|
|
529
|
+
return XI, dXI
|
|
530
|
+
|
|
531
|
+
def normalize_knots(self):
|
|
532
|
+
"""
|
|
533
|
+
Maps all knot vectors to the interval [0, 1] in each isoparametric dimension.
|
|
534
|
+
|
|
535
|
+
This method normalizes the knot vectors of each `BSplineBasis` instance stored
|
|
536
|
+
in the `bases` array by applying an affine transformation that maps the span
|
|
537
|
+
interval to [0, 1].
|
|
538
|
+
|
|
539
|
+
Notes
|
|
540
|
+
-----
|
|
541
|
+
- The transformation preserves the relative spacing between knots
|
|
542
|
+
- The transformation preserves the multiplicity of knots
|
|
543
|
+
- The transformation is applied independently to each isoparametric dimension
|
|
544
|
+
- This operation modifies the knot vectors in place
|
|
545
|
+
|
|
546
|
+
Examples
|
|
547
|
+
--------
|
|
548
|
+
>>> degrees = [2, 2]
|
|
549
|
+
>>> knots = [np.array([-1, -1, -1, 0, 1, 1, 1], dtype='float'),
|
|
550
|
+
... np.array([0, 0, 0, 2, 4, 4, 4], dtype='float')]
|
|
551
|
+
>>> spline = BSpline(degrees, knots)
|
|
552
|
+
>>> spline.getKnots()
|
|
553
|
+
[array([-1., -1., -1., 0., 1., 1., 1.]),
|
|
554
|
+
array([0., 0., 0., 2., 4., 4., 4.])]
|
|
555
|
+
>>> spline.normalize_knots()
|
|
556
|
+
>>> spline.getKnots()
|
|
557
|
+
[array([0., 0., 0., 0.5, 1., 1., 1.]),
|
|
558
|
+
array([0., 0., 0., 0.5, 1., 1., 1.])]
|
|
559
|
+
"""
|
|
560
|
+
for basis in self.bases:
|
|
561
|
+
basis.normalize_knots()
|
|
562
|
+
|
|
563
|
+
def DN(
|
|
564
|
+
self,
|
|
565
|
+
XI: Union[np.ndarray[np.floating], tuple[np.ndarray[np.floating], ...]],
|
|
566
|
+
k: Union[int, Iterable[int]] = 0,
|
|
567
|
+
) -> Union[sps.spmatrix, np.ndarray[sps.spmatrix]]:
|
|
568
|
+
"""
|
|
569
|
+
Compute the `k`-th derivative of the B-spline basis at given points in the isoparametric space.
|
|
570
|
+
|
|
571
|
+
This method evaluates the basis functions or their derivatives at specified points, returning
|
|
572
|
+
a matrix that can be used to evaluate the B-spline through a dot product with the control points.
|
|
573
|
+
|
|
574
|
+
Parameters
|
|
575
|
+
----------
|
|
576
|
+
XI : Union[np.ndarray[np.floating], tuple[np.ndarray[np.floating], ...]]
|
|
577
|
+
Points in the isoparametric space where to evaluate the basis functions.
|
|
578
|
+
Two input formats are accepted:
|
|
579
|
+
1. `numpy.ndarray`: Array of coordinates with shape (`NPa`, n_points).
|
|
580
|
+
Each column represents one evaluation point [`xi`, `eta`, ...].
|
|
581
|
+
The resulting matrices will have shape (n_points, number of functions).
|
|
582
|
+
2. `tuple`: Contains `NPa` arrays of coordinates (`xi`, `eta`, ...).
|
|
583
|
+
The resulting matrices will have (n_xi × n_eta × ...) rows.
|
|
584
|
+
|
|
585
|
+
k : Union[int, Iterable[int]], optional
|
|
586
|
+
Derivative orders to compute. Two formats are accepted:
|
|
587
|
+
1. `int`: Same derivative order along all axes. Common values:
|
|
588
|
+
- `k=0`: Evaluate basis functions (default)
|
|
589
|
+
- `k=1`: Compute first derivatives (gradient)
|
|
590
|
+
- `k=2`: Compute second derivatives (hessian)
|
|
591
|
+
2. `list[int]`: Different derivative orders for each axis.
|
|
592
|
+
Example: `[1, 0]` computes first derivative w.r.t `xi`, no derivative w.r.t `eta`.
|
|
593
|
+
By default, 0.
|
|
594
|
+
|
|
595
|
+
Returns
|
|
596
|
+
-------
|
|
597
|
+
DN : Union[sps.spmatrix, np.ndarray[sps.spmatrix]]
|
|
598
|
+
Sparse matrix or array of sparse matrices containing the basis evaluations:
|
|
599
|
+
- If `k` is a `list` or is 0: Returns a single sparse matrix containing the mixed
|
|
600
|
+
derivative specified by the list.
|
|
601
|
+
- If `k` is an `int` > 0: Returns an array of sparse matrices with shape [`NPa`]*`k`.
|
|
602
|
+
For example, if `k=1`, returns `NPa` matrices containing derivatives along each axis.
|
|
603
|
+
|
|
604
|
+
Notes
|
|
605
|
+
-----
|
|
606
|
+
- For evaluating the B-spline with control points in `NPh`-D space:
|
|
607
|
+
`values = DN @ ctrl_pts.reshape((NPh, -1)).T`
|
|
608
|
+
- When using tuple input format for `XI`, points are evaluated at all combinations of coordinates
|
|
609
|
+
- When using array input format for `XI`, each column represents one evaluation point
|
|
610
|
+
- The gradient (`k=1`) returns `NPa` matrices for derivatives along each axis
|
|
611
|
+
- Mixed derivatives can be computed using a list of derivative orders
|
|
612
|
+
|
|
613
|
+
Examples
|
|
614
|
+
--------
|
|
615
|
+
Create a 2D quadratic B-spline:
|
|
616
|
+
>>> degrees = [2, 2]
|
|
617
|
+
>>> knots = [np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float'),
|
|
618
|
+
... np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float')]
|
|
619
|
+
>>> spline = BSpline(degrees, knots)
|
|
620
|
+
|
|
621
|
+
Evaluate basis functions at specific points using array input:
|
|
622
|
+
>>> XI = np.array([[0, 0.5, 1], # xi coordinates
|
|
623
|
+
... [0, 0.5, 1]]) # eta coordinates
|
|
624
|
+
>>> N = spline.DN(XI, k=0)
|
|
625
|
+
>>> N.A # Convert sparse matrix to dense for display
|
|
626
|
+
array([[1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
|
|
627
|
+
[0., 0., 0., 0., 0., 0.25, 0.25, 0., 0., 0.25, 0.25, 0., 0., 0., 0., 0.],
|
|
628
|
+
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1.]])
|
|
629
|
+
|
|
630
|
+
Compute first derivatives using tuple input:
|
|
631
|
+
>>> xi = np.array([0, 0.5])
|
|
632
|
+
>>> eta = np.array([0, 1])
|
|
633
|
+
>>> dN = spline.DN((xi, eta), k=1) # Returns [NPa] matrices
|
|
634
|
+
>>> len(dN) # Number of derivative matrices
|
|
635
|
+
2
|
|
636
|
+
>>> dN[0].A # Derivative w.r.t xi
|
|
637
|
+
array([[-4., 0., 0., 0., 4., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
|
|
638
|
+
[ 0., 0., 0.,-4., 0., 0., 0., 4., 0., 0., 0., 0., 0., 0., 0., 0.],
|
|
639
|
+
[ 0., 0., 0., 0.,-2., 0., 0., 0., 2., 0., 0., 0., 0., 0., 0., 0.],
|
|
640
|
+
[ 0., 0., 0., 0., 0., 0., 0.,-2., 0., 0., 0., 2., 0., 0., 0., 0.]])
|
|
641
|
+
>>> dN[1].A # Derivative w.r.t eta
|
|
642
|
+
array([[-4., 4., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
|
|
643
|
+
[ 0., 0.,-4., 4., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
|
|
644
|
+
[ 0., 0., 0., 0.,-2., 2., 0., 0.,-2., 2., 0., 0., 0., 0., 0., 0.],
|
|
645
|
+
[ 0., 0., 0., 0., 0., 0.,-2., 2., 0., 0.,-2., 2., 0., 0., 0., 0.]])
|
|
646
|
+
|
|
647
|
+
Compute mixed derivatives:
|
|
648
|
+
>>> d2N = spline.DN((xi, eta), k=[1, 1]) # Second derivative: d²/dxi·deta
|
|
649
|
+
>>> d2N.A
|
|
650
|
+
array([[16.,-16., 0., 0.,-16.,16., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
|
|
651
|
+
[ 0., 0., 16.,-16., 0., 0.,-16.,16., 0., 0., 0., 0., 0., 0., 0., 0.],
|
|
652
|
+
[ 0., 0., 0., 0., 8.,-8., 0., 0.,-8., 8., 0., 0., 0., 0., 0., 0.],
|
|
653
|
+
[ 0., 0., 0., 0., 0., 0., 8.,-8., 0.,-0.,-8., 8., 0., 0., 0., 0.]])
|
|
654
|
+
"""
|
|
655
|
+
|
|
656
|
+
if isinstance(XI, np.ndarray):
|
|
657
|
+
fct = my_wide_product
|
|
658
|
+
XI = XI.reshape((self.NPa, -1))
|
|
659
|
+
else:
|
|
660
|
+
fct = sps.kron
|
|
661
|
+
|
|
662
|
+
if isinstance(k, int):
|
|
663
|
+
if k == 0:
|
|
664
|
+
k = [0] * self.NPa # type: ignore
|
|
665
|
+
|
|
666
|
+
if isinstance(k, int):
|
|
667
|
+
dkbasis_dxik = np.empty((self.NPa, k + 1), dtype="object")
|
|
668
|
+
for idx in range(self.NPa):
|
|
669
|
+
basis = self.bases[idx]
|
|
670
|
+
xi = XI[idx] - np.finfo("float").eps * (XI[idx] == basis.knot[-1])
|
|
671
|
+
for k_querry in range(k + 1):
|
|
672
|
+
dkbasis_dxik[idx, k_querry] = basis.N(xi, k=k_querry)
|
|
673
|
+
DN = np.empty([self.NPa] * k, dtype="object")
|
|
674
|
+
dic = {}
|
|
675
|
+
for axes in np.ndindex(*DN.shape):
|
|
676
|
+
u, c = np.unique(axes, return_counts=True)
|
|
677
|
+
k_arr = np.zeros(self.NPa, dtype="int")
|
|
678
|
+
k_arr[u] = c
|
|
679
|
+
key = tuple(k_arr)
|
|
680
|
+
if key not in dic:
|
|
681
|
+
for idx in range(self.NPa):
|
|
682
|
+
k_querry = k_arr[idx]
|
|
683
|
+
if idx == 0:
|
|
684
|
+
dic[key] = dkbasis_dxik[idx, k_querry]
|
|
685
|
+
else:
|
|
686
|
+
dic[key] = fct(dic[key], dkbasis_dxik[idx, k_querry])
|
|
687
|
+
DN[axes] = dic[key]
|
|
688
|
+
return DN
|
|
689
|
+
else:
|
|
690
|
+
for idx in range(self.NPa):
|
|
691
|
+
basis = self.bases[idx]
|
|
692
|
+
k_idx = k[idx]
|
|
693
|
+
xi = XI[idx] - np.finfo("float").eps * (XI[idx] == basis.knot[-1])
|
|
694
|
+
DN_elem = basis.N(xi, k=k_idx)
|
|
695
|
+
if idx == 0:
|
|
696
|
+
DN = DN_elem
|
|
697
|
+
else:
|
|
698
|
+
DN = fct(DN, DN_elem)
|
|
699
|
+
return DN
|
|
700
|
+
|
|
701
|
+
def __call__(
|
|
702
|
+
self,
|
|
703
|
+
ctrl_pts: np.ndarray[np.floating],
|
|
704
|
+
XI: Union[np.ndarray[np.floating], tuple[np.ndarray[np.floating], ...]],
|
|
705
|
+
k: Union[int, Iterable[int]] = 0,
|
|
706
|
+
) -> np.ndarray[np.floating]:
|
|
707
|
+
"""
|
|
708
|
+
Evaluate the `k`-th derivative of the B-spline at given points in the isoparametric space.
|
|
709
|
+
|
|
710
|
+
This method evaluates the B-spline or its derivatives at specified points by computing
|
|
711
|
+
the basis functions and performing a dot product with the control points.
|
|
712
|
+
|
|
713
|
+
Parameters
|
|
714
|
+
----------
|
|
715
|
+
ctrl_pts : np.ndarray[np.floating]
|
|
716
|
+
Control points defining the B-spline geometry.
|
|
717
|
+
Shape: (`NPh`, n1, n2, ...) where:
|
|
718
|
+
- `NPh` is the dimension of the physical space
|
|
719
|
+
- ni is the number of control points in the i-th isoparametric dimension,
|
|
720
|
+
i.e. the number of basis functions on this isoparametric axis
|
|
721
|
+
|
|
722
|
+
XI : Union[np.ndarray[np.floating], tuple[np.ndarray[np.floating], ...]]
|
|
723
|
+
Points in the isoparametric space where to evaluate the B-spline.
|
|
724
|
+
Two input formats are accepted:
|
|
725
|
+
1. `numpy.ndarray`: Array of coordinates with shape (`NPa`, n_points).
|
|
726
|
+
Each column represents one evaluation point [`xi`, `eta`, ...].
|
|
727
|
+
2. `tuple`: Contains `NPa` arrays of coordinates (`xi`, `eta`, ...).
|
|
728
|
+
|
|
729
|
+
k : Union[int, Iterable[int]], optional
|
|
730
|
+
Derivative orders to compute. Two formats are accepted:
|
|
731
|
+
1. `int`: Same derivative order along all axes. Common values:
|
|
732
|
+
- `k=0`: Evaluate the B-spline mapping (default)
|
|
733
|
+
- `k=1`: Compute first derivatives (gradient)
|
|
734
|
+
- `k=2`: Compute second derivatives (hessian)
|
|
735
|
+
2. `list[int]`: Different derivative orders for each axis.
|
|
736
|
+
Example: `[1, 0]` computes first derivative w.r.t `xi`, no derivative w.r.t `eta`.
|
|
737
|
+
By default, 0.
|
|
738
|
+
|
|
739
|
+
Returns
|
|
740
|
+
-------
|
|
741
|
+
values : np.ndarray[np.floating]
|
|
742
|
+
B-spline evaluation at the specified points.
|
|
743
|
+
Shape depends on input format and derivative order:
|
|
744
|
+
- For array input: (`NPh`, shape of derivative, n_points)
|
|
745
|
+
- For tuple input: (`NPh`, shape of derivative, n_xi, n_eta, ...)
|
|
746
|
+
Where "shape of derivative" depends on `k`:
|
|
747
|
+
- For `k=0` or `k` as list: Empty shape
|
|
748
|
+
- For `k=1`: Shape is (`NPa`,) for gradient
|
|
749
|
+
- For `k>1` as int: Shape is (`NPa`,) repeated `k` times
|
|
750
|
+
|
|
751
|
+
Notes
|
|
752
|
+
-----
|
|
753
|
+
- The method first computes basis functions using `DN` then performs a dot product
|
|
754
|
+
with control points
|
|
755
|
+
- When using tuple input format, points are evaluated at all combinations of coordinates
|
|
756
|
+
- When using array input format, each column represents one evaluation point
|
|
757
|
+
- The gradient (`k=1`) returns derivatives along each isoparametric axis
|
|
758
|
+
- Mixed derivatives can be computed using a list of derivative orders
|
|
759
|
+
|
|
760
|
+
Examples
|
|
761
|
+
--------
|
|
762
|
+
Create a 2D quadratic B-spline:
|
|
763
|
+
>>> degrees = [2, 2]
|
|
764
|
+
>>> knots = [np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float'),
|
|
765
|
+
... np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float')]
|
|
766
|
+
>>> spline = BSpline(degrees, knots)
|
|
767
|
+
>>> ctrl_pts = np.random.rand(3, 4, 4) # 3D control points
|
|
768
|
+
|
|
769
|
+
Evaluate B-spline at specific points using array input:
|
|
770
|
+
>>> XI = np.array([[0, 0.5, 1], # xi coordinates
|
|
771
|
+
... [0, 0.5, 1]]) # eta coordinates
|
|
772
|
+
>>> values = spline(ctrl_pts, XI, k=0)
|
|
773
|
+
>>> values.shape
|
|
774
|
+
(3, 3) # (NPh, n_points)
|
|
775
|
+
|
|
776
|
+
Evaluate gradient using tuple input:
|
|
777
|
+
>>> xi = np.array([0, 0.5])
|
|
778
|
+
>>> eta = np.array([0, 1])
|
|
779
|
+
>>> derivatives = spline(ctrl_pts, (xi, eta), k=1)
|
|
780
|
+
>>> derivatives.shape
|
|
781
|
+
(2, 3, 2, 2) # (NPa, NPh, n_xi, n_eta)
|
|
782
|
+
|
|
783
|
+
Compute mixed derivatives:
|
|
784
|
+
>>> mixed = spline(ctrl_pts, (xi, eta), k=[1, 1])
|
|
785
|
+
>>> mixed.shape
|
|
786
|
+
(3, 2, 2) # (NPh, n_xi, n_eta)
|
|
787
|
+
"""
|
|
788
|
+
if isinstance(XI, np.ndarray):
|
|
789
|
+
XI_shape = XI.shape[1:]
|
|
790
|
+
else:
|
|
791
|
+
XI_shape = [xi.size for xi in XI]
|
|
792
|
+
DN = self.DN(XI, k)
|
|
793
|
+
NPh = ctrl_pts.shape[0]
|
|
794
|
+
if isinstance(DN, np.ndarray):
|
|
795
|
+
values = np.empty((*DN.shape, NPh, *XI_shape), dtype="float")
|
|
796
|
+
for axes in np.ndindex(*DN.shape):
|
|
797
|
+
values[axes] = (DN[axes] @ ctrl_pts.reshape((NPh, -1)).T).T.reshape(
|
|
798
|
+
(NPh, *XI_shape)
|
|
799
|
+
)
|
|
800
|
+
else:
|
|
801
|
+
values = (DN @ ctrl_pts.reshape((NPh, -1)).T).T.reshape((NPh, *XI_shape))
|
|
802
|
+
return values
|
|
803
|
+
|
|
804
|
+
def knotInsertion(
|
|
805
|
+
self,
|
|
806
|
+
ctrl_pts: Union[np.ndarray[np.floating], None],
|
|
807
|
+
knots_to_add: Iterable[Union[np.ndarray[np.float64], int]],
|
|
808
|
+
) -> np.ndarray[np.floating]:
|
|
809
|
+
"""
|
|
810
|
+
Add knots to the B-spline while preserving its geometry.
|
|
811
|
+
|
|
812
|
+
This method performs knot insertion by adding new knots to each isoparametric dimension
|
|
813
|
+
and computing the new control points to maintain the exact same geometry. The method
|
|
814
|
+
modifies the `BSpline` object by updating its basis functions with the new knots.
|
|
815
|
+
|
|
816
|
+
Parameters
|
|
817
|
+
----------
|
|
818
|
+
ctrl_pts : np.ndarray[np.floating]
|
|
819
|
+
Control points defining the B-spline geometry.
|
|
820
|
+
Shape: (`NPh`, n1, n2, ...) where:
|
|
821
|
+
- `NPh` is the dimension of the physical space
|
|
822
|
+
- ni is the number of control points in the i-th isoparametric dimension
|
|
823
|
+
If None is passed, the knot insertion is performed on the basis functions
|
|
824
|
+
but not on the control points.
|
|
825
|
+
|
|
826
|
+
knots_to_add : Iterable[Union[np.ndarray[np.floating], int]]
|
|
827
|
+
Refinement specification for each isoparametric dimension.
|
|
828
|
+
For each dimension, two formats are accepted:
|
|
829
|
+
1. `numpy.ndarray`: Array of knots to insert. These knots must lie within
|
|
830
|
+
the span of the existing knot vector.
|
|
831
|
+
2. `int`: Number of equally spaced knots to insert in each element.
|
|
832
|
+
|
|
833
|
+
Returns
|
|
834
|
+
-------
|
|
835
|
+
new_ctrl_pts : np.ndarray[np.floating]
|
|
836
|
+
New control points after knot insertion.
|
|
837
|
+
Shape: (`NPh`, m1, m2, ...) where mi ≥ ni is the new number of
|
|
838
|
+
control points in the i-th isoparametric dimension.
|
|
839
|
+
|
|
840
|
+
Notes
|
|
841
|
+
-----
|
|
842
|
+
- Knot insertion preserves the geometry and parameterization of the B-spline
|
|
843
|
+
- The number of new control points depends on the number and multiplicity of inserted knots
|
|
844
|
+
- When using integer input, knots are inserted with uniform spacing in each element
|
|
845
|
+
- The method modifies the basis functions but maintains `C^{p-m}` continuity,
|
|
846
|
+
where `p` is the degree and `m` is the multiplicity of the inserted knot
|
|
847
|
+
|
|
848
|
+
Examples
|
|
849
|
+
--------
|
|
850
|
+
Create a 2D quadratic B-spline and insert knots:
|
|
851
|
+
>>> degrees = [2, 2]
|
|
852
|
+
>>> knots = [np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float'),
|
|
853
|
+
... np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float')]
|
|
854
|
+
>>> spline = BSpline(degrees, knots)
|
|
855
|
+
>>> ctrl_pts = np.random.rand(3, 4, 4) # 3D control points
|
|
856
|
+
|
|
857
|
+
Insert specific knots in first dimension only:
|
|
858
|
+
>>> knots_to_add = [np.array([0.25, 0.75], dtype='float'),
|
|
859
|
+
... np.array([], dtype='float')]
|
|
860
|
+
>>> new_ctrl_pts = spline.knotInsertion(ctrl_pts, knots_to_add)
|
|
861
|
+
>>> new_ctrl_pts.shape
|
|
862
|
+
(3, 6, 4) # Two new control points added in first dimension
|
|
863
|
+
>>> spline.getKnots()[0] # The knot vector is modified
|
|
864
|
+
array([0. , 0. , 0. , 0.25, 0.5 , 0.75, 1. , 1. , 1. ])
|
|
865
|
+
|
|
866
|
+
Insert two knots per element in both dimensions:
|
|
867
|
+
>>> new_ctrl_pts = spline.knotInsertion(new_ctrl_pts, [1, 1])
|
|
868
|
+
>>> new_ctrl_pts.shape
|
|
869
|
+
(3, 10, 6) # Uniform refinement in both dimensions
|
|
870
|
+
>>> spline.getKnots()[0] # The knot vectors are further modified
|
|
871
|
+
array([0. , 0. , 0. , 0.125, 0.25 , 0.375, 0.5 , 0.625, 0.75 ,
|
|
872
|
+
0.875, 1. , 1. , 1. ])
|
|
873
|
+
"""
|
|
874
|
+
true_knots_to_add = []
|
|
875
|
+
for axis, knots_to_add_elem in enumerate(knots_to_add):
|
|
876
|
+
if isinstance(
|
|
877
|
+
knots_to_add_elem, int
|
|
878
|
+
): # It is a number of knots to add in each element
|
|
879
|
+
u_knot = np.unique(self.bases[axis].knot)
|
|
880
|
+
a, b = u_knot[:-1, None], u_knot[1:, None]
|
|
881
|
+
mu = np.linspace(0, 1, knots_to_add_elem + 1, endpoint=False)[None, 1:]
|
|
882
|
+
true_knots_to_add.append((a + (b - a) * mu).ravel())
|
|
883
|
+
else:
|
|
884
|
+
true_knots_to_add.append(knots_to_add_elem)
|
|
885
|
+
knots_to_add = true_knots_to_add
|
|
886
|
+
|
|
887
|
+
if ctrl_pts is None:
|
|
888
|
+
for basis, knots_to_add_elem in zip(self.bases, knots_to_add):
|
|
889
|
+
basis.knotInsertion(knots_to_add_elem)
|
|
890
|
+
return None
|
|
891
|
+
|
|
892
|
+
pts_shape = np.empty(self.NPa, dtype="int")
|
|
893
|
+
D = None
|
|
894
|
+
for idx in range(self.NPa):
|
|
895
|
+
basis = self.bases[idx]
|
|
896
|
+
knots_to_add_elem = knots_to_add[idx]
|
|
897
|
+
D_elem = basis.knotInsertion(knots_to_add_elem)
|
|
898
|
+
pts_shape[idx] = D_elem.shape[0]
|
|
899
|
+
if D is None:
|
|
900
|
+
D = D_elem
|
|
901
|
+
else:
|
|
902
|
+
D = sps.kron(D, D_elem)
|
|
903
|
+
NPh = ctrl_pts.shape[0]
|
|
904
|
+
pts = (D @ ctrl_pts.reshape((NPh, -1)).T).T
|
|
905
|
+
ctrl_pts = pts.reshape((NPh, *pts_shape))
|
|
906
|
+
return ctrl_pts
|
|
907
|
+
|
|
908
|
+
def orderElevation(
|
|
909
|
+
self, ctrl_pts: Union[np.ndarray[np.floating], None], t: Iterable[int]
|
|
910
|
+
) -> np.ndarray[np.floating]:
|
|
911
|
+
"""
|
|
912
|
+
Elevate the polynomial degree of the B-spline while preserving its geometry.
|
|
913
|
+
|
|
914
|
+
This method performs order elevation by increasing the polynomial degree of each
|
|
915
|
+
isoparametric dimension and computing the new control points to maintain the exact
|
|
916
|
+
same geometry. The method modifies the `BSpline` object by updating its basis
|
|
917
|
+
functions with the new degrees.
|
|
918
|
+
|
|
919
|
+
Parameters
|
|
920
|
+
----------
|
|
921
|
+
ctrl_pts : Union[np.ndarray[np.floating], None]
|
|
922
|
+
Control points defining the B-spline geometry.
|
|
923
|
+
Shape: (`NPh`, n1, n2, ...) where:
|
|
924
|
+
- `NPh` is the dimension of the physical space
|
|
925
|
+
- ni is the number of control points in the i-th isoparametric dimension
|
|
926
|
+
If None is passed, the order elevation is performed on the basis functions
|
|
927
|
+
but not on the control points.
|
|
928
|
+
|
|
929
|
+
t : Iterable[int]
|
|
930
|
+
Degree elevation for each isoparametric dimension.
|
|
931
|
+
For each dimension i, the new degree will be `p_i + t_i` where `p_i`
|
|
932
|
+
is the current degree.
|
|
933
|
+
|
|
934
|
+
Returns
|
|
935
|
+
-------
|
|
936
|
+
new_ctrl_pts : np.ndarray[np.floating]
|
|
937
|
+
New control points after order elevation.
|
|
938
|
+
Shape: (`NPh`, m1, m2, ...) where mi ≥ ni is the new number of
|
|
939
|
+
control points in the i-th isoparametric dimension.
|
|
940
|
+
|
|
941
|
+
Notes
|
|
942
|
+
-----
|
|
943
|
+
- Order elevation preserves the geometry and parameterization of the B-spline
|
|
944
|
+
- The number of new control points depends on the current degree and number of
|
|
945
|
+
elements
|
|
946
|
+
- The method modifies the `BSpline` object by updating its basis functions
|
|
947
|
+
- This operation is more computationally expensive than knot insertion
|
|
948
|
+
|
|
949
|
+
Examples
|
|
950
|
+
--------
|
|
951
|
+
Create a 2D quadratic B-spline and elevate its order:
|
|
952
|
+
>>> degrees = [2, 2]
|
|
953
|
+
>>> knots = [np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float'),
|
|
954
|
+
... np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float')]
|
|
955
|
+
>>> spline = BSpline(degrees, knots)
|
|
956
|
+
>>> ctrl_pts = np.random.rand(3, 4, 4) # 3D control points
|
|
957
|
+
|
|
958
|
+
Elevate order by 1 in first dimension only:
|
|
959
|
+
>>> t = [1, 0] # Increase degree by 1 in first dimension
|
|
960
|
+
>>> new_ctrl_pts = spline.orderElevation(ctrl_pts, t)
|
|
961
|
+
>>> new_ctrl_pts.shape
|
|
962
|
+
(3, 6, 4) # Two new control points added in first dimension (one per element)
|
|
963
|
+
>>> spline.getDegrees() # The degrees are modified
|
|
964
|
+
array([3, 2])
|
|
965
|
+
"""
|
|
966
|
+
if ctrl_pts is None:
|
|
967
|
+
for basis, t_basis in zip(self.bases, t):
|
|
968
|
+
basis.orderElevation(t_basis)
|
|
969
|
+
return None
|
|
970
|
+
|
|
971
|
+
pts_shape = np.empty(self.NPa, dtype="int")
|
|
972
|
+
STD = None
|
|
973
|
+
for idx in range(self.NPa):
|
|
974
|
+
basis = self.bases[idx]
|
|
975
|
+
t_basis = t[idx]
|
|
976
|
+
STD_elem = basis.orderElevation(t_basis)
|
|
977
|
+
pts_shape[idx] = STD_elem.shape[0]
|
|
978
|
+
if STD is None:
|
|
979
|
+
STD = STD_elem
|
|
980
|
+
else:
|
|
981
|
+
STD = sps.kron(STD, STD_elem)
|
|
982
|
+
NPh = ctrl_pts.shape[0]
|
|
983
|
+
pts = (STD @ ctrl_pts.reshape((NPh, -1)).T).T
|
|
984
|
+
ctrl_pts = pts.reshape((NPh, *pts_shape))
|
|
985
|
+
return ctrl_pts
|
|
986
|
+
|
|
987
|
+
def greville_abscissa(self, return_weights: bool = False) -> Union[
|
|
988
|
+
list[np.ndarray[np.floating]],
|
|
989
|
+
tuple[list[np.ndarray[np.floating]], list[np.ndarray[np.floating]]],
|
|
990
|
+
]:
|
|
991
|
+
"""
|
|
992
|
+
Compute the Greville abscissa and optionally their weights for each isoparametric dimension.
|
|
993
|
+
|
|
994
|
+
The Greville abscissa can be interpreted as the "position" of the control points in the
|
|
995
|
+
isoparametric space. They are often used as interpolation points for B-splines.
|
|
996
|
+
|
|
997
|
+
Parameters
|
|
998
|
+
----------
|
|
999
|
+
return_weights : bool, optional
|
|
1000
|
+
If `True`, also returns the weights (span lengths) of each basis function.
|
|
1001
|
+
By default, False.
|
|
1002
|
+
|
|
1003
|
+
Returns
|
|
1004
|
+
-------
|
|
1005
|
+
greville : list[np.ndarray[np.floating]]
|
|
1006
|
+
List containing the Greville abscissa for each isoparametric dimension.
|
|
1007
|
+
The list has length `NPa`, where each element is an array of size `n + 1`,
|
|
1008
|
+
`n` being the last index of the basis functions in that dimension.
|
|
1009
|
+
|
|
1010
|
+
weights : list[np.ndarray[np.floating]], optional
|
|
1011
|
+
Only returned if `return_weights` is `True`.
|
|
1012
|
+
List containing the weights for each isoparametric dimension.
|
|
1013
|
+
The list has length `NPa`, where each element is an array containing
|
|
1014
|
+
the span length of each basis function.
|
|
1015
|
+
|
|
1016
|
+
Notes
|
|
1017
|
+
-----
|
|
1018
|
+
- For a curve (1D), returns [`xi` abscissa]
|
|
1019
|
+
- For a surface (2D), returns [`xi` abscissa, `eta` abscissa]
|
|
1020
|
+
- For a volume (3D), returns [`xi` abscissa, `eta` abscissa, `zeta` abscissa]
|
|
1021
|
+
- The Greville abscissa are computed as averages of `p` consecutive knots
|
|
1022
|
+
- The weights represent the size of the support of each basis function
|
|
1023
|
+
- The number of abscissa in each dimension equals the number of control points
|
|
1024
|
+
|
|
1025
|
+
Examples
|
|
1026
|
+
--------
|
|
1027
|
+
Compute Greville abscissa for a 2D B-spline:
|
|
1028
|
+
>>> degrees = [2, 2]
|
|
1029
|
+
>>> knots = [np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float'),
|
|
1030
|
+
... np.array([0, 0, 0, 1, 1, 1], dtype='float')]
|
|
1031
|
+
>>> spline = BSpline(degrees, knots)
|
|
1032
|
+
>>> greville = spline.greville_abscissa()
|
|
1033
|
+
>>> greville[0] # xi coordinates
|
|
1034
|
+
array([0. , 0.25, 0.75, 1. ])
|
|
1035
|
+
>>> greville[1] # eta coordinates
|
|
1036
|
+
array([0. , 0.5, 1. ])
|
|
1037
|
+
|
|
1038
|
+
Compute both abscissa and weights:
|
|
1039
|
+
>>> greville, weights = spline.greville_abscissa(return_weights=True)
|
|
1040
|
+
>>> weights[0] # weights for xi direction
|
|
1041
|
+
array([0.5, 1. , 1. , 0.5])
|
|
1042
|
+
"""
|
|
1043
|
+
res = [
|
|
1044
|
+
basis.greville_abscissa(return_weights=return_weights)
|
|
1045
|
+
for basis in self.bases
|
|
1046
|
+
]
|
|
1047
|
+
if return_weights:
|
|
1048
|
+
greville, weights = zip(*res)
|
|
1049
|
+
greville = list(greville)
|
|
1050
|
+
weights = list(weights)
|
|
1051
|
+
return greville, weights
|
|
1052
|
+
else:
|
|
1053
|
+
greville = list(res)
|
|
1054
|
+
return greville
|
|
1055
|
+
|
|
1056
|
+
def make_control_poly_meshes(
|
|
1057
|
+
self,
|
|
1058
|
+
ctrl_pts: np.ndarray[np.floating],
|
|
1059
|
+
n_eval_per_elem: Union[int, Iterable[int]] = 10,
|
|
1060
|
+
n_step: int = 1,
|
|
1061
|
+
fields: dict = {},
|
|
1062
|
+
XI: Union[None, tuple[np.ndarray[np.floating], ...]] = None,
|
|
1063
|
+
paraview_sizes: dict = {},
|
|
1064
|
+
) -> list[io.Mesh]:
|
|
1065
|
+
"""
|
|
1066
|
+
Create meshes containing all the data needed to plot the control polygon of the B-spline.
|
|
1067
|
+
|
|
1068
|
+
This method generates a list of `io.Mesh` objects representing the control mesh
|
|
1069
|
+
(polygonal connectivity) of the B-spline, suitable for visualization (e.g. in Paraview).
|
|
1070
|
+
It supports time-dependent fields and arbitrary dimension.
|
|
1071
|
+
|
|
1072
|
+
Parameters
|
|
1073
|
+
----------
|
|
1074
|
+
ctrl_pts : np.ndarray[np.floating]
|
|
1075
|
+
Array of control points of the B-spline, with shape
|
|
1076
|
+
(`NPh`, number of elements for dim 1, ..., number of elements for dim `NPa`),
|
|
1077
|
+
where `NPh` is the physical space dimension and `NPa` is the dimension of the
|
|
1078
|
+
isoparametric space.
|
|
1079
|
+
n_step : int, optional
|
|
1080
|
+
Number of time steps to plot. By default, 1.
|
|
1081
|
+
n_eval_per_elem : Union[int, Iterable[int]], optional
|
|
1082
|
+
Number of evaluation points per element for each isoparametric dimension.
|
|
1083
|
+
By default, 10.
|
|
1084
|
+
- If an `int` is provided, the same number is used for all dimensions.
|
|
1085
|
+
- If an `Iterable` is provided, each value corresponds to a different dimension.
|
|
1086
|
+
n_step : int, optional
|
|
1087
|
+
Number of time steps to plot. By default, 1.
|
|
1088
|
+
fields : dict, optional
|
|
1089
|
+
Dictionary of fields to plot at each time step. Keys are field names. Values can be:
|
|
1090
|
+
- a `function` taking (`BSpline` spline, `tuple` of `np.ndarray[np.floating]` XI) and
|
|
1091
|
+
returning a `np.ndarray[np.floating]` of shape (`n_step`, number of combinations of XI, field size),
|
|
1092
|
+
- a `np.ndarray[np.floating]` defined **on the control points**, of shape (`n_step`, field size, *`ctrl_pts.shape[1:]`),
|
|
1093
|
+
which is then interpolated using the B-spline basis functions,
|
|
1094
|
+
- a `np.ndarray[np.floating]` defined **on the evaluation grid**, of shape (`n_step`, field size, *grid shape),
|
|
1095
|
+
where `grid shape` matches the discretization provided by XI or `n_eval_per_elem`.
|
|
1096
|
+
In this case, the field is interpolated in physical space using `scipy.interpolate.griddata`.
|
|
1097
|
+
XI : tuple[np.ndarray[np.floating], ...], optional
|
|
1098
|
+
Parametric coordinates at which to evaluate the B-spline and fields.
|
|
1099
|
+
If not `None`, overrides the `n_eval_per_elem` parameter.
|
|
1100
|
+
If `None`, a regular grid is generated according to `n_eval_per_elem`.
|
|
1101
|
+
paraview_sizes: dict, optionnal
|
|
1102
|
+
The fields present in this `dict` are overrided by `np.NaN`s.
|
|
1103
|
+
The keys must be the fields names and the values must be the fields sizes for paraview.
|
|
1104
|
+
By default, {}.
|
|
1105
|
+
|
|
1106
|
+
Returns
|
|
1107
|
+
-------
|
|
1108
|
+
list[io.Mesh]
|
|
1109
|
+
List of `io.Mesh` objects, one for each time step, containing the control mesh geometry
|
|
1110
|
+
and associated fields.
|
|
1111
|
+
|
|
1112
|
+
Notes
|
|
1113
|
+
-----
|
|
1114
|
+
- The control mesh is constructed by connecting control points along each isoparametric direction.
|
|
1115
|
+
- Fields can be provided either as functions evaluated at the Greville abscissae, or as arrays defined on the
|
|
1116
|
+
control points or on a regular parametric grid (in which case they are interpolated at the Greville abscissae).
|
|
1117
|
+
- The first axis of the field array or function output corresponds to the time step, even if there is only one.
|
|
1118
|
+
- The method is compatible with B-splines of arbitrary dimension.
|
|
1119
|
+
|
|
1120
|
+
Examples
|
|
1121
|
+
--------
|
|
1122
|
+
>>> degrees = [2, 2]
|
|
1123
|
+
>>> knots = [np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float'),
|
|
1124
|
+
... np.array([0, 0, 0, 1, 1, 1], dtype='float')]
|
|
1125
|
+
>>> spline = BSpline(degrees, knots)
|
|
1126
|
+
>>> ctrl_pts = np.random.rand(3, 4, 3) # 3D control points for a 2D surface
|
|
1127
|
+
>>> meshes = spline.make_control_poly_meshes(ctrl_pts)
|
|
1128
|
+
>>> mesh = meshes[0]
|
|
1129
|
+
"""
|
|
1130
|
+
if XI is None:
|
|
1131
|
+
XI = self.linspace(n_eval_per_elem)
|
|
1132
|
+
interp_points = self(ctrl_pts, XI).reshape((3, -1)).T
|
|
1133
|
+
shape = [xi.size for xi in XI]
|
|
1134
|
+
NXI = np.prod(shape)
|
|
1135
|
+
NPh = ctrl_pts.shape[0]
|
|
1136
|
+
lines = np.empty((0, 2), dtype="int")
|
|
1137
|
+
size = np.prod(ctrl_pts.shape[1:])
|
|
1138
|
+
inds = np.arange(size).reshape(ctrl_pts.shape[1:])
|
|
1139
|
+
for idx in range(self.NPa):
|
|
1140
|
+
rng = np.arange(inds.shape[idx])
|
|
1141
|
+
lines = np.append(
|
|
1142
|
+
lines,
|
|
1143
|
+
np.concatenate(
|
|
1144
|
+
(
|
|
1145
|
+
np.expand_dims(np.take(inds, rng[:-1], axis=idx), axis=-1),
|
|
1146
|
+
np.expand_dims(np.take(inds, rng[1:], axis=idx), axis=-1),
|
|
1147
|
+
),
|
|
1148
|
+
axis=-1,
|
|
1149
|
+
).reshape((-1, 2)),
|
|
1150
|
+
axis=0,
|
|
1151
|
+
)
|
|
1152
|
+
cells = {"line": lines}
|
|
1153
|
+
points = np.moveaxis(ctrl_pts, 0, -1).reshape((-1, NPh))
|
|
1154
|
+
greville = tuple(self.greville_abscissa())
|
|
1155
|
+
n = self.getNbFunc()
|
|
1156
|
+
point_data = {}
|
|
1157
|
+
for key, value in fields.items():
|
|
1158
|
+
if key in paraview_sizes:
|
|
1159
|
+
point_data[key] = np.full((n_step, n, paraview_sizes[key]), np.NAN)
|
|
1160
|
+
elif callable(value):
|
|
1161
|
+
point_data[key] = value(self, greville)
|
|
1162
|
+
else:
|
|
1163
|
+
value = np.asarray(value)
|
|
1164
|
+
if value.ndim >= 2 and value.shape[-self.NPa :] == tuple(
|
|
1165
|
+
ctrl_pts.shape[1:]
|
|
1166
|
+
):
|
|
1167
|
+
point_data[key] = value.reshape((n_step, -1, n)).transpose(0, 2, 1)
|
|
1168
|
+
elif value.ndim >= 2 and value.shape[-self.NPa :] == tuple(shape):
|
|
1169
|
+
paraview_size = value.shape[1]
|
|
1170
|
+
interp_field = griddata(
|
|
1171
|
+
interp_points,
|
|
1172
|
+
value.reshape((n_step * paraview_size, NXI)).T,
|
|
1173
|
+
points,
|
|
1174
|
+
method="linear",
|
|
1175
|
+
)
|
|
1176
|
+
point_data[key] = interp_field.reshape(
|
|
1177
|
+
(n, n_step, paraview_size)
|
|
1178
|
+
).transpose(1, 0, 2)
|
|
1179
|
+
else:
|
|
1180
|
+
raise ValueError(f"Field {key} shape {value.shape} not understood.")
|
|
1181
|
+
# make meshes
|
|
1182
|
+
meshes = []
|
|
1183
|
+
for i in range(n_step):
|
|
1184
|
+
point_data_step = {}
|
|
1185
|
+
for key, value in point_data.items():
|
|
1186
|
+
point_data_step[key] = value[i]
|
|
1187
|
+
mesh = io.Mesh(points, cells, point_data_step) # type: ignore
|
|
1188
|
+
meshes.append(mesh)
|
|
1189
|
+
|
|
1190
|
+
return meshes
|
|
1191
|
+
|
|
1192
|
+
def make_elem_separator_meshes(
|
|
1193
|
+
self,
|
|
1194
|
+
ctrl_pts: np.ndarray[np.floating],
|
|
1195
|
+
n_eval_per_elem: Union[int, Iterable[int]] = 10,
|
|
1196
|
+
n_step: int = 1,
|
|
1197
|
+
fields: dict = {},
|
|
1198
|
+
XI: Union[None, tuple[np.ndarray[np.floating], ...]] = None,
|
|
1199
|
+
paraview_sizes: dict = {},
|
|
1200
|
+
) -> list[io.Mesh]:
|
|
1201
|
+
"""
|
|
1202
|
+
Create meshes representing the boundaries of every element in the B-spline for visualization.
|
|
1203
|
+
|
|
1204
|
+
This method generates a list of `io.Mesh` objects containing the geometry and optional fields
|
|
1205
|
+
needed to plot the limits (borders) of all elements from the isoparametric space of the B-spline.
|
|
1206
|
+
Supports time-dependent fields and arbitrary dimension.
|
|
1207
|
+
|
|
1208
|
+
Parameters
|
|
1209
|
+
----------
|
|
1210
|
+
ctrl_pts : np.ndarray[np.floating]
|
|
1211
|
+
Array of control points of the B-spline, with shape
|
|
1212
|
+
(`NPh`, number of elements for dim 1, ..., number of elements for dim `NPa`),
|
|
1213
|
+
where `NPh` is the physical space dimension and `NPa` is the dimension of the
|
|
1214
|
+
isoparametric space.
|
|
1215
|
+
n_eval_per_elem : Union[int, Iterable[int]], optional
|
|
1216
|
+
Number of evaluation points per element for each isoparametric dimension.
|
|
1217
|
+
By default, 10.
|
|
1218
|
+
- If an `int` is provided, the same number is used for all dimensions.
|
|
1219
|
+
- If an `Iterable` is provided, each value corresponds to a different dimension.
|
|
1220
|
+
n_step : int, optional
|
|
1221
|
+
Number of time steps to plot. By default, 1.
|
|
1222
|
+
fields : dict, optional
|
|
1223
|
+
Dictionary of fields to plot at each time step. Keys are field names. Values can be:
|
|
1224
|
+
- a `function` taking (`BSpline` spline, `tuple` of `np.ndarray[np.floating]` XI) and
|
|
1225
|
+
returning a `np.ndarray[np.floating]` of shape (`n_step`, number of combinations of XI, field size),
|
|
1226
|
+
- a `np.ndarray[np.floating]` defined **on the control points**, of shape (`n_step`, field size, *`ctrl_pts.shape[1:]`),
|
|
1227
|
+
which is then interpolated using the B-spline basis functions,
|
|
1228
|
+
- a `np.ndarray[np.floating]` defined **on the evaluation grid**, of shape (`n_step`, field size, *grid shape),
|
|
1229
|
+
where `grid shape` matches the discretization provided by XI or `n_eval_per_elem`.
|
|
1230
|
+
In this case, the field is interpolated in physical space using `scipy.interpolate.griddata`.
|
|
1231
|
+
XI : tuple[np.ndarray[np.floating], ...], optional
|
|
1232
|
+
Parametric coordinates at which to evaluate the B-spline and fields.
|
|
1233
|
+
If not `None`, overrides the `n_eval_per_elem` parameter.
|
|
1234
|
+
If `None`, a regular grid is generated according to `n_eval_per_elem`.
|
|
1235
|
+
paraview_sizes: dict, optionnal
|
|
1236
|
+
The fields present in this `dict` are overrided by `np.NaN`s.
|
|
1237
|
+
The keys must be the fields names and the values must be the fields sizes for paraview.
|
|
1238
|
+
By default, {}.
|
|
1239
|
+
|
|
1240
|
+
Returns
|
|
1241
|
+
-------
|
|
1242
|
+
list[io.Mesh]
|
|
1243
|
+
List of `io.Mesh` objects, one for each time step, containing the element boundary geometry
|
|
1244
|
+
and associated fields.
|
|
1245
|
+
|
|
1246
|
+
Notes
|
|
1247
|
+
-----
|
|
1248
|
+
- The element boundary mesh is constructed by connecting points along the unique knot values
|
|
1249
|
+
in each isoparametric direction, outlining the limits of each element.
|
|
1250
|
+
- Fields can be provided either as callable functions, as arrays defined on the control points,
|
|
1251
|
+
or as arrays already defined on a regular evaluation grid.
|
|
1252
|
+
- When fields are defined on a grid, they are interpolated in the physical space using
|
|
1253
|
+
`scipy.interpolate.griddata` with linear interpolation.
|
|
1254
|
+
- The first axis of the field array or function output corresponds to the time step, even if there is only one.
|
|
1255
|
+
- The method supports B-splines of arbitrary dimension.
|
|
1256
|
+
|
|
1257
|
+
Examples
|
|
1258
|
+
--------
|
|
1259
|
+
>>> degrees = [2, 2]
|
|
1260
|
+
>>> knots = [np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float'),
|
|
1261
|
+
... np.array([0, 0, 0, 1, 1, 1], dtype='float')]
|
|
1262
|
+
>>> spline = BSpline(degrees, knots)
|
|
1263
|
+
>>> ctrl_pts = np.random.rand(3, 4, 3) # 3D control points for a 2D surface
|
|
1264
|
+
>>> meshes = spline.make_elem_separator_meshes(ctrl_pts)
|
|
1265
|
+
>>> mesh = meshes[0]
|
|
1266
|
+
"""
|
|
1267
|
+
if type(n_eval_per_elem) is int:
|
|
1268
|
+
n_eval_per_elem = [n_eval_per_elem] * self.NPa
|
|
1269
|
+
if XI is None:
|
|
1270
|
+
XI = self.linspace(n_eval_per_elem)
|
|
1271
|
+
interp_points = self(ctrl_pts, XI).reshape((3, -1)).T
|
|
1272
|
+
shape = [xi.size for xi in XI]
|
|
1273
|
+
NXI = np.prod(shape)
|
|
1274
|
+
NPh = ctrl_pts.shape[0]
|
|
1275
|
+
knots_uniq = []
|
|
1276
|
+
shape_uniq = []
|
|
1277
|
+
for basis in self.bases:
|
|
1278
|
+
knot = basis.knot
|
|
1279
|
+
knot_uniq = np.unique(knot[np.logical_and(knot >= 0, knot <= 1)])
|
|
1280
|
+
knots_uniq.append(knot_uniq)
|
|
1281
|
+
shape_uniq.append(knot_uniq.size)
|
|
1282
|
+
points = None
|
|
1283
|
+
point_data = {key: None for key in fields}
|
|
1284
|
+
lines = None
|
|
1285
|
+
Size = 0
|
|
1286
|
+
n = self.getNbFunc()
|
|
1287
|
+
for idx in range(self.NPa):
|
|
1288
|
+
basis = self.bases[idx]
|
|
1289
|
+
knot_uniq = knots_uniq[idx]
|
|
1290
|
+
n_eval = n_eval_per_elem[idx]
|
|
1291
|
+
couples = (
|
|
1292
|
+
np.concatenate(
|
|
1293
|
+
(
|
|
1294
|
+
np.expand_dims(knot_uniq[:-1], axis=0),
|
|
1295
|
+
np.expand_dims(knot_uniq[1:], axis=0),
|
|
1296
|
+
),
|
|
1297
|
+
axis=0,
|
|
1298
|
+
)
|
|
1299
|
+
.reshape((2, -1))
|
|
1300
|
+
.T
|
|
1301
|
+
)
|
|
1302
|
+
for a, b in couples:
|
|
1303
|
+
lin = np.linspace(a, b, n_eval)
|
|
1304
|
+
inner_XI = tuple(knots_uniq[:idx] + [lin] + knots_uniq[(idx + 1) :])
|
|
1305
|
+
inner_shape = shape_uniq[:idx] + [lin.size] + shape_uniq[(idx + 1) :]
|
|
1306
|
+
size = np.prod(inner_shape)
|
|
1307
|
+
N = self.DN(inner_XI, [0] * self.NPa) # type: ignore
|
|
1308
|
+
pts = N @ ctrl_pts.reshape((NPh, -1)).T
|
|
1309
|
+
points = pts if points is None else np.vstack((points, pts))
|
|
1310
|
+
for key, value in fields.items():
|
|
1311
|
+
if key in paraview_sizes:
|
|
1312
|
+
to_store = np.full((n_step, size, paraview_sizes[key]), np.NAN)
|
|
1313
|
+
elif callable(value):
|
|
1314
|
+
to_store = value(self, inner_XI)
|
|
1315
|
+
else:
|
|
1316
|
+
value = np.asarray(value)
|
|
1317
|
+
if value.ndim >= 2 and value.shape[-self.NPa :] == tuple(
|
|
1318
|
+
ctrl_pts.shape[1:]
|
|
1319
|
+
):
|
|
1320
|
+
paraview_size = value.shape[1]
|
|
1321
|
+
arr = value.reshape((n_step, paraview_size, n)).reshape(
|
|
1322
|
+
(n_step * paraview_size, n)
|
|
1323
|
+
)
|
|
1324
|
+
to_store = (
|
|
1325
|
+
(arr @ N.T)
|
|
1326
|
+
.reshape((n_step, paraview_size, -1))
|
|
1327
|
+
.transpose(0, 2, 1)
|
|
1328
|
+
)
|
|
1329
|
+
elif value.ndim >= 2 and value.shape[-self.NPa :] == tuple(
|
|
1330
|
+
shape
|
|
1331
|
+
):
|
|
1332
|
+
paraview_size = value.shape[1]
|
|
1333
|
+
interp_field = griddata(
|
|
1334
|
+
interp_points,
|
|
1335
|
+
value.reshape((n_step * paraview_size, NXI)).T,
|
|
1336
|
+
pts,
|
|
1337
|
+
method="linear",
|
|
1338
|
+
)
|
|
1339
|
+
to_store = interp_field.reshape(
|
|
1340
|
+
(pts.shape[0], n_step, paraview_size)
|
|
1341
|
+
).transpose(1, 0, 2)
|
|
1342
|
+
else:
|
|
1343
|
+
raise ValueError(
|
|
1344
|
+
f"Field {key} shape {value.shape} not understood."
|
|
1345
|
+
)
|
|
1346
|
+
if point_data[key] is None:
|
|
1347
|
+
point_data[key] = to_store
|
|
1348
|
+
else:
|
|
1349
|
+
point_data[key] = np.concatenate((point_data[key], to_store), axis=1) # type: ignore
|
|
1350
|
+
lns = Size + np.arange(size).reshape(inner_shape)
|
|
1351
|
+
lns = np.moveaxis(lns, idx, 0).reshape((inner_shape[idx], -1))
|
|
1352
|
+
lns = np.concatenate(
|
|
1353
|
+
(
|
|
1354
|
+
np.expand_dims(lns[:-1], axis=-1),
|
|
1355
|
+
np.expand_dims(lns[1:], axis=-1),
|
|
1356
|
+
),
|
|
1357
|
+
axis=-1,
|
|
1358
|
+
).reshape((-1, 2))
|
|
1359
|
+
lines = lns if lines is None else np.vstack((lines, lns))
|
|
1360
|
+
Size += size
|
|
1361
|
+
cells = {"line": lines}
|
|
1362
|
+
# make meshes
|
|
1363
|
+
meshes = []
|
|
1364
|
+
for i in range(n_step):
|
|
1365
|
+
point_data_step = {}
|
|
1366
|
+
for key, value in point_data.items():
|
|
1367
|
+
point_data_step[key] = value[i] # type: ignore
|
|
1368
|
+
mesh = io.Mesh(points, cells, point_data_step) # type: ignore
|
|
1369
|
+
meshes.append(mesh)
|
|
1370
|
+
|
|
1371
|
+
return meshes
|
|
1372
|
+
|
|
1373
|
+
def make_elements_interior_meshes(
|
|
1374
|
+
self,
|
|
1375
|
+
ctrl_pts: np.ndarray[np.floating],
|
|
1376
|
+
n_eval_per_elem: Union[int, Iterable[int]] = 10,
|
|
1377
|
+
n_step: int = 1,
|
|
1378
|
+
fields: dict = {},
|
|
1379
|
+
XI: Union[None, tuple[np.ndarray[np.floating], ...]] = None,
|
|
1380
|
+
) -> list[io.Mesh]:
|
|
1381
|
+
"""
|
|
1382
|
+
Create meshes representing the interior of each element in the B-spline.
|
|
1383
|
+
|
|
1384
|
+
This method generates a list of `io.Mesh` objects containing the geometry and optional fields
|
|
1385
|
+
for the interior of all elements, suitable for visualization (e.g., in ParaView). Supports
|
|
1386
|
+
time-dependent fields and arbitrary dimension.
|
|
1387
|
+
|
|
1388
|
+
Parameters
|
|
1389
|
+
----------
|
|
1390
|
+
ctrl_pts : np.ndarray[np.floating]
|
|
1391
|
+
Array of control points of the B-spline, with shape
|
|
1392
|
+
(`NPh`, number of points for dim 1, ..., number of points for dim `NPa`),
|
|
1393
|
+
where `NPh` is the physical space dimension and `NPa` is the dimension of
|
|
1394
|
+
the isoparametric space.
|
|
1395
|
+
n_eval_per_elem : Union[int, Iterable[int]], optional
|
|
1396
|
+
Number of evaluation points per element for each isoparametric dimension.
|
|
1397
|
+
By default, 10.
|
|
1398
|
+
- If an `int` is provided, the same number is used for all dimensions.
|
|
1399
|
+
- If an `Iterable` is provided, each value corresponds to a different dimension.
|
|
1400
|
+
n_step : int, optional
|
|
1401
|
+
Number of time steps to plot. By default, 1.
|
|
1402
|
+
fields : dict, optional
|
|
1403
|
+
Dictionary of fields to plot at each time step. Keys are field names. Values can be:
|
|
1404
|
+
- a `function` taking (`BSpline` spline, `tuple` of `np.ndarray[np.floating]` XI) and
|
|
1405
|
+
returning a `np.ndarray[np.floating]` of shape (`n_step`, number of combinations of XI, field size),
|
|
1406
|
+
- a `np.ndarray[np.floating]` defined **on the control points**, of shape (`n_step`, field size, *`ctrl_pts.shape[1:]`),
|
|
1407
|
+
in which case it is interpolated using the B-spline basis functions,
|
|
1408
|
+
- a `np.ndarray[np.floating]` defined **directly on the evaluation grid**, of shape (`n_step`, field size, *grid shape),
|
|
1409
|
+
where `grid shape` is the shape of the discretization XI (i.e., number of points along each parametric axis).
|
|
1410
|
+
By default, `{}` (no fields).
|
|
1411
|
+
XI : tuple[np.ndarray[np.floating], ...], optional
|
|
1412
|
+
Parametric coordinates at which to evaluate the B-spline and fields.
|
|
1413
|
+
If not `None`, overrides the `n_eval_per_elem` parameter.
|
|
1414
|
+
If `None`, a regular grid is generated according to `n_eval_per_elem`.
|
|
1415
|
+
|
|
1416
|
+
Returns
|
|
1417
|
+
-------
|
|
1418
|
+
list[io.Mesh]
|
|
1419
|
+
List of `io.Mesh` objects, one for each time step, containing the element interior geometry
|
|
1420
|
+
and associated fields.
|
|
1421
|
+
|
|
1422
|
+
Notes
|
|
1423
|
+
-----
|
|
1424
|
+
- The interior mesh is constructed by evaluating the B-spline at a regular grid of points
|
|
1425
|
+
in the isoparametric space, with connectivity corresponding to lines (1D), quads (2D), or
|
|
1426
|
+
hexahedra (3D).
|
|
1427
|
+
- Fields can be provided either as arrays (on control points or on the discretization grid) or as functions.
|
|
1428
|
+
- Arrays given on control points are automatically interpolated using the B-spline basis functions.
|
|
1429
|
+
- Arrays already given on the evaluation grid are used directly without interpolation.
|
|
1430
|
+
- The first axis of the field array or function output must correspond to the time step, even if there is only one.
|
|
1431
|
+
- The method is compatible with B-splines of arbitrary dimension.
|
|
1432
|
+
|
|
1433
|
+
Examples
|
|
1434
|
+
--------
|
|
1435
|
+
>>> degrees = [2, 2]
|
|
1436
|
+
>>> knots = [np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float'),
|
|
1437
|
+
... np.array([0, 0, 0, 1, 1, 1], dtype='float')]
|
|
1438
|
+
>>> spline = BSpline(degrees, knots)
|
|
1439
|
+
>>> ctrl_pts = np.random.rand(3, 4, 3) # 3D control points for a 2D surface
|
|
1440
|
+
>>> # Field given on control points (needs interpolation)
|
|
1441
|
+
>>> field_on_ctrl_pts = np.random.rand(1, 1, 4, 3)
|
|
1442
|
+
>>> # Field given directly on the evaluation grid (no interpolation)
|
|
1443
|
+
>>> field_on_grid = np.random.rand(1, 1, 10, 10)
|
|
1444
|
+
>>> meshes = spline.make_elements_interior_meshes(
|
|
1445
|
+
... ctrl_pts,
|
|
1446
|
+
... fields={'temperature': field_on_ctrl_pts, 'pressure': field_on_grid}
|
|
1447
|
+
... XI= # TODO
|
|
1448
|
+
... )
|
|
1449
|
+
>>> mesh = meshes[0]
|
|
1450
|
+
"""
|
|
1451
|
+
if XI is None:
|
|
1452
|
+
XI = self.linspace(n_eval_per_elem)
|
|
1453
|
+
# make points
|
|
1454
|
+
N = self.DN(XI)
|
|
1455
|
+
NPh = ctrl_pts.shape[0]
|
|
1456
|
+
points = N @ ctrl_pts.reshape((NPh, -1)).T
|
|
1457
|
+
# make connectivity
|
|
1458
|
+
elements = {2: "line", 4: "quad", 8: "hexahedron"}
|
|
1459
|
+
shape = [xi.size for xi in XI]
|
|
1460
|
+
NXI = np.prod(shape)
|
|
1461
|
+
inds = np.arange(NXI).reshape(shape)
|
|
1462
|
+
for idx in range(self.NPa):
|
|
1463
|
+
rng = np.arange(inds.shape[idx])
|
|
1464
|
+
inds = np.concatenate(
|
|
1465
|
+
(
|
|
1466
|
+
np.expand_dims(np.take(inds, rng[:-1], axis=idx), axis=-1),
|
|
1467
|
+
np.expand_dims(np.take(inds, rng[1:], axis=idx), axis=-1),
|
|
1468
|
+
),
|
|
1469
|
+
axis=-1,
|
|
1470
|
+
)
|
|
1471
|
+
if self.NPa >= 2:
|
|
1472
|
+
for i in np.ndindex(*inds.shape[:-2]):
|
|
1473
|
+
inds[i] = [
|
|
1474
|
+
[inds[i + (0, 0)], inds[i + (0, 1)]],
|
|
1475
|
+
[inds[i + (1, 1)], inds[i + (1, 0)]],
|
|
1476
|
+
]
|
|
1477
|
+
inds = inds.reshape((*inds.shape[: self.NPa], -1))
|
|
1478
|
+
inds = inds.reshape((-1, inds.shape[-1]))
|
|
1479
|
+
cells = {elements[inds.shape[-1]]: inds}
|
|
1480
|
+
# make fields
|
|
1481
|
+
n = self.getNbFunc()
|
|
1482
|
+
point_data = {}
|
|
1483
|
+
for key, value in fields.items():
|
|
1484
|
+
if callable(value):
|
|
1485
|
+
point_data[key] = value(self, XI)
|
|
1486
|
+
else:
|
|
1487
|
+
value = np.asarray(value)
|
|
1488
|
+
if value.ndim >= 2 and value.shape[-self.NPa :] == tuple(
|
|
1489
|
+
ctrl_pts.shape[1:]
|
|
1490
|
+
):
|
|
1491
|
+
# Field given on control points: interpolate with basis functions
|
|
1492
|
+
paraview_size = value.shape[1]
|
|
1493
|
+
arr = value.reshape((n_step * paraview_size, n))
|
|
1494
|
+
point_data[key] = (
|
|
1495
|
+
(arr @ N.T)
|
|
1496
|
+
.reshape((n_step, paraview_size, NXI))
|
|
1497
|
+
.transpose(0, 2, 1)
|
|
1498
|
+
)
|
|
1499
|
+
elif value.ndim >= 2 and value.shape[-self.NPa :] == tuple(shape):
|
|
1500
|
+
# Field already given on discretization grid
|
|
1501
|
+
paraview_size = value.shape[1]
|
|
1502
|
+
point_data[key] = value.reshape(
|
|
1503
|
+
(n_step, paraview_size, NXI)
|
|
1504
|
+
).transpose(0, 2, 1)
|
|
1505
|
+
else:
|
|
1506
|
+
raise ValueError(f"Field {key} shape {value.shape} not understood.")
|
|
1507
|
+
# make meshes
|
|
1508
|
+
meshes = []
|
|
1509
|
+
for i in range(n_step):
|
|
1510
|
+
point_data_step = {}
|
|
1511
|
+
for key, value in point_data.items():
|
|
1512
|
+
point_data_step[key] = value[i]
|
|
1513
|
+
mesh = io.Mesh(points, cells, point_data_step) # type: ignore
|
|
1514
|
+
meshes.append(mesh)
|
|
1515
|
+
|
|
1516
|
+
return meshes
|
|
1517
|
+
|
|
1518
|
+
def saveParaview(
|
|
1519
|
+
self,
|
|
1520
|
+
ctrl_pts: np.ndarray[np.floating],
|
|
1521
|
+
path: str,
|
|
1522
|
+
name: str,
|
|
1523
|
+
n_step: int = 1,
|
|
1524
|
+
n_eval_per_elem: Union[int, Iterable[int]] = 10,
|
|
1525
|
+
fields: Union[dict, None] = None,
|
|
1526
|
+
XI: Union[None, tuple[np.ndarray[np.floating], ...]] = None,
|
|
1527
|
+
groups: Union[dict[str, dict[str, Union[str, int]]], None] = None,
|
|
1528
|
+
make_pvd: bool = True,
|
|
1529
|
+
verbose: bool = True,
|
|
1530
|
+
fields_on_interior_only: Union[bool, Literal["auto"], list[str]] = "auto",
|
|
1531
|
+
) -> dict[str, dict[str, Union[str, int]]]:
|
|
1532
|
+
"""
|
|
1533
|
+
Save B-spline visualization data as Paraview files.
|
|
1534
|
+
|
|
1535
|
+
This method creates three types of visualization files:
|
|
1536
|
+
- Interior mesh showing the B-spline surface/volume
|
|
1537
|
+
- Element borders showing the mesh structure
|
|
1538
|
+
- Control points mesh showing the control structure
|
|
1539
|
+
|
|
1540
|
+
All files are saved in VTU format with an optional PVD file to group them.
|
|
1541
|
+
|
|
1542
|
+
Parameters
|
|
1543
|
+
----------
|
|
1544
|
+
ctrl_pts : np.ndarray[np.floating]
|
|
1545
|
+
Control points defining the B-spline geometry.
|
|
1546
|
+
Shape: (`NPh`, n1, n2, ...) where:
|
|
1547
|
+
- `NPh` is the dimension of the physical space
|
|
1548
|
+
- ni is the number of control points in the i-th isoparametric dimension
|
|
1549
|
+
|
|
1550
|
+
path : str
|
|
1551
|
+
Directory path where the PV files will be saved
|
|
1552
|
+
|
|
1553
|
+
name : str
|
|
1554
|
+
Base name for the output files
|
|
1555
|
+
|
|
1556
|
+
n_step : int, optional
|
|
1557
|
+
Number of time steps to save. By default, 1.
|
|
1558
|
+
|
|
1559
|
+
n_eval_per_elem : Union[int, Iterable[int]], optional
|
|
1560
|
+
Number of evaluation points per element for each isoparametric dimension.
|
|
1561
|
+
By default, 10.
|
|
1562
|
+
- If an `int` is provided, the same number is used for all dimensions.
|
|
1563
|
+
- If an `Iterable` is provided, each value corresponds to a different dimension.
|
|
1564
|
+
|
|
1565
|
+
fields : Union[dict, None], optional
|
|
1566
|
+
Fields to visualize at each time step. Dictionary format:
|
|
1567
|
+
{
|
|
1568
|
+
"field_name": `field_value`
|
|
1569
|
+
}
|
|
1570
|
+
where `field_value` can be either:
|
|
1571
|
+
|
|
1572
|
+
1. A numpy array with shape (`n_step`, `field_size`, `*ctrl_pts.shape[1:]`) where:
|
|
1573
|
+
- `n_step`: Number of time steps
|
|
1574
|
+
- `field_size`: Size of the field at each point (1 for scalar, 3 for vector)
|
|
1575
|
+
- `*ctrl_pts.shape[1:]`: Same shape as control points (excluding `NPh`)
|
|
1576
|
+
|
|
1577
|
+
2. A numpy array with shape (`n_step`, `field_size`, `*grid_shape`) where:
|
|
1578
|
+
- `n_step`: Number of time steps
|
|
1579
|
+
- `field_size`: Size of the field at each point (1 for scalar, 3 for vector)
|
|
1580
|
+
- `*grid_shape`: Shape of the evaluation grid (number of points along each isoparametric axis)
|
|
1581
|
+
|
|
1582
|
+
3. A function that computes field values (`np.ndarray[np.floating]`) at given
|
|
1583
|
+
points from the `BSpline` instance and `XI`, the tuple of arrays containing evaluation
|
|
1584
|
+
points for each dimension (`tuple[np.ndarray[np.floating], ...]`).
|
|
1585
|
+
The result should be an array of shape (`n_step`, `n_points`, `field_size`) where:
|
|
1586
|
+
- `n_step`: Number of time steps
|
|
1587
|
+
- `n_points`: Number of evaluation points (n_xi × n_eta × ...)
|
|
1588
|
+
- `field_size`: Size of the field at each point (1 for scalar, 3 for vector)
|
|
1589
|
+
|
|
1590
|
+
By default, None.
|
|
1591
|
+
|
|
1592
|
+
XI : tuple[np.ndarray[np.floating], ...], optional
|
|
1593
|
+
Parametric coordinates at which to evaluate the B-spline and fields.
|
|
1594
|
+
If not `None`, overrides the `n_eval_per_elem` parameter.
|
|
1595
|
+
If `None`, a regular grid is generated according to `n_eval_per_elem`.
|
|
1596
|
+
|
|
1597
|
+
groups : Union[dict[str, dict[str, Union[str, int]]], None], optional
|
|
1598
|
+
Nested dictionary specifying file groups for PVD organization. Format:
|
|
1599
|
+
{
|
|
1600
|
+
"group_name": {
|
|
1601
|
+
"ext": str, # File extension (e.g., "vtu")
|
|
1602
|
+
"npart": int, # Number of parts in the group
|
|
1603
|
+
"nstep": int # Number of timesteps
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
The method automatically creates/updates three groups:
|
|
1607
|
+
- "interior": For the B-spline surface/volume mesh
|
|
1608
|
+
- "elements_borders": For the element boundary mesh
|
|
1609
|
+
- "control_points": For the control point mesh
|
|
1610
|
+
|
|
1611
|
+
If provided, existing groups are updated; if None, these groups are created.
|
|
1612
|
+
By default, None.
|
|
1613
|
+
|
|
1614
|
+
make_pvd : bool, optional
|
|
1615
|
+
Whether to create a PVD file grouping all VTU files. By default, True.
|
|
1616
|
+
|
|
1617
|
+
verbose : bool, optional
|
|
1618
|
+
Whether to print progress information. By default, True.
|
|
1619
|
+
|
|
1620
|
+
fields_on_interior_only: Union[bool, Literal['auto'], list[str]], optionnal
|
|
1621
|
+
Whether to include fields only on the interior mesh (`True`), on all meshes (`False`),
|
|
1622
|
+
or on specified field names.
|
|
1623
|
+
If set to `'auto'`, fields named `'u'`, `'U'`, `'displacement'` or `'displ'`
|
|
1624
|
+
are included on all meshes while others are only included on the interior mesh.
|
|
1625
|
+
By default, 'auto'.
|
|
1626
|
+
|
|
1627
|
+
Returns
|
|
1628
|
+
-------
|
|
1629
|
+
groups : dict[str, dict[str, Union[str, int]]]
|
|
1630
|
+
Updated groups dictionary with information about saved files.
|
|
1631
|
+
|
|
1632
|
+
Notes
|
|
1633
|
+
-----
|
|
1634
|
+
- Creates three types of VTU files for each time step:
|
|
1635
|
+
- {name}_interior_{part}_{step}.vtu
|
|
1636
|
+
- {name}_elements_borders_{part}_{step}.vtu
|
|
1637
|
+
- {name}_control_points_{part}_{step}.vtu
|
|
1638
|
+
- If `make_pvd=True`, creates a PVD file named {name}.pvd
|
|
1639
|
+
- Fields can be visualized as scalars or vectors in Paraview
|
|
1640
|
+
- The method supports time-dependent visualization through `n_step`
|
|
1641
|
+
|
|
1642
|
+
Examples
|
|
1643
|
+
--------
|
|
1644
|
+
Save a 2D B-spline visualization:
|
|
1645
|
+
>>> degrees = [2, 2]
|
|
1646
|
+
>>> knots = [np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float'),
|
|
1647
|
+
... np.array([0, 0, 0, 0.5, 1, 1, 1], dtype='float')]
|
|
1648
|
+
>>> spline = BSpline(degrees, knots)
|
|
1649
|
+
>>> ctrl_pts = np.random.rand(3, 4, 4) # 3D control points
|
|
1650
|
+
>>> spline.saveParaview(ctrl_pts, "./output", "bspline")
|
|
1651
|
+
|
|
1652
|
+
Save with a custom field:
|
|
1653
|
+
>>> def displacement(spline, XI):
|
|
1654
|
+
... # Compute displacement field
|
|
1655
|
+
... return np.random.rand(1, np.prod([x.size for x in XI]), 3)
|
|
1656
|
+
>>> fields = {"displacement": displacement}
|
|
1657
|
+
>>> spline.saveParaview(ctrl_pts, "./output", "bspline", fields=fields)
|
|
1658
|
+
"""
|
|
1659
|
+
|
|
1660
|
+
if type(n_eval_per_elem) is int:
|
|
1661
|
+
n_eval_per_elem = [n_eval_per_elem] * self.NPa
|
|
1662
|
+
|
|
1663
|
+
if fields is None:
|
|
1664
|
+
fields = {}
|
|
1665
|
+
|
|
1666
|
+
if groups is None:
|
|
1667
|
+
groups = {}
|
|
1668
|
+
|
|
1669
|
+
interior = "interior"
|
|
1670
|
+
if interior in groups:
|
|
1671
|
+
groups[interior]["npart"] += 1
|
|
1672
|
+
else:
|
|
1673
|
+
groups[interior] = {"ext": "vtu", "npart": 1, "nstep": n_step}
|
|
1674
|
+
elements_borders = "elements_borders"
|
|
1675
|
+
if elements_borders in groups:
|
|
1676
|
+
groups[elements_borders]["npart"] += 1
|
|
1677
|
+
else:
|
|
1678
|
+
groups[elements_borders] = {"ext": "vtu", "npart": 1, "nstep": n_step}
|
|
1679
|
+
control_points = "control_points"
|
|
1680
|
+
if control_points in groups:
|
|
1681
|
+
groups[control_points]["npart"] += 1
|
|
1682
|
+
else:
|
|
1683
|
+
groups[control_points] = {"ext": "vtu", "npart": 1, "nstep": n_step}
|
|
1684
|
+
|
|
1685
|
+
paraview_sizes = {}
|
|
1686
|
+
if fields_on_interior_only is True:
|
|
1687
|
+
for key, value in fields.items():
|
|
1688
|
+
if callable(value):
|
|
1689
|
+
paraview_sizes[key] = value(self, np.zeros((self.NPa, 1))).shape[2]
|
|
1690
|
+
else:
|
|
1691
|
+
paraview_sizes[key] = value.shape[1]
|
|
1692
|
+
elif fields_on_interior_only is False:
|
|
1693
|
+
pass
|
|
1694
|
+
elif fields_on_interior_only == "auto":
|
|
1695
|
+
for key, value in fields.items():
|
|
1696
|
+
if key not in ["u", "U", "displacement", "displ"]:
|
|
1697
|
+
if callable(value):
|
|
1698
|
+
paraview_sizes[key] = value(
|
|
1699
|
+
self, np.zeros((self.NPa, 1))
|
|
1700
|
+
).shape[2]
|
|
1701
|
+
else:
|
|
1702
|
+
paraview_sizes[key] = value.shape[1]
|
|
1703
|
+
else:
|
|
1704
|
+
for key in fields_on_interior_only:
|
|
1705
|
+
value = fields[key]
|
|
1706
|
+
if callable(value):
|
|
1707
|
+
paraview_sizes[key] = value(self, np.zeros((self.NPa, 1))).shape[2]
|
|
1708
|
+
else:
|
|
1709
|
+
paraview_sizes[key] = value.shape[1]
|
|
1710
|
+
|
|
1711
|
+
meshes = self.make_elements_interior_meshes(
|
|
1712
|
+
ctrl_pts, n_eval_per_elem, n_step, fields, XI
|
|
1713
|
+
)
|
|
1714
|
+
prefix = os.path.join(
|
|
1715
|
+
path, f"{name}_{interior}_{groups[interior]['npart'] - 1}"
|
|
1716
|
+
)
|
|
1717
|
+
for time_step, mesh in enumerate(meshes):
|
|
1718
|
+
mesh.write(f"{prefix}_{time_step}.vtu")
|
|
1719
|
+
if verbose:
|
|
1720
|
+
print(interior, "done")
|
|
1721
|
+
|
|
1722
|
+
meshes = self.make_elem_separator_meshes(
|
|
1723
|
+
ctrl_pts, n_eval_per_elem, n_step, fields, XI, paraview_sizes
|
|
1724
|
+
)
|
|
1725
|
+
prefix = os.path.join(
|
|
1726
|
+
path, f"{name}_{elements_borders}_{groups[elements_borders]['npart'] - 1}"
|
|
1727
|
+
)
|
|
1728
|
+
for time_step, mesh in enumerate(meshes):
|
|
1729
|
+
mesh.write(f"{prefix}_{time_step}.vtu")
|
|
1730
|
+
if verbose:
|
|
1731
|
+
print(elements_borders, "done")
|
|
1732
|
+
|
|
1733
|
+
meshes = self.make_control_poly_meshes(
|
|
1734
|
+
ctrl_pts, n_eval_per_elem, n_step, fields, XI, paraview_sizes
|
|
1735
|
+
)
|
|
1736
|
+
prefix = os.path.join(
|
|
1737
|
+
path, f"{name}_{control_points}_{groups[control_points]['npart'] - 1}"
|
|
1738
|
+
)
|
|
1739
|
+
for time_step, mesh in enumerate(meshes):
|
|
1740
|
+
mesh.write(f"{prefix}_{time_step}.vtu")
|
|
1741
|
+
if verbose:
|
|
1742
|
+
print(control_points, "done")
|
|
1743
|
+
|
|
1744
|
+
if make_pvd:
|
|
1745
|
+
writePVD(os.path.join(path, name), groups)
|
|
1746
|
+
|
|
1747
|
+
return groups
|
|
1748
|
+
|
|
1749
|
+
def getGeomdl(self, ctrl_pts):
|
|
1750
|
+
try:
|
|
1751
|
+
from geomdl import BSpline as geomdlBS
|
|
1752
|
+
except:
|
|
1753
|
+
raise
|
|
1754
|
+
if self.NPa == 1:
|
|
1755
|
+
curve = geomdlBS.Curve()
|
|
1756
|
+
curve.degree = self.bases[0].p
|
|
1757
|
+
curve.ctrl_pts = ctrl_pts.T.tolist()
|
|
1758
|
+
curve.knotvector = self.bases[0].knot
|
|
1759
|
+
return curve
|
|
1760
|
+
elif self.NPa == 2:
|
|
1761
|
+
surf = geomdlBS.Surface()
|
|
1762
|
+
surf.degree_u = self.bases[0].p
|
|
1763
|
+
surf.degree_v = self.bases[1].p
|
|
1764
|
+
surf.ctrl_pts2d = ctrl_pts.transpose((1, 2, 0)).tolist()
|
|
1765
|
+
surf.knotvector_u = self.bases[0].knot
|
|
1766
|
+
surf.knotvector_v = self.bases[1].knot
|
|
1767
|
+
return surf
|
|
1768
|
+
elif self.NPa == 3:
|
|
1769
|
+
vol = geomdlBS.Volume()
|
|
1770
|
+
vol.degree_u = self.bases[0].p
|
|
1771
|
+
vol.degree_v = self.bases[1].p
|
|
1772
|
+
vol.degree_w = self.bases[2].p
|
|
1773
|
+
vol.cpsize = ctrl_pts.shape[1:]
|
|
1774
|
+
# ctrl_pts format (zeta, xi, eta)
|
|
1775
|
+
vol.ctrl_pts = (
|
|
1776
|
+
ctrl_pts.transpose(3, 1, 2, 0).reshape((-1, ctrl_pts.shape[0])).tolist()
|
|
1777
|
+
)
|
|
1778
|
+
vol.knotvector_u = self.bases[0].knot
|
|
1779
|
+
vol.knotvector_v = self.bases[1].knot
|
|
1780
|
+
vol.knotvector_w = self.bases[2].knot
|
|
1781
|
+
return vol
|
|
1782
|
+
else:
|
|
1783
|
+
raise NotImplementedError("Can only export curves, sufaces or volumes !")
|
|
1784
|
+
|
|
1785
|
+
def to_dict(self) -> dict:
|
|
1786
|
+
"""
|
|
1787
|
+
Returns a dictionary representation of the BSpline object.
|
|
1788
|
+
"""
|
|
1789
|
+
return {"NPa": self.NPa, "bases": [b.to_dict() for b in self.bases]}
|
|
1790
|
+
|
|
1791
|
+
@classmethod
|
|
1792
|
+
def from_dict(cls, data: dict) -> "BSpline":
|
|
1793
|
+
"""
|
|
1794
|
+
Creates a BSpline object from a dictionary representation.
|
|
1795
|
+
"""
|
|
1796
|
+
NPa = data["NPa"]
|
|
1797
|
+
bases = np.array([BSplineBasis.from_dict(b) for b in data["bases"]])
|
|
1798
|
+
assert len(bases) == NPa, "The parametric space must be of size 'NPa'."
|
|
1799
|
+
return cls.from_bases(bases)
|
|
1800
|
+
|
|
1801
|
+
def save(self, filepath: str, ctrl_pts: Union[np.ndarray, None] = None) -> None:
|
|
1802
|
+
"""
|
|
1803
|
+
Save the BSpline object to a file.
|
|
1804
|
+
Control points are optional.
|
|
1805
|
+
Supported extensions: json, pkl
|
|
1806
|
+
"""
|
|
1807
|
+
data = self.to_dict()
|
|
1808
|
+
if ctrl_pts is not None:
|
|
1809
|
+
data["ctrl_pts"] = ctrl_pts.tolist()
|
|
1810
|
+
ext = filepath.split(".")[-1]
|
|
1811
|
+
if ext == "json":
|
|
1812
|
+
with open(filepath, "w") as f:
|
|
1813
|
+
json.dump(data, f, indent=2)
|
|
1814
|
+
elif ext == "pkl":
|
|
1815
|
+
with open(filepath, "wb") as f:
|
|
1816
|
+
pickle.dump(data, f)
|
|
1817
|
+
else:
|
|
1818
|
+
raise ValueError(
|
|
1819
|
+
f"Unknown extension {ext}. Supported extensions: json, pkl."
|
|
1820
|
+
)
|
|
1821
|
+
|
|
1822
|
+
@classmethod
|
|
1823
|
+
def load(cls, filepath: str) -> Union["BSpline", tuple["BSpline", np.ndarray]]:
|
|
1824
|
+
"""
|
|
1825
|
+
Load a BSpline object from a file.
|
|
1826
|
+
May return control points if the file contains them.
|
|
1827
|
+
Supported extensions: json, pkl
|
|
1828
|
+
"""
|
|
1829
|
+
ext = filepath.split(".")[-1]
|
|
1830
|
+
if ext == "json":
|
|
1831
|
+
with open(filepath, "r") as f:
|
|
1832
|
+
data = json.load(f)
|
|
1833
|
+
elif ext == "pkl":
|
|
1834
|
+
with open(filepath, "rb") as f:
|
|
1835
|
+
data = pickle.load(f)
|
|
1836
|
+
else:
|
|
1837
|
+
raise ValueError(
|
|
1838
|
+
f"Unknown extension {ext}. Supported extensions: json, pkl."
|
|
1839
|
+
)
|
|
1840
|
+
this = cls.from_dict(data)
|
|
1841
|
+
if "ctrl_pts" in data:
|
|
1842
|
+
ctrl_pts = np.array(data["ctrl_pts"])
|
|
1843
|
+
return this, ctrl_pts
|
|
1844
|
+
return this
|
|
1845
|
+
|
|
1846
|
+
def plot(
|
|
1847
|
+
self,
|
|
1848
|
+
ctrl_pts: np.ndarray[np.floating],
|
|
1849
|
+
n_eval_per_elem: Union[int, Iterable[int]] = 10,
|
|
1850
|
+
plotter: Union[mpl.axes.Axes, "pv.Plotter", None] = None,
|
|
1851
|
+
ctrl_color: str = "#d95f02",
|
|
1852
|
+
interior_color: str = "#666666",
|
|
1853
|
+
elem_color: str = "#7570b3",
|
|
1854
|
+
border_color: str = "#1b9e77",
|
|
1855
|
+
language: Union[Literal["english"], Literal["français"]] = "english",
|
|
1856
|
+
show: bool = True,
|
|
1857
|
+
) -> Union[mpl.axes.Axes, "pv.Plotter", None]:
|
|
1858
|
+
"""
|
|
1859
|
+
Plot the B-spline using either Matplotlib or PyVista, depending on availability.
|
|
1860
|
+
|
|
1861
|
+
Automatically selects the appropriate plotting backend (Matplotlib or PyVista)
|
|
1862
|
+
based on which libraries are installed. Supports visualization of B-spline curves,
|
|
1863
|
+
surfaces, and volumes in 2D or 3D space, with control mesh, element borders, and patch borders.
|
|
1864
|
+
|
|
1865
|
+
Parameters
|
|
1866
|
+
----------
|
|
1867
|
+
ctrl_pts : np.ndarray[np.floating]
|
|
1868
|
+
Control points defining the B-spline geometry.
|
|
1869
|
+
Shape: (NPh, n1, n2, ...) where:
|
|
1870
|
+
- NPh is the dimension of the physical space (2 or 3)
|
|
1871
|
+
- ni is the number of control points in the i-th isoparametric dimension
|
|
1872
|
+
n_eval_per_elem : Union[int, Iterable[int]], optional
|
|
1873
|
+
Number of evaluation points per element for visualizing the B-spline.
|
|
1874
|
+
Can be specified as:
|
|
1875
|
+
- Single integer: Same number for all dimensions
|
|
1876
|
+
- Iterable of integers: Different numbers for each dimension
|
|
1877
|
+
By default, 10.
|
|
1878
|
+
plotter : Union[mpl.axes.Axes, 'pv.Plotter', None], optional
|
|
1879
|
+
Plotter object for the visualization:
|
|
1880
|
+
- If PyVista is available: Can be a `pv.Plotter` instance
|
|
1881
|
+
- If only Matplotlib is available: Can be a `mpl.axes.Axes` instance
|
|
1882
|
+
- If None, creates a new plotter/axes.
|
|
1883
|
+
Default is None.
|
|
1884
|
+
ctrl_color : str, optional
|
|
1885
|
+
Color for the control mesh visualization.
|
|
1886
|
+
Default is '#d95f02' (orange).
|
|
1887
|
+
interior_color : str, optional
|
|
1888
|
+
Color for the B-spline geometry.
|
|
1889
|
+
Default is '#666666' (gray).
|
|
1890
|
+
elem_color : str, optional
|
|
1891
|
+
Color for element boundary visualization.
|
|
1892
|
+
Default is '#7570b3' (purple).
|
|
1893
|
+
border_color : str, optional
|
|
1894
|
+
Color for patch boundary visualization.
|
|
1895
|
+
Default is '#1b9e77' (green).
|
|
1896
|
+
language : str, optional
|
|
1897
|
+
Language for the plot labels and legends in matplotlib. Can be 'english' or 'français'.
|
|
1898
|
+
Default is 'english'.
|
|
1899
|
+
show : bool, optional
|
|
1900
|
+
Whether to display the plot immediately.
|
|
1901
|
+
Default is True.
|
|
1902
|
+
|
|
1903
|
+
Returns
|
|
1904
|
+
-------
|
|
1905
|
+
plotter : Union[mpl.axes.Axes, 'pv.Plotter', None]
|
|
1906
|
+
The plotter object used for visualization (Matplotlib axes or PyVista plotter)
|
|
1907
|
+
if `show` is False. Otherwise, returns None.
|
|
1908
|
+
|
|
1909
|
+
Notes
|
|
1910
|
+
-----
|
|
1911
|
+
- If PyVista is available and the physical space is 3D, uses `plotPV` for 3D visualization.
|
|
1912
|
+
- Otherwise, uses `plotMPL` for 2D/3D visualization with Matplotlib.
|
|
1913
|
+
- For 3D visualization, PyVista is recommended for better interactivity and rendering.
|
|
1914
|
+
- For 2D visualization, Matplotlib is used by default.
|
|
1915
|
+
|
|
1916
|
+
Examples
|
|
1917
|
+
--------
|
|
1918
|
+
Plot a 2D curve in 2D space:
|
|
1919
|
+
>>> degrees = [2]
|
|
1920
|
+
>>> knots = [np.array([0, 0, 0, 1, 1, 1], dtype='float')]
|
|
1921
|
+
>>> spline = BSpline(degrees, knots)
|
|
1922
|
+
>>> ctrl_pts = np.random.rand(2, 3) # 2D control points
|
|
1923
|
+
>>> spline.plot(ctrl_pts)
|
|
1924
|
+
|
|
1925
|
+
Plot a 2D surface in 3D space with PyVista (if available):
|
|
1926
|
+
>>> degrees = [2, 2]
|
|
1927
|
+
>>> knots = [np.array([0, 0, 0, 1, 1, 1], dtype='float'),
|
|
1928
|
+
... np.array([0, 0, 0, 1, 1, 1], dtype='float')]
|
|
1929
|
+
>>> spline = BSpline(degrees, knots)
|
|
1930
|
+
>>> ctrl_pts = np.random.rand(3, 3, 3) # 3D control points
|
|
1931
|
+
>>> spline.plot(ctrl_pts)
|
|
1932
|
+
"""
|
|
1933
|
+
NPh = ctrl_pts.shape[0]
|
|
1934
|
+
if _can_visualize_pyvista and NPh == 3:
|
|
1935
|
+
return self.plotPV(
|
|
1936
|
+
ctrl_pts,
|
|
1937
|
+
n_eval_per_elem,
|
|
1938
|
+
plotter,
|
|
1939
|
+
ctrl_color,
|
|
1940
|
+
interior_color,
|
|
1941
|
+
elem_color,
|
|
1942
|
+
border_color,
|
|
1943
|
+
show,
|
|
1944
|
+
)
|
|
1945
|
+
else:
|
|
1946
|
+
return self.plotMPL(
|
|
1947
|
+
ctrl_pts,
|
|
1948
|
+
n_eval_per_elem,
|
|
1949
|
+
plotter,
|
|
1950
|
+
ctrl_color,
|
|
1951
|
+
interior_color,
|
|
1952
|
+
elem_color,
|
|
1953
|
+
border_color,
|
|
1954
|
+
language,
|
|
1955
|
+
show,
|
|
1956
|
+
)
|
|
1957
|
+
|
|
1958
|
+
def plotMPL(
|
|
1959
|
+
self,
|
|
1960
|
+
ctrl_pts: np.ndarray[np.floating],
|
|
1961
|
+
n_eval_per_elem: Union[int, Iterable[int]] = 10,
|
|
1962
|
+
ax: Union[mpl.axes.Axes, None] = None,
|
|
1963
|
+
ctrl_color: str = "#1b9e77",
|
|
1964
|
+
interior_color: str = "#7570b3",
|
|
1965
|
+
elem_color: str = "#666666",
|
|
1966
|
+
border_color: str = "#d95f02",
|
|
1967
|
+
language: Union[Literal["english"], Literal["français"]] = "english",
|
|
1968
|
+
show: bool = True,
|
|
1969
|
+
) -> Union[mpl.axes.Axes, None]:
|
|
1970
|
+
"""
|
|
1971
|
+
Plot the B-spline using Matplotlib.
|
|
1972
|
+
|
|
1973
|
+
Creates a visualization of the B-spline geometry showing the control mesh,
|
|
1974
|
+
B-spline surface/curve, element borders, and patch borders. Supports plotting
|
|
1975
|
+
1D curves and 2D surfaces in 2D space, and 2D surfaces and 3D volumes in 3D space.
|
|
1976
|
+
|
|
1977
|
+
Parameters
|
|
1978
|
+
----------
|
|
1979
|
+
ctrl_pts : np.ndarray[np.floating]
|
|
1980
|
+
Control points defining the B-spline geometry.
|
|
1981
|
+
Shape: (NPh, n1, n2, ...) where:
|
|
1982
|
+
- NPh is the dimension of the physical space (2 or 3)
|
|
1983
|
+
- ni is the number of control points in the i-th isoparametric dimension
|
|
1984
|
+
|
|
1985
|
+
n_eval_per_elem : Union[int, Iterable[int]], optional
|
|
1986
|
+
Number of evaluation points per element for visualizing the B-spline.
|
|
1987
|
+
Can be specified as:
|
|
1988
|
+
- Single integer: Same number for all dimensions
|
|
1989
|
+
- Iterable of integers: Different numbers for each dimension
|
|
1990
|
+
By default, 10.
|
|
1991
|
+
|
|
1992
|
+
ax : Union[mpl.axes.Axes, None], optional
|
|
1993
|
+
Matplotlib axes for plotting. If None, creates a new figure and axes.
|
|
1994
|
+
For 3D visualizations, must be a 3D axes if provided (created with
|
|
1995
|
+
`projection='3d'`).
|
|
1996
|
+
Default is None (creates new axes).
|
|
1997
|
+
|
|
1998
|
+
ctrl_color : str, optional
|
|
1999
|
+
Color for the control mesh visualization:
|
|
2000
|
+
- Applied to control points (markers)
|
|
2001
|
+
- Applied to control mesh lines
|
|
2002
|
+
Default is '#1b9e77' (green).
|
|
2003
|
+
|
|
2004
|
+
interior_color : str, optional
|
|
2005
|
+
Color for the B-spline geometry:
|
|
2006
|
+
- For curves: Line color
|
|
2007
|
+
- For surfaces: Face color (with transparency)
|
|
2008
|
+
- For volumes: Face color of boundary surfaces (with transparency)
|
|
2009
|
+
Default is '#7570b3' (purple).
|
|
2010
|
+
|
|
2011
|
+
elem_color : str, optional
|
|
2012
|
+
Color for element boundary visualization:
|
|
2013
|
+
- Shows internal mesh structure
|
|
2014
|
+
- Helps visualize knot locations
|
|
2015
|
+
Default is '#666666' (gray).
|
|
2016
|
+
|
|
2017
|
+
border_color : str, optional
|
|
2018
|
+
Color for patch boundary visualization:
|
|
2019
|
+
- Outlines the entire B-spline patch
|
|
2020
|
+
- Helps distinguish patch edges
|
|
2021
|
+
Default is '#d95f02' (orange).
|
|
2022
|
+
|
|
2023
|
+
language: str, optional
|
|
2024
|
+
Language for the plot labels. Can be 'english' or 'français'.
|
|
2025
|
+
Default is 'english'.
|
|
2026
|
+
|
|
2027
|
+
show : bool, optional
|
|
2028
|
+
Whether to display the plot immediately. Can be useful to add more stuff to the plot.
|
|
2029
|
+
Default is True.
|
|
2030
|
+
|
|
2031
|
+
Returns
|
|
2032
|
+
-------
|
|
2033
|
+
ax : Union[mpl.axes.Axes, None]
|
|
2034
|
+
Matplotlib axes for the plot if show is deactivated, otherwise None.
|
|
2035
|
+
|
|
2036
|
+
Notes
|
|
2037
|
+
-----
|
|
2038
|
+
Visualization components:
|
|
2039
|
+
- Control mesh: Shows control points and their connections
|
|
2040
|
+
- B-spline: Shows the actual curve/surface/volume
|
|
2041
|
+
- Element borders: Shows the boundaries between elements
|
|
2042
|
+
- Patch borders: Shows the outer boundaries of the B-spline
|
|
2043
|
+
|
|
2044
|
+
Supported configurations:
|
|
2045
|
+
- 1D B-spline in 2D space (curve)
|
|
2046
|
+
- 2D B-spline in 2D space (surface)
|
|
2047
|
+
- 2D B-spline in 3D space (surface)
|
|
2048
|
+
- 3D B-spline in 3D space (volume)
|
|
2049
|
+
|
|
2050
|
+
For 3D visualization:
|
|
2051
|
+
- Surfaces are shown with transparency
|
|
2052
|
+
- Volume visualization shows the faces with transparency
|
|
2053
|
+
- View angle is automatically set for surfaces based on surface normal
|
|
2054
|
+
|
|
2055
|
+
Examples
|
|
2056
|
+
--------
|
|
2057
|
+
Plot a 2D curve in 2D space:
|
|
2058
|
+
>>> degrees = [2]
|
|
2059
|
+
>>> knots = [np.array([0, 0, 0, 1, 1, 1], dtype='float')]
|
|
2060
|
+
>>> spline = BSpline(degrees, knots)
|
|
2061
|
+
>>> ctrl_pts = np.random.rand(2, 3) # 2D control points
|
|
2062
|
+
>>> spline.plotMPL(ctrl_pts)
|
|
2063
|
+
|
|
2064
|
+
Plot a 2D surface in 3D space:
|
|
2065
|
+
>>> degrees = [2, 2]
|
|
2066
|
+
>>> knots = [np.array([0, 0, 0, 1, 1, 1], dtype='float'),
|
|
2067
|
+
... np.array([0, 0, 0, 1, 1, 1], dtype='float')]
|
|
2068
|
+
>>> spline = BSpline(degrees, knots)
|
|
2069
|
+
>>> ctrl_pts = np.random.rand(3, 3, 3) # 3D control points
|
|
2070
|
+
>>> spline.plotMPL(ctrl_pts)
|
|
2071
|
+
|
|
2072
|
+
Plot on existing axes with custom colors:
|
|
2073
|
+
>>> fig = plt.figure()
|
|
2074
|
+
>>> ax = fig.add_subplot(projection='3d')
|
|
2075
|
+
>>> spline.plotMPL(ctrl_pts, ax=ax, ctrl_color='red', interior_color='blue')
|
|
2076
|
+
"""
|
|
2077
|
+
import matplotlib.pyplot as plt
|
|
2078
|
+
from matplotlib.collections import LineCollection
|
|
2079
|
+
from matplotlib.patches import Polygon
|
|
2080
|
+
from mpl_toolkits.mplot3d.art3d import Line3DCollection
|
|
2081
|
+
from matplotlib import lines
|
|
2082
|
+
|
|
2083
|
+
if language == "english":
|
|
2084
|
+
ctrl_mesh = "Control mesh"
|
|
2085
|
+
elems_bord = "Elements borders"
|
|
2086
|
+
b_spline = "B-spline"
|
|
2087
|
+
b_spline_patch = "B-spline patch"
|
|
2088
|
+
patch_bord = "Patch borders"
|
|
2089
|
+
elif language == "français":
|
|
2090
|
+
ctrl_mesh = "Maillage de contrôle"
|
|
2091
|
+
elems_bord = "Frontières inter-éléments"
|
|
2092
|
+
b_spline = "B-spline"
|
|
2093
|
+
b_spline_patch = "Patch B-spline"
|
|
2094
|
+
patch_bord = "Frontières inter-patchs"
|
|
2095
|
+
else:
|
|
2096
|
+
raise NotImplementedError(
|
|
2097
|
+
f"Can't understand language '{language}'. Try 'english' or 'français'."
|
|
2098
|
+
)
|
|
2099
|
+
NPh = ctrl_pts.shape[0]
|
|
2100
|
+
fig = plt.figure() if ax is None else ax.get_figure()
|
|
2101
|
+
if NPh == 2:
|
|
2102
|
+
ax = fig.add_subplot() if ax is None else ax
|
|
2103
|
+
if self.NPa == 1:
|
|
2104
|
+
ax.plot(
|
|
2105
|
+
ctrl_pts[0],
|
|
2106
|
+
ctrl_pts[1],
|
|
2107
|
+
marker="o",
|
|
2108
|
+
c=ctrl_color,
|
|
2109
|
+
label=ctrl_mesh,
|
|
2110
|
+
zorder=0,
|
|
2111
|
+
)
|
|
2112
|
+
(xi,) = self.linspace(n_eval_per_elem=n_eval_per_elem)
|
|
2113
|
+
x, y = self.__call__(ctrl_pts, [xi])
|
|
2114
|
+
ax.plot(x, y, c=interior_color, label=b_spline, zorder=1)
|
|
2115
|
+
(xi_elem,) = self.linspace(n_eval_per_elem=1)
|
|
2116
|
+
x_elem, y_elem = self.__call__(ctrl_pts, [xi_elem])
|
|
2117
|
+
ax.scatter(x_elem, y_elem, marker="*", c=elem_color, label=elems_bord, zorder=2) # type: ignore
|
|
2118
|
+
elif self.NPa == 2:
|
|
2119
|
+
xi, eta = self.linspace(n_eval_per_elem=n_eval_per_elem)
|
|
2120
|
+
xi_elem, eta_elem = self.linspace(n_eval_per_elem=1)
|
|
2121
|
+
x_xi, y_xi = self.__call__(ctrl_pts, [xi_elem, eta])
|
|
2122
|
+
x_eta, y_eta = self.__call__(ctrl_pts, [xi, eta_elem])
|
|
2123
|
+
x_pol = np.hstack(
|
|
2124
|
+
(x_xi[0, ::1], x_eta[::1, -1], x_xi[-1, ::-1], x_eta[::-1, 0])
|
|
2125
|
+
)
|
|
2126
|
+
y_pol = np.hstack(
|
|
2127
|
+
(y_xi[0, ::1], y_eta[::1, -1], y_xi[-1, ::-1], y_eta[::-1, 0])
|
|
2128
|
+
)
|
|
2129
|
+
xy_pol = np.hstack((x_pol[:, None], y_pol[:, None]))
|
|
2130
|
+
ax.add_patch(Polygon(xy_pol, fill=True, edgecolor=None, facecolor=interior_color, alpha=0.5, label=b_spline_patch, zorder=0)) # type: ignore
|
|
2131
|
+
ax.plot(
|
|
2132
|
+
ctrl_pts[0, 0, 0],
|
|
2133
|
+
ctrl_pts[1, 0, 0],
|
|
2134
|
+
marker="o",
|
|
2135
|
+
c=ctrl_color,
|
|
2136
|
+
label=ctrl_mesh,
|
|
2137
|
+
zorder=1,
|
|
2138
|
+
ms=plt.rcParams["lines.markersize"] / np.sqrt(2),
|
|
2139
|
+
)
|
|
2140
|
+
ax.add_collection(LineCollection(ctrl_pts.transpose(1, 2, 0), colors=ctrl_color, zorder=1)) # type: ignore
|
|
2141
|
+
ax.add_collection(LineCollection(ctrl_pts.transpose(2, 1, 0), colors=ctrl_color, zorder=1)) # type: ignore
|
|
2142
|
+
ax.scatter(ctrl_pts[0].ravel(), ctrl_pts[1].ravel(), marker="o", c=ctrl_color, zorder=1, s=0.5 * plt.rcParams["lines.markersize"] ** 2) # type: ignore
|
|
2143
|
+
ax.plot(
|
|
2144
|
+
x_xi[0, 0],
|
|
2145
|
+
y_xi[0, 0],
|
|
2146
|
+
linestyle="-",
|
|
2147
|
+
c=elem_color,
|
|
2148
|
+
label=elems_bord,
|
|
2149
|
+
zorder=2,
|
|
2150
|
+
)
|
|
2151
|
+
ax.add_collection(LineCollection(np.array([x_xi, y_xi]).transpose(1, 2, 0)[1:-1], colors=elem_color, zorder=2)) # type: ignore
|
|
2152
|
+
ax.add_collection(LineCollection(np.array([x_eta, y_eta]).transpose(2, 1, 0)[1:-1], colors=elem_color, zorder=2)) # type: ignore
|
|
2153
|
+
ax.add_patch(Polygon(xy_pol, lw=1.25 * plt.rcParams["lines.linewidth"], fill=False, edgecolor=border_color, label=patch_bord, zorder=2)) # type: ignore
|
|
2154
|
+
else:
|
|
2155
|
+
raise ValueError(f"Can't plot a {self.NPa}D shape in a 2D space.")
|
|
2156
|
+
ax.legend()
|
|
2157
|
+
ax.set_aspect(1)
|
|
2158
|
+
elif NPh == 3:
|
|
2159
|
+
ax = fig.add_subplot(projection="3d") if ax is None else ax
|
|
2160
|
+
if self.NPa == 1:
|
|
2161
|
+
pass
|
|
2162
|
+
elif self.NPa == 2:
|
|
2163
|
+
xi, eta = self.linspace(n_eval_per_elem=n_eval_per_elem)
|
|
2164
|
+
xi_elem, eta_elem = self.linspace(n_eval_per_elem=1)
|
|
2165
|
+
x, y, z = self.__call__(ctrl_pts, [xi, eta])
|
|
2166
|
+
x_xi, y_xi, z_xi = self.__call__(ctrl_pts, [xi_elem, eta])
|
|
2167
|
+
x_eta, y_eta, z_eta = self.__call__(ctrl_pts, [xi, eta_elem])
|
|
2168
|
+
ax.plot_surface(
|
|
2169
|
+
x,
|
|
2170
|
+
y,
|
|
2171
|
+
z,
|
|
2172
|
+
rcount=1,
|
|
2173
|
+
ccount=1,
|
|
2174
|
+
edgecolor=None,
|
|
2175
|
+
facecolor=interior_color,
|
|
2176
|
+
alpha=0.5,
|
|
2177
|
+
)
|
|
2178
|
+
ax.plot_wireframe(
|
|
2179
|
+
ctrl_pts[0], ctrl_pts[1], ctrl_pts[2], color=ctrl_color, zorder=2
|
|
2180
|
+
)
|
|
2181
|
+
ax.scatter(
|
|
2182
|
+
ctrl_pts[0], ctrl_pts[1], ctrl_pts[2], color=ctrl_color, zorder=2
|
|
2183
|
+
)
|
|
2184
|
+
ax.add_collection(Line3DCollection(np.array([x_xi, y_xi, z_xi]).transpose(1, 2, 0), colors=elem_color, zorder=1)) # type: ignore
|
|
2185
|
+
ax.add_collection(Line3DCollection(np.array([x_eta, y_eta, z_eta]).transpose(2, 1, 0), colors=elem_color, zorder=1)) # type: ignore
|
|
2186
|
+
# ax.plot_surface(x, y, z, rcount=1, ccount=1, edgecolor=border_color, facecolor=None, alpha=0)
|
|
2187
|
+
ctrl_handle = lines.Line2D(
|
|
2188
|
+
[], [], color=ctrl_color, marker="o", linestyle="-", label=ctrl_mesh
|
|
2189
|
+
)
|
|
2190
|
+
elem_handle = lines.Line2D(
|
|
2191
|
+
[], [], color=elem_color, linestyle="-", label=elems_bord
|
|
2192
|
+
)
|
|
2193
|
+
border_handle = lines.Line2D(
|
|
2194
|
+
[], [], color=border_color, linestyle="-", label=patch_bord
|
|
2195
|
+
)
|
|
2196
|
+
ax.legend(handles=[ctrl_handle, elem_handle, border_handle])
|
|
2197
|
+
mid_param = [
|
|
2198
|
+
np.array([sum(self.bases[0].span) / 2]),
|
|
2199
|
+
np.array([sum(self.bases[1].span) / 2]),
|
|
2200
|
+
]
|
|
2201
|
+
dxi, deta = self.__call__(ctrl_pts, mid_param, k=1)
|
|
2202
|
+
nx, ny, nz = np.cross(dxi.ravel(), deta.ravel())
|
|
2203
|
+
azim = np.degrees(np.arctan2(ny, nx))
|
|
2204
|
+
elev = np.degrees(np.arcsin(nz / np.sqrt(nx**2 + ny**2 + nz**2)))
|
|
2205
|
+
ax.view_init(elev=elev + 30, azim=azim)
|
|
2206
|
+
elif self.NPa == 3:
|
|
2207
|
+
XI = self.linspace(n_eval_per_elem=n_eval_per_elem)
|
|
2208
|
+
XI_elem = self.linspace(n_eval_per_elem=1)
|
|
2209
|
+
for face in range(3):
|
|
2210
|
+
for side in [-1, 0]:
|
|
2211
|
+
XI_face = list(XI)
|
|
2212
|
+
XI_face[face] = np.array([XI[face][side]])
|
|
2213
|
+
X = np.squeeze(np.array(self.__call__(ctrl_pts, XI_face)))
|
|
2214
|
+
ax.plot_surface(
|
|
2215
|
+
*X,
|
|
2216
|
+
rcount=1,
|
|
2217
|
+
ccount=1,
|
|
2218
|
+
edgecolor=None,
|
|
2219
|
+
color=interior_color,
|
|
2220
|
+
alpha=0.5,
|
|
2221
|
+
)
|
|
2222
|
+
for axis in range(3):
|
|
2223
|
+
ctrl_mesh_axis = np.rollaxis(ctrl_pts, axis + 1, 1).reshape(
|
|
2224
|
+
(3, ctrl_pts.shape[axis + 1], -1)
|
|
2225
|
+
)
|
|
2226
|
+
ax.add_collection(Line3DCollection(ctrl_mesh_axis.transpose(2, 1, 0), colors=ctrl_color, zorder=2)) # type: ignore
|
|
2227
|
+
ax.scatter(*ctrl_pts.reshape((3, -1)), color=ctrl_color, zorder=2)
|
|
2228
|
+
for face in range(3):
|
|
2229
|
+
for side in [-1, 0]:
|
|
2230
|
+
for face_i, transpose in zip(
|
|
2231
|
+
sorted([(face + 1) % 3, (face + 2) % 3]),
|
|
2232
|
+
((1, 2, 0), (2, 1, 0)),
|
|
2233
|
+
):
|
|
2234
|
+
XI_elem_border = list(XI)
|
|
2235
|
+
XI_elem_border[face] = np.array([XI[face][side]])
|
|
2236
|
+
XI_elem_border[face_i] = XI_elem[face_i]
|
|
2237
|
+
X_elem_border = np.squeeze(
|
|
2238
|
+
np.array(self.__call__(ctrl_pts, XI_elem_border))
|
|
2239
|
+
)
|
|
2240
|
+
ax.add_collection(Line3DCollection(X_elem_border.transpose(*transpose), colors=elem_color, zorder=1)) # type: ignore
|
|
2241
|
+
ctrl_handle = lines.Line2D(
|
|
2242
|
+
[], [], color=ctrl_color, marker="o", linestyle="-", label=ctrl_mesh
|
|
2243
|
+
)
|
|
2244
|
+
elem_handle = lines.Line2D(
|
|
2245
|
+
[], [], color=elem_color, linestyle="-", label=elems_bord
|
|
2246
|
+
)
|
|
2247
|
+
border_handle = lines.Line2D(
|
|
2248
|
+
[], [], color=border_color, linestyle="-", label=patch_bord
|
|
2249
|
+
)
|
|
2250
|
+
ax.legend(handles=[ctrl_handle, elem_handle, border_handle])
|
|
2251
|
+
else:
|
|
2252
|
+
raise ValueError(f"Can't plot a {self.NPa}D shape in a 3D space.")
|
|
2253
|
+
else:
|
|
2254
|
+
raise ValueError(f"Can't plot in a {NPh}D space.")
|
|
2255
|
+
if show:
|
|
2256
|
+
plt.show()
|
|
2257
|
+
else:
|
|
2258
|
+
return ax
|
|
2259
|
+
|
|
2260
|
+
def plotPV(
|
|
2261
|
+
self,
|
|
2262
|
+
ctrl_pts: np.ndarray[np.floating],
|
|
2263
|
+
n_eval_per_elem: Union[int, Iterable[int]] = 10,
|
|
2264
|
+
pv_plotter: Union["pv.Plotter", None] = None,
|
|
2265
|
+
ctrl_color: str = "#d95f02",
|
|
2266
|
+
interior_color: str = "#666666",
|
|
2267
|
+
elem_color: str = "#7570b3",
|
|
2268
|
+
border_color: str = "#1b9e77",
|
|
2269
|
+
show: bool = True,
|
|
2270
|
+
) -> Union["pv.Plotter", None]:
|
|
2271
|
+
"""
|
|
2272
|
+
Plot the B-spline using PyVista for 3D visualization.
|
|
2273
|
+
|
|
2274
|
+
Creates an interactive 3D visualization of the B-spline geometry showing the control mesh,
|
|
2275
|
+
B-spline surface/curve/volume, element borders, and patch borders. Supports plotting
|
|
2276
|
+
1D curves, 2D surfaces, and 3D volumes in 3D space.
|
|
2277
|
+
|
|
2278
|
+
Parameters
|
|
2279
|
+
----------
|
|
2280
|
+
ctrl_pts : np.ndarray[np.floating]
|
|
2281
|
+
Control points defining the B-spline geometry.
|
|
2282
|
+
Shape: (NPh, n1, n2, ...) where:
|
|
2283
|
+
- NPh is the dimension of the physical space (2 or 3)
|
|
2284
|
+
- ni is the number of control points in the i-th isoparametric dimension
|
|
2285
|
+
If NPh=2, control points are automatically converted to 3D for plotting.
|
|
2286
|
+
n_eval_per_elem : Union[int, Iterable[int]], optional
|
|
2287
|
+
Number of evaluation points per element for visualizing the B-spline.
|
|
2288
|
+
Can be specified as:
|
|
2289
|
+
- Single integer: Same number for all dimensions
|
|
2290
|
+
- Iterable of integers: Different numbers for each dimension
|
|
2291
|
+
By default, 10.
|
|
2292
|
+
pv_plotter : Union['pv.Plotter', None], optional
|
|
2293
|
+
PyVista plotter for visualization. If None, creates a new plotter.
|
|
2294
|
+
Default is None.
|
|
2295
|
+
ctrl_color : str, optional
|
|
2296
|
+
Color for the control mesh visualization.
|
|
2297
|
+
Default is '#d95f02' (orange).
|
|
2298
|
+
interior_color : str, optional
|
|
2299
|
+
Color for the B-spline geometry.
|
|
2300
|
+
Default is '#666666' (gray).
|
|
2301
|
+
elem_color : str, optional
|
|
2302
|
+
Color for element boundary visualization.
|
|
2303
|
+
Default is '#7570b3' (purple).
|
|
2304
|
+
border_color : str, optional
|
|
2305
|
+
Color for patch boundary visualization.
|
|
2306
|
+
Default is '#1b9e77' (green).
|
|
2307
|
+
show : bool, optional
|
|
2308
|
+
Whether to display the plot immediately.
|
|
2309
|
+
Default is True.
|
|
2310
|
+
|
|
2311
|
+
Returns
|
|
2312
|
+
-------
|
|
2313
|
+
pv_plotter : Union['pv.Plotter', None]
|
|
2314
|
+
The PyVista plotter object used for visualization if show is False.
|
|
2315
|
+
Otherwise, returns None.
|
|
2316
|
+
|
|
2317
|
+
Notes
|
|
2318
|
+
-----
|
|
2319
|
+
- For 1D B-splines: Plots the curve and control points.
|
|
2320
|
+
- For 2D B-splines: Plots the surface, control points, element borders, and patch borders.
|
|
2321
|
+
- For 3D B-splines: Plots the volume faces, control points, element borders, and patch borders.
|
|
2322
|
+
- If control points are 2D, they are automatically converted to 3D with z=0 for plotting.
|
|
2323
|
+
|
|
2324
|
+
Examples
|
|
2325
|
+
--------
|
|
2326
|
+
Plot a curved line in 3D space:
|
|
2327
|
+
>>> degrees = [2]
|
|
2328
|
+
>>> knots = [np.array([0, 0, 0, 1, 1, 1], dtype='float')]
|
|
2329
|
+
>>> spline = BSpline(degrees, knots)
|
|
2330
|
+
>>> ctrl_pts = np.random.rand(3, 3) # 3D control points
|
|
2331
|
+
>>> spline.plotPV(ctrl_pts)
|
|
2332
|
+
|
|
2333
|
+
Plot a 2D surface in 3D space:
|
|
2334
|
+
>>> degrees = [2, 2]
|
|
2335
|
+
>>> knots = [np.array([0, 0, 0, 1, 1, 1], dtype='float'),
|
|
2336
|
+
... np.array([0, 0, 0, 1, 1, 1], dtype='float')]
|
|
2337
|
+
>>> spline = BSpline(degrees, knots)
|
|
2338
|
+
>>> ctrl_pts = np.random.rand(3, 3, 3) # 3D control points
|
|
2339
|
+
>>> spline.plotPV(ctrl_pts)
|
|
2340
|
+
"""
|
|
2341
|
+
import pyvista as pv
|
|
2342
|
+
|
|
2343
|
+
NPh = ctrl_pts.shape[0]
|
|
2344
|
+
if NPh == 2:
|
|
2345
|
+
ctrl_pts = np.concatenate(
|
|
2346
|
+
(ctrl_pts, np.zeros((1, *ctrl_pts.shape[1:]))), axis=0
|
|
2347
|
+
)
|
|
2348
|
+
print("Control points converted to 3D for plot.")
|
|
2349
|
+
elif NPh != 3:
|
|
2350
|
+
raise ValueError("Can only plot in a 3D space.")
|
|
2351
|
+
if pv_plotter is None:
|
|
2352
|
+
pv_plotter = pv.Plotter()
|
|
2353
|
+
if self.NPa == 1:
|
|
2354
|
+
lines = self.__call__(
|
|
2355
|
+
ctrl_pts, self.linspace(n_eval_per_elem=n_eval_per_elem)
|
|
2356
|
+
).T
|
|
2357
|
+
pv_plotter.add_lines(lines, connected=True, color=interior_color, width=2)
|
|
2358
|
+
points = self.__call__(ctrl_pts, self.linspace(n_eval_per_elem=1)).T
|
|
2359
|
+
pv_plotter.add_points(points, color=border_color, point_size=10)
|
|
2360
|
+
pv_plotter.add_points(
|
|
2361
|
+
ctrl_pts.T,
|
|
2362
|
+
color=ctrl_color,
|
|
2363
|
+
point_size=8,
|
|
2364
|
+
render_points_as_spheres=True,
|
|
2365
|
+
)
|
|
2366
|
+
elif self.NPa == 2:
|
|
2367
|
+
xi, eta = self.linspace(n_eval_per_elem=n_eval_per_elem)
|
|
2368
|
+
xi_elem, eta_elem = self.linspace(n_eval_per_elem=1)
|
|
2369
|
+
x, y, z = self.__call__(ctrl_pts, [xi, eta])
|
|
2370
|
+
x_xi, y_xi, z_xi = self.__call__(ctrl_pts, [xi_elem, eta])
|
|
2371
|
+
x_eta, y_eta, z_eta = self.__call__(ctrl_pts, [xi, eta_elem])
|
|
2372
|
+
pv_plotter.add_mesh(
|
|
2373
|
+
pv.StructuredGrid(x, y, z), color=interior_color, opacity=0.5
|
|
2374
|
+
)
|
|
2375
|
+
lines_xi = np.repeat(
|
|
2376
|
+
np.stack((x_xi, y_xi, z_xi)).transpose(1, 2, 0), 2, axis=1
|
|
2377
|
+
)[:, 1:-1].reshape((-1, 3))
|
|
2378
|
+
lines_eta = np.repeat(
|
|
2379
|
+
np.stack((x_eta, y_eta, z_eta)).transpose(2, 1, 0), 2, axis=1
|
|
2380
|
+
)[:, 1:-1].reshape((-1, 3))
|
|
2381
|
+
lines = np.concatenate((lines_xi, lines_eta), axis=0)
|
|
2382
|
+
pv_plotter.add_lines(lines, connected=False, color=elem_color, width=2)
|
|
2383
|
+
pv_plotter.add_points(
|
|
2384
|
+
ctrl_pts.reshape((3, -1)).T,
|
|
2385
|
+
color=ctrl_color,
|
|
2386
|
+
point_size=8,
|
|
2387
|
+
render_points_as_spheres=True,
|
|
2388
|
+
)
|
|
2389
|
+
elif self.NPa == 3:
|
|
2390
|
+
xi, eta, zeta = self.linspace(n_eval_per_elem=n_eval_per_elem)
|
|
2391
|
+
xi_elem, eta_elem, zeta_elem = self.linspace(n_eval_per_elem=1)
|
|
2392
|
+
XI_faces = [
|
|
2393
|
+
[[xi, eta, np.array([zeta[0]])], [xi, eta, np.array([zeta[-1]])]],
|
|
2394
|
+
[[xi, np.array([eta[0]]), zeta], [xi, np.array([eta[-1]]), zeta]],
|
|
2395
|
+
[[np.array([xi[0]]), eta, zeta], [np.array([xi[-1]]), eta, zeta]],
|
|
2396
|
+
]
|
|
2397
|
+
XI_elem_borders = [
|
|
2398
|
+
[
|
|
2399
|
+
[
|
|
2400
|
+
[xi_elem, eta, np.array([zeta[0]])],
|
|
2401
|
+
[xi, eta_elem, np.array([zeta[0]])],
|
|
2402
|
+
],
|
|
2403
|
+
[
|
|
2404
|
+
[xi_elem, eta, np.array([zeta[-1]])],
|
|
2405
|
+
[xi, eta_elem, np.array([zeta[-1]])],
|
|
2406
|
+
],
|
|
2407
|
+
],
|
|
2408
|
+
[
|
|
2409
|
+
[
|
|
2410
|
+
[xi_elem, np.array([eta[0]]), zeta],
|
|
2411
|
+
[xi, np.array([eta[0]]), zeta_elem],
|
|
2412
|
+
],
|
|
2413
|
+
[
|
|
2414
|
+
[xi_elem, np.array([eta[-1]]), zeta],
|
|
2415
|
+
[xi, np.array([eta[-1]]), zeta_elem],
|
|
2416
|
+
],
|
|
2417
|
+
],
|
|
2418
|
+
[
|
|
2419
|
+
[
|
|
2420
|
+
[np.array([xi[0]]), eta_elem, zeta],
|
|
2421
|
+
[np.array([xi[0]]), eta, zeta_elem],
|
|
2422
|
+
],
|
|
2423
|
+
[
|
|
2424
|
+
[np.array([xi[-1]]), eta_elem, zeta],
|
|
2425
|
+
[np.array([xi[-1]]), eta, zeta_elem],
|
|
2426
|
+
],
|
|
2427
|
+
],
|
|
2428
|
+
]
|
|
2429
|
+
for XI_face_pair, XI_elem_borders_pair in zip(XI_faces, XI_elem_borders):
|
|
2430
|
+
for XI_face, (XI_elem_border_a, XI_elem_border_b) in zip(
|
|
2431
|
+
XI_face_pair, XI_elem_borders_pair
|
|
2432
|
+
):
|
|
2433
|
+
x, y, z = np.squeeze(np.array(self.__call__(ctrl_pts, XI_face)))
|
|
2434
|
+
x_a, y_a, z_a = np.squeeze(
|
|
2435
|
+
np.array(self.__call__(ctrl_pts, XI_elem_border_a))
|
|
2436
|
+
)
|
|
2437
|
+
x_b, y_b, z_b = np.squeeze(
|
|
2438
|
+
np.array(self.__call__(ctrl_pts, XI_elem_border_b))
|
|
2439
|
+
)
|
|
2440
|
+
pv_plotter.add_mesh(
|
|
2441
|
+
pv.StructuredGrid(x, y, z), color=interior_color, opacity=0.5
|
|
2442
|
+
)
|
|
2443
|
+
lines_xi = np.repeat(
|
|
2444
|
+
np.stack((x_a, y_a, z_a)).transpose(1, 2, 0), 2, axis=1
|
|
2445
|
+
)[:, 1:-1].reshape((-1, 3))
|
|
2446
|
+
lines_eta = np.repeat(
|
|
2447
|
+
np.stack((x_b, y_b, z_b)).transpose(2, 1, 0), 2, axis=1
|
|
2448
|
+
)[:, 1:-1].reshape((-1, 3))
|
|
2449
|
+
lines = np.concatenate((lines_xi, lines_eta), axis=0)
|
|
2450
|
+
pv_plotter.add_lines(
|
|
2451
|
+
lines, connected=False, color=elem_color, width=2
|
|
2452
|
+
)
|
|
2453
|
+
pv_plotter.add_points(
|
|
2454
|
+
ctrl_pts.reshape((3, -1)).T,
|
|
2455
|
+
color=ctrl_color,
|
|
2456
|
+
point_size=8,
|
|
2457
|
+
render_points_as_spheres=True,
|
|
2458
|
+
)
|
|
2459
|
+
else:
|
|
2460
|
+
raise ValueError(f"Can't plot a {self.NPa}D shape in a 3D space.")
|
|
2461
|
+
if show:
|
|
2462
|
+
pv_plotter.show()
|
|
2463
|
+
else:
|
|
2464
|
+
return pv_plotter
|