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

create_uniform_open_knots()

an open (clamped) uniform knot vector

endpoints have multiplicity p+1 so the curve is clamped to its end control points

create_uniform_periodic_knots()

a periodic uniform knot vector

wraps smoothly; for closed curves/surfaces

create_cardinal_knots()

a cardinal (uniform B-spline) knot vector

shift-invariant interior basis

create_uniform_space()

a whole tensor-product BsplineSpace

per-direction degree and num_intervals; the one-call convenience

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

insert_knots()

adds knots + control points; refines the net

Knot removal

remove_knots()

removes knots where smoothness permits

Degree elevation

elevate_degree()

raises the degree, adds control points

Degree reduction

reduce_degree()

lowers the degree (approximate if exact reduction is impossible)

Split

split()

cuts into two geometries at a parameter

Restrict

restrict()

extracts the geometry over a sub-box

Open / periodic

to_open_bspline() / to_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

"bezier"

Bernstein / Bézier on each element

"lagrange"

Lagrange at a LagrangeVariant node set (lagrange_variant=…)

"cardinal"

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

apply()

y = M v

convert coefficients (spline → target)

apply_transpose()

y = Mᵀ v

adjoint / dual conversion

apply_MT_K_M()

B = Mᵀ K M

pull a target-basis element matrix back to the spline basis

apply_M_K_MT()

B = M K Mᵀ

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