Spaces, knots & element extraction¶
This chapter follows one thread end to end: from the knot vector that defines a spline space, through the representation changes that rewrite a geometry without altering it, to the element extraction operators that hand each element to a finite-element / isogeometric assembly loop. It assumes the vocabulary from Core concepts.
Knot vectors¶
A BsplineSpace1D is a non-decreasing knot vector plus a
degree p. The knots partition the domain into spans (elements); on each span the
basis is a degree-p polynomial, and the knot multiplicities control how the pieces
join (see Continuity below). A BsplineSpace is a
tensor product of these 1-D spaces.
You rarely type knot vectors by hand. The factories in pantr.bspline build the
standard families:
Factory |
Builds |
Notes |
|---|---|---|
an open (clamped) uniform knot vector |
endpoints have multiplicity |
|
a periodic uniform knot vector |
wraps smoothly; for closed curves/surfaces |
|
a cardinal (uniform B-spline) knot vector |
shift-invariant interior basis |
|
a whole tensor-product |
per-direction |
from pantr.bspline import create_uniform_space
# Biquadratic surface space, 8 x 8 elements on [0,1]^2
space = create_uniform_space([2, 2], [8, 8])
space.num_basis # (10, 10) basis functions per direction
space.num_total_intervals # 64 elements
Each factory takes an optional continuity to raise interior knot multiplicities, and
create_uniform_space() also takes periodic and domain.
Continuity¶
Across an interior knot of multiplicity m, a degree-p spline is
C^{p-m} continuous:
m = 1(simple knot):C^{p-1}— the maximal smoothness of a uniform B-spline.m = p:C^0— a kink is allowed; the curve still connects.m = p+1: the geometry splits into independent pieces (this is exactly the clamped end of an open knot vector, and the basis of Bézier extraction).
get_unique_knots_and_multiplicity() reports the
multiplicities; multiplicity comparisons use the space’s tolerance
(from pantr.tolerance).
Greville abscissae¶
The Greville abscissa of a basis function is the average of its interior knots — the
parameter value naturally “attached” to its control point.
get_greville_abscissae() returns them for a 1-D space, and
create_greville_lattice() builds the tensor-product lattice for a
multivariate space. They are the default sites for interpolation
(Approximation: interpolation, fitting, projection, quasi-interpolation).
Representation-preserving changes¶
A central property: several operations change a geometry’s representation — its space and control points — while leaving the curve/surface/volume it describes unchanged (to round-off). They are the workhorses of refinement and inter-operability; see Piegl and Tiller [1997] for the underlying knot-insertion and degree-elevation algorithms:
Operation |
Method |
Effect on representation |
|---|---|---|
Knot insertion |
adds knots + control points; refines the net |
|
Knot removal |
removes knots where smoothness permits |
|
Degree elevation |
raises the degree, adds control points |
|
Degree reduction |
lowers the degree (approximate if exact reduction is impossible) |
|
Split |
cuts into two geometries at a parameter |
|
Restrict |
extracts the geometry over a sub-box |
|
Open / periodic |
converts between clamped and periodic forms |
Knot operations and Bézier extraction demonstrates the geometric invariance directly (evaluate before and after, compare).
Bézier extraction¶
Insert every interior knot up to multiplicity p and each element becomes an isolated
Bézier patch — a single polynomial in Bernstein form. This Bézier extraction is the
bridge between the smooth, globally-coupled spline basis and the element-local basis a
finite-element code assembles against [Borden et al., 2011, Scott et al., 2011].
to_beziers() returns the per-element Bézier pieces (as an
array of Bezier objects); to_bezier()
returns a single one when the geometry is already a single element. Conversely a
Bezier round-trips back with create_from_bezier().
The Bézier patches themselves are the subject of Bézier geometry and Bernstein root finding.
Element extraction operators¶
Advanced
The rest of this page is for code that assembles element matrices/vectors (IGA / FEM). If you only need geometry, you can stop here.
to_beziers() materializes Bézier geometries. When you
instead need the change-of-basis operator itself — to convert coefficients or to
pull element matrices between the spline basis and an element-local basis — use
SpanwiseElementExtraction. It builds the per-direction 1-D
operators once and applies the d-dimensional Kronecker product matrix-free, never
forming the full tensor product in memory.
Construction and targets¶
from pantr.bspline import SpanwiseElementExtraction
ext = SpanwiseElementExtraction(space, "bezier") # Bernstein/Bézier basis per element
ext = SpanwiseElementExtraction(space, "lagrange") # Lagrange basis at chosen nodes
ext = SpanwiseElementExtraction(space, "cardinal") # cardinal B-spline basis
Target |
Element-local basis |
|---|---|
|
Bernstein / Bézier on each element |
|
Lagrange at a |
|
cardinal (uniform) B-spline |
Construction is O(n_elements · p²) per direction and happens once; all later applies
reuse the cached operators.
Applying the operator¶
Let M be the extraction operator for one element. Four operation kinds cover the
bilateral pattern of element assembly, each with a single-cell and a batch (_many,
parallelized over cells) form:
Method |
Computes |
Typical use |
|---|---|---|
|
convert coefficients (spline → target) |
|
|
adjoint / dual conversion |
|
|
pull a target-basis element matrix back to the spline basis |
|
|
push a spline-basis element matrix to the target basis |
import numpy as np
n_in = int(np.prod(ext.input_shape_per_dir))
n_out = int(np.prod(ext.output_shape_per_dir))
v = np.ones(n_in)
y = ext.apply(v, cell=5) # one element, y.shape == (n_out,)
cells = np.arange(ext.num_total_intervals)
Y = ext.apply_many(np.ones((cells.size, n_in)), cells) # all elements at once
Pass a pre-allocated out= (and scratch=) array to avoid per-call allocation in
loops. A cell index may be a flat int (row-major over the elements) or a
per-direction tuple.
Identity short-circuit¶
On many elements an extraction operator is exactly the identity (e.g. an element that is
already a Bézier patch, or a cardinal interval). The operator detects these structurally
— no floating-point comparison — and skips the multiply.
is_identity,
is_identity_at(), and
num_identity_elements expose the result;
identity-heavy spaces also use a compact storage that saves memory.
Materializing operators¶
When you do want the dense matrix, operator()
assembles the (n_out, n_in) Kronecker product for one element and
tabulate() returns the full
(num_total_intervals, n_out, n_in) stack (this is what backs
to_beziers()). Prefer the matrix-free applies in hot loops.
Calling kernels from Numba¶
For code that is itself @njit-compiled, the operator stacks
(ops_1d,
is_identity_mask_1d, the compact arrays
and index maps) are plain read-only NumPy arrays, and the underlying Kronecker kernels in
pantr.bspline._extraction_kernels are importable Numba functions. The helper
make_struct_view() bundles every array + shape into a single
ExtractionStructView (a NamedTuple Numba can unbox) so you can
pass one argument instead of forwarding the pieces. See the
SpanwiseElementExtraction and
ExtractionStructView API entries for the full field list.
Where to go next¶
Element extraction in action (Bézier pieces of a curve): Knot operations and Bézier extraction.
The element-local bases themselves: Polynomial bases and change of basis.
Quadrature and grids to complete an assembly loop: Grids and quadrature.