Cubic Adjoint (1D)
The CubicAdjoint operator computes $\bar{\mathbf{f}} = W^\top \bar{\mathbf{y}}$ for 1D cubic spline interpolation — mapping query-space sensitivities back to grid-space.
It is query-baked and data-free: constructed from grid x and query points xq only. The same operator can be applied to any $\bar{\mathbf{y}}$ vector.
Quick Start
using FastInterpolations
using LinearAlgebra: dot
x = collect(range(0, 1, 50))
xq = [0.1, 0.25, 0.5, 0.75, 0.9]
f = sin.(2π .* x)
# Build the adjoint operator (grid + queries only, no data)
adj = cubic_adjoint(x, xq)CubicAdjoint{Float64}
├─ Grid: Vector, 50 points ∈ [0, 1]
├─ Query: 5 points
└─ BC: CubicFit# Apply to a sensitivity vector
ȳ = collect(1.0:length(xq))
f̄ = adj(ȳ)50-element Vector{Float64}:
0.00018469921830076248
-0.0014248225411773103
0.0064117014352978955
-0.024644152841844597
0.09313852432946604
0.9791428267292088
-0.06670983124630073
0.0166964982559946
-7.616177767766135e-5
-0.016391851145283968
⋮
-0.014594242814458095
0.0872909694569015
-0.3345696350131474
4.895987570595675
0.46561935263044585
-0.12320116705522254
0.03205340855933533
-0.007122979679852251
0.0009233492177586249Verify correctness via the dot-product identity $\langle W f, \bar{y} \rangle = \langle f, W^\top \bar{y} \rangle$:
itp = cubic_interp(x, f)
Wf = itp(xq) # forward: W·f
WTy = adj(ȳ) # adjoint: Wᵀ·ȳ
@assert dot(Wf, ȳ) ≈ dot(f, WTy)
println("⟨Wf, ȳ⟩ = ", dot(Wf, ȳ))
println("⟨f, Wᵀȳ⟩ = ", dot(f, WTy))⟨Wf, ȳ⟩ = -4.351140062672254
⟨f, Wᵀȳ⟩ = -4.351140062672254Constructor
cubic_adjoint(x, x_query; bc=CubicFit()) -> CubicAdjointx: Grid points (sortedAbstractVector)x_query: Query points (baked into the operator)bc: Any boundary condition — all BC types are supported includingPeriodicBC
See Adjoint API Reference for full docstrings.
Calling the Operator
Allocating
f̄ = adj(ȳ) # value adjoint
f̄ = adj(ȳ; deriv=DerivOp(1)) # 1st derivative adjoint
f̄ = adj(ȳ; deriv=DerivOp(2)) # 2nd derivative adjointIn-Place (Zero Allocation)
adj(f̄, ȳ) # zeros f̄, then accumulates
adj(f̄, ȳ; deriv=DerivOp(1)) # derivative adjoint, in-placef̄_buf = zeros(length(x))
adj(f̄_buf, ȳ)
f̄_buf[1:5]5-element Vector{Float64}:
0.00018469921830076248
-0.0014248225411773103
0.0064117014352978955
-0.024644152841844597
0.09313852432946604Size
size(adj) # (n_grid, n_query)(50, 5)Matrix Materialization
For verification and debugging, you can materialize the full weight matrix. This is $O(n \times m)$ and not intended for production use.
Wᵀ = Matrix(adj)
size(Wᵀ) # (n_grid, n_query)(50, 5)@assert Wᵀ * ȳ ≈ adj(ȳ)
println("Matrix × ȳ ≈ adj(ȳ): ", isapprox(Wᵀ * ȳ, adj(ȳ)))Matrix × ȳ ≈ adj(ȳ): trueThe transpose gives the forward interpolation matrix:
W = Matrix(adj)'
@assert W * f ≈ itp(xq)
println("W * f ≈ itp(xq): ", isapprox(W * f, itp(xq)))W * f ≈ itp(xq): trueA convenience method builds the forward matrix directly from an interpolant:
W_direct = Matrix(itp, xq)
@assert W_direct * f ≈ itp(xq)
println("Matrix(itp, xq) * f ≈ itp(xq): ", isapprox(W_direct * f, itp(xq)))Matrix(itp, xq) * f ≈ itp(xq): trueFor BCs with non-zero values (e.g., Deriv1(0.5)), the forward operator is affine: $y = W f + c$. The adjoint computes $W^\top \bar{y}$ — the constant $c$ does not appear.
Performance
- Construction: $O(n + m \log n)$ — Thomas factorization + anchor binary search (amortized via
autocache) - Apply (per call): $O(n + m)$ — one transpose solve + one scatter pass
- In-place: Zero allocation after construction
- Cache sharing: Reuses the same
CubicSplineCacheas forward interpolation
See Also
- Adjoint Overview: Concepts and mathematical background
- Adjoint via AD: Using AD backends for
∂f/∂y - Cubic Interpolation: Forward cubic spline API
- AD Support: General AD support for
∂f/∂x