Guide
This guide builds the StencilCore types by hand. In normal use you would obtain a stencil from StencilCalculus (by differentiation) and hand it to StencilAssembly (to assemble a matrix); here we look under the hood.
Lattice offsets
Offsets are type-level: a StaticShift is a normalized collection of StaticPair{D,O} (axis D, offset O). They support an algebra and print like lattice vectors, with ô the zero shift and ê₁ … ê₉ the unit shifts:
using StencilCore
-2ê₁ # StaticShift{Tuple{StaticPair{1,-2}}}
3ê₁ + ê₂ # StaticShift{Tuple{StaticPair{1,3}, StaticPair{2,1}}}
ê₁ - ê₁ # ô (the zero shift)Same-axis pairs are summed, zero offsets dropped, and the result is sorted by axis — so the representation is canonical.
The scalar algebra
A scalar — a timestep, a Reynolds number, a literal 2.0 — has no spatial extent. StencilCore models it directly with AbstractScalar{T} and a small CAS that mirrors the term-side one. Five concrete types:
Var{S,T}— a named substitution leaf.Sis aSymbol(the name);Tis the materialised element type. Declared with@var name T.Constant{T}— a literal leaf carrying itsval::T.Tis any concrete type —Number,SVector, anything. This is what numeric (and non-Number) literals canonicalise to at the operator boundary.Scalar{F,A,T}— an interior tree nodefn(args…).Null{T}/Unity{T}— type-level structural0/1.Unityrequiresone(T)defined (Number, squareSMatrix); its outer ctor routes an SVector through to its square Jacobian space.
The operator overloads build Scalar trees, with numeric literals canonicalising to Constant:
@var τ Float64
α = Constant(2.0)
τ * α + α # Scalar(+, (Scalar(*, (τ, α)), α))
typeof(τ * α + α) # Scalar{typeof(+), …, Float64}
eltype(τ * α + α) # Float64simplify post-walks a scalar tree with the same rule-rewriter shape as the term side. Identities are purely structural — Null + x → x, x * Unity → x (with an eltype-preservation gate), x * Null → Null — never inspecting .val. Folding combines values; it does not match on them.
simplify(Null{Float64}() + τ) # τ
simplify(τ * Unity{Float64}()) # τ
simplify(Constant(2.0) + Constant(3.0)) # Constant(5.0)
simplify(τ * Constant(1.0)) # stays a Scalar — numerical `1`
# is not a structural identitydifferentiate on a scalar tree returns an AbstractScalar whose eltype is the Jacobian of eltype(s) w.r.t. eltype(v). For Number variables the Jacobian is a Number; for SVector{N} variables it is the square SMatrix{N, N}. Mixed shape-classes are rejected at the top level.
@var τ Float64
differentiate(sin(τ) * τ, τ) # cos(τ)*τ + sin(τ)*1 simplified
differentiate(Constant(2.0), τ) # Null{Float64}() — no dependence
# Vector-valued: ∂(2x)/∂x lands in the matching SMatrix Jacobian space.
@var x SVector{2, Float64}
differentiate(2x, x)materialize reduces a scalar tree to a single value, substituting Var leaves from a NamedTuple:
materialize(τ * Constant(3.0), (τ = 4.0,)) # 12.0
materialize(Unity{SMatrix{2,2,Float64,4}}()) # the 2×2 identity matrixThese scalars become term coefficients once StencilCalculus wraps them in a Fill — see its docs for the bridge.
Generating methods from scalar expressions
@addmethod installs a typed Julia method whose body is derived from a scalar expression. Each Var argument declares both the argument name (from its S type parameter) and its declared type (from T):
using StencilCore
@var x Float64
@var y Int
foo = x ^ y
@addmethod myfun foo (y, x)
# equivalent to:
# function myfun(y::Int, x::Float64)
# x ^ y
# end
myfun(2, 3.0) # 9.0Because @addmethod adds an ordinary Julia method, subsequent calls add more dispatch methods to the same function — hence the name:
using StaticArrays
@var A SMatrix{2, 2, Float64, 4}
@var u SVector{2, Float64}
bar = A * u
@addmethod myfun bar (A, u)
# adds: function myfun(A::SMatrix{2,2,Float64,4}, u::SVector{2,Float64}); A * u; end
# Both methods coexist:
myfun(2, 3.0) # 9.0 (first method)
myfun(SMatrix{2,2}(1,0,0,2), SVector(1.0, 3.0)) # [1.0, 6.0] (second method)The argument list is a tuple (@addmethod fname expr (v1, v2, ...)), and the order determines the method signature. Every listed argument must be a Var; @addmethod raises an ArgumentError at call time if:
- any
Varin the expression is not listed as an argument (free variable), - an element in the argument list is not a
Var, - the same name appears more than once, or
- the expression calls
fnameitself — the generated method would recurse infinitely (this is why, e.g.,getindexcannot be defined by indexing the same argument; the bodyc[i]lowers togetindex(c, i)).
Widening the emitted signature
The expression is built and validated at the Vars' concrete types, which keeps simplify and differentiate fully type-inferred. To emit a method that accepts a wider family of inputs, pass a NamedTuple of type overrides instead of the Var list — only the generated signature widens, the body is unchanged:
@var x Float64
@var y Int
foo = x ^ y
@addmethod myfun foo (y = Integer, x = AbstractFloat)
# adds: function myfun(y::Integer, x::AbstractFloat); x ^ y; end
myfun(big(3), 2.0f0) # works for any Integer / AbstractFloatEach override must be a supertype of the Var's declared type (widening only). Note that StaticArrays element parameters are invariant (SVector{2,Float64} <: SVector{2,Number} is false), so shape widening uses the UnionAll form:
@var V SVector{2, Float64}
vv = V + V
@addmethod vfun vv (V = SVector{2, <:Number},)
vfun(SVector(1, 2)) # [2, 4]Linear and star stencils
A LinearStencil has contiguous offsets along one mesh axis. Its coefficient is one SVector per cell, holding every per-offset coefficient on that column in ascending-offset order:
using StencilCore, StaticArrays
n = 5
# offsets 0,1 along axis 1; coefficients (-1, 1) at every column.
fwd = LinearStencil{1}(SUnitRange(0, 1), fill(SVector(-1.0, 1.0), n))A StarStencil is the N-D star with symmetric reach −L … +L per axis, stored interlaced: a single SVector{2NL+1} per cell, in reverse-lex offset order, with the diagonal as the explicit middle slot — so a free diagonal term (Helmholtz, an unsteady term) has a home.
n1, n2 = 5, 4
# 2-D five-point Laplacian (L = 1 ⇒ M = 5):
# (axis2,−1), (axis1,−1), diagonal, (axis1,+1), (axis2,+1).
lap = StarStencil{1}(fill(SVector(-1.0, -1.0, 4.0, -1.0, -1.0), n1, n2))The general stencil and narrowing
Stencil is the lingua franca: an explicit reverse-lex list of StaticShift offsets plus a matching SVector coefficient. It is not assembled directly — as_linear / as_star narrow it to an assemblable type by matching the offset pattern and reusing the coefficient verbatim:
st = Stencil(ColumnAccess, (-2ê₁, -ê₁, ô), fill(SVector(1.0, -4.0, 3.0), n))
ln = as_linear(st) # LinearStencil{1, -2, 3, …}
ln.term === st.term # true — verbatim copyA Stencil whose offsets form the canonical star pattern narrows with as_star instead; mismatched patterns raise an ArgumentError.