SimpleSplines.jl

B-spline finite elements on an interval, built from the Cox-de Boor recursion, with the quadrature and assembly a Galerkin discretisation needs.

The package is deliberately small: one family of bases, one assembly table, and the mass, stiffness, derivative and variable-coefficient matrices as single weighted contractions of that table. What it is not is a curve- and surface-modelling library — there are no NURBS, no knot insertion, no degree elevation and no least-squares fitting of data. What it is for is discretising a differential equation and then solving with the result.

What it provides

Basesclamped (BSplineBasis), periodic (PeriodicBSplineBasis), and recombined (RecombinedBSplineBasis) — of any degree $p \ge 0$
MeshesUniformMesh, GradedMesh, RandomMesh, GeneralMesh
Boundary conditionsFree, Dirichlet, Neumann, Natural, Robin, and the general Constraint — per end, plus Periodic for the whole basis
AssemblySplineQuadrature and, from it, mass_matrix, stiffness_matrix, derivative_matrix, mixed_matrix, weighted_matrix, basis_integrals
Mass solvesCirculantMass (FFT), BandedMass (banded Cholesky), FactorizedMass (sparse Cholesky), KroneckerMass (factored, $D$-dimensional)
Tensor productsTensorProductBasis and TensorProductQuadrature in any number of dimensions, with degree, mesh, domain and boundary condition per axis
FunctionsSpline, SplineDerivative, l2_projection
Particle pathfindcell and evaluate_all!, allocation-free, for depositing onto the basis

Installation

using Pkg
Pkg.add(url = "https://github.com/JuliaDEC/SimpleSplines.jl")

In thirty seconds

A basis is a Mesh, a degree, and a BoundaryCondition:

using SimpleSplines

b = BSplineBasis(UniformMesh(16, 0 .. 1), 3, Dirichlet())
nbasis(b), degree(b), order(b), polynomial_reproduction(b)
(17, 3, 4, -1)

Assembly goes through a SplineQuadrature, which tabulates the basis and its derivatives at the global Gauß-Legendre points and hands back every matrix built on them:

q = SplineQuadrature(b)
M = mass_matrix(q)
K = stiffness_matrix(q)
size(M), size(K)
((17, 17), (17, 17))

Fitting a function to the space is an $L^2$ projection, and the result is callable:

u = Spline(b, l2_projection(q, x -> sin(π * x)))
u(0.5), u(0.5, 1), u(0.0)        # value, first derivative, and the imposed u(0) = 0
(1.0000020843436348, 8.881784197001252e-16, 0.0)

Solving $-u'' = f$ with $u(0) = u(1) = 0$ is then two lines, because the recombined basis has already taken care of the boundary condition:

f(x) = π^2 * sin(π * x)
rhs = basis_values(q, 0) * (quadrature_weights(q) .* f.(quadrature_nodes(q)))
û = Matrix(K) \ rhs
maximum(abs(evaluate(b, û, x) - sin(π * x)) for x in range(0, 1; length = 101))
2.081776101725552e-6

How the pieces fit

Mesh + BoundaryCondition
        │
        ▼
AbstractBSplineBasis  ───▶  Spline        ───▶  s(x),  derivative(s)
        │
        ▼
SplineQuadrature      ───▶  mass_matrix, stiffness_matrix, derivative_matrix,
        │                   mixed_matrix, weighted_matrix, basis_integrals,
        │                   l2_projection
        ▼
MassOperator          ───▶  mass_solve!,  op \ x

The mesh is geometry alone and carries no boundary condition; the boundary condition decides which of the three bases the one constructor BSplineBasis(mesh, p, bc) returns; the basis decides which representation the mass matrix takes. A tensor product is the same picture with a tuple in each box.

Where to go next

CompactBasisFunctions.jl provides the Basis hierarchy these bases join, along with the Lagrange, Chebyshev, Legendre and Bernstein families; QuadratureRules.jl provides the Gauß-Legendre nodes and weights; and GeometricBase owns the shared accessors basis, degree, nodes, nnodes and order, so that one generic function per accessor is extended across the ecosystem rather than one defined per package.