Conventions
This page collects the main conventions used throughout UnifiedSparseGrids.jl.
Refinement indices in 1D axis families
A 1D axis family is indexed by a refinement index $r = 0, 1, 2, \ldots$. For nodal axis families this defines a sequence of point sets
\[X_0 \subset X_1 \subset X_2 \subset \cdots,\]
with incremental blocks
\[\Delta X_r = X_r \setminus X_{r-1},\qquad r \ge 0,\]
and API accessors:
points(axis, r)returns $X_r$,newpoints(axis, r)returns $\Delta X_r$,totalsize(axis, r)returns $|X_r|$,blocksize(axis, r)returns $|\Delta X_r|$.
The old names npoints / delta_count still exist for nodal axes, but totalsize / blocksize are the preferred concepts.
Endpoint handling at $r = 0$
Many nodal axis families expose an endpoints setting. In this package it is interpreted as a 2-bit mask describing which endpoints are kept at $r = 0$:
endpoints=:nonekeeps no endpoints,endpoints=:leftkeeps only the left endpoint,endpoints=:rightkeeps only the right endpoint,endpoints=:bothkeeps both endpoints.
Equivalently, you may pass endpoints=false/true, or Val(mask) with $mask \in 0:3$.
With this convention, refinement index $r = 0$ is exactly the set of kept endpoints (possibly empty), and interior refinement starts at $r = 1$.
Local-support bases and boundary points
For local-support dyadic bases such as HatBasis() and PiecewisePolynomialBasis(), the package follows the rule:
Boundary degrees of freedom exist if and only if boundary points exist.
So Dirichlet-style discretizations are typically expressed by choosing an axis family with endpoints=:none, not by switching to a separate "interior-only" basis type.
Axis families vs. index sets
A sparse grid is determined by two independent ingredients:
- a tuple of 1D axis families, which define what one refinement index $r$ means in each dimension, and
- a downward-closed multi-index set $I \subset \mathbb{N}_0^D$, which decides which refinement vectors are active.
Therefore the approximation meaning of an index set depends on the chosen axis families. For example, the same FullTensorIndexSet can represent a box in nested nodal refinements, one-point-at-a-time Leja growth, or something else in a future modal backend.
For iteration and planning, use refinement_caps(grid) (or refinement_caps(indexset)) to obtain the per-dimension bounds used by the recursive engine.
Smolyak index sets
SmolyakIndexSet(D, L) is the classical isotropic Smolyak set in refinement-index coordinates:
\[r \in \mathbb{N}_0^D,\qquad \sum_{j=1}^D r_j \le L,\qquad r \le \mathrm{cap}.\]
Mapping to the classical 1-based notation, $r_{\mathrm{old}} = r + 1$ and $q_{\mathrm{old}} = L + 1$ give
\[\|r_{\mathrm{old}}\|_1 \le q_{\mathrm{old}} + D - 1.\]
A weighted variant is provided by WeightedSmolyakIndexSet(D, L, weights):
\[r \in \mathbb{N}_0^D,\qquad \sum_{j=1}^D \theta_j r_j \le L,\qquad r \le \mathrm{cap},\]
with positive integer weights $\theta_j = \mathrm{weights}[j]$. This is the natural place to encode anisotropy such as parabolic space-time scaling.
Full tensor index sets
FullTensorIndexSet(D, R) is a refinement-index box:
\[r \in \mathbb{N}_0^D,\qquad 0 \le r_j \le \mathrm{cap}_j.\]
If cap is omitted, all dimensions use the isotropic bound $R$.
Matrix-family convention for line operators
For LineDiagonalOp(f) and LineBandedOp(f), the user-supplied family function must have the signature
f(n, T)where:
nis the 1D size of the active fiber (for example a modal count or matrix dimension), andTis the desired scalar element type.
The function should be a top-level function, not a closure.
The contract is:
LineDiagonalOp(f):f(n, T)returns the diagonal data itself, as a vector-like container of lengthn.LineBandedOp(f):f(n, T)returns a squareBandedMatrices.AbstractBandedMatrixof sizen × n. The recommended concrete return type isBandedMatrix.
The current implementation caches only the largest-level object and constructs smaller levels by taking prefixes:
LineDiagonalOp: the firstmentriesd[1:m],LineBandedOp: the leadingm × mprincipal banded view.
So both families must be prefix-compatible across n: for every smaller size m, the object returned by f(m, T) must agree with the corresponding prefix of f(n, T) built at the largest cached size.
Thread-safety contract for line plans
lineplan(op, axis, rmax, T) returns a plan vector planvec, and the unidirectional engine passes planvec[r+1] into apply_line!. Planned families are typically constructed via make_plan_shared(op, axis, rmax, T) and make_plan_entry(op, axis, n, r, T, shared) with n = totalsize(axis, r).
The project assumes the following contract:
- Plan entries must be read-only during application.
- Plans must not own mutable scratch buffers (no internal
work,tmp, etc.). - All scratch memory is supplied by the caller via the
workargument ofapply_line!.
This enables safe multithreading: plans can be shared across inner worker tasks, and only caller-owned scratch buffers are needed during application. For the execution backends, automatic backend selection, and workspace ownership model, see Multi-threading.