PDE solving example

In this section we solve the following PDE problem with a spline grid:

\[\begin{align*} \begin{cases} u : \Omega \rightarrow \mathbb{R} \\ \Delta u = u^3 &\text{ for }& \mathbf{x} \in \Omega \\ u = g(\mathbf{x}) \; &\text{ for }& \mathbf{x} \in \partial\Omega \end{cases} \end{align*},\]

where the domain is given by the open unit cube: $\Omega = (0,1)^3$. We assume that $g(x,y,z) = 0$ for $z \in (0,1)$ and $g(x,y,z) = f(x,y)$ for $z = 0, 1$.

We solve this problem by sampling the domain and enforcing the PDE on the interior points and the boundary condition on the boundary points.

The residual kernel

We first define the kernel which calculates the residual of a given approximation to the solution of the problem above. Note that this kernel is agnostic of the fact that the solution will come from a spline grid.

using KernelAbstractions

@kernel function pde_residual_kernel(
        residual,
        @Const(f),
        @Const(u),
        @Const(∂₁²u),
        @Const(∂₂²u),
        @Const(∂₃²u)
)
    I = @index(Global, Cartesian)

    is_boundary = false
    for (i, i_max) in zip(Tuple(I)[1:(end - 1)], size(residual)[1:(end - 1)])
        if (i == 1) || (i == i_max)
            is_boundary = true
            break
        end
    end

    residual[I] = if is_boundary
        if I[3] == 1 || I[3] == size(residual)[3]
            u[I] - f[I[1], I[2]]
        else
            u[I]
        end
    else
        ∂₁²u[I] + ∂₂²u[I] + ∂₃²u[I] - u[I]^3
    end
end
pde_residual_kernel (generic function with 4 methods)

The residual as a function of the spline grid control points

Whe now define a function which generates the input of the above residual kernel from a spline grid and computes the residual in place. It assumes a parameter object with the spline grid, f (an array that specifies the boundary condition for the bottom and top of the domain, the boundary condition is 0 elsewhere) and caches for the spline grid evaluation.

function pde_residual!(residual, control_points_flat, p)::Nothing
    (; spline_grid, f, u, ∂₁²u, ∂₂²u, ∂₃²u) = p

    control_points = reshape(
        control_points_flat,
        size(spline_grid.control_points)
    )

    evaluate!(spline_grid; control_points, eval = u)
    evaluate!(spline_grid; control_points, eval = ∂₁²u, derivative_order = (2, 0, 0))
    evaluate!(spline_grid; control_points, eval = ∂₂²u, derivative_order = (0, 2, 0))
    evaluate!(spline_grid; control_points, eval = ∂₃²u, derivative_order = (0, 0, 2))

    backend = get_backend(u)
    pde_residual_kernel(backend)(
        reshape(residual, size(u)),
        f,
        u,
        ∂₁²u,
        ∂₂²u,
        ∂₃²u,
        ndrange = size(u)
    )
    synchronize(backend)
    return nothing
end
pde_residual! (generic function with 1 method)

Defining the spline grid

We would like to define a residual function $\mathbb{R}^N \rightarrow \mathbb{R}^N$, i.e. the number of sample points must equal the number of control points. For each dimension the maximum derivative order is 2, to evaluate the result of the Laplacian operator on the spline.

using SplineGrids

n_control_points = (25, 25, 25)
degree = (2, 2, 2)
n_sample_points = n_control_points
dim_out = 1

spline_dimensions = SplineDimension.(
    n_control_points, degree, n_sample_points; max_derivative_order = 2)
spline_grid = SplineGrid(spline_dimensions, dim_out)
spline_grid
SplineGrid volume with outputs in ℝ¹ (Float32)
----------------------------------------------
* Properties per dimension:
┌─────────────────┬────────┬───────────────────┬─────────────────┐
│ input dimension │ degree │ # basis functions │ # sample points │
├─────────────────┼────────┼───────────────────┼─────────────────┤
│               1 │      2 │                25 │              25 │
│               2 │      2 │                25 │              25 │
│               3 │      2 │                25 │              25 │
└─────────────────┴────────┴───────────────────┴─────────────────┘
* Control points:
DefaultControlPoints for grid of size (25, 25, 25) in ℝ¹ (Float32).

Defining the boundary condition

We define a boundary condition inspired by the Julia logo.

using GLMakie

function hill(x, y, x0, y0, β)
    r = sqrt((x - x0)^2 + (y - y0)^2)
    return exp(-(r / β)^2)
end

function hills(x, y)
    R = 0.25
    out = 0.0
    for θ in range(0, 2π, length = 4)[1:(end - 1)]
        x0 = 0.5 + R * cos(θ)
        y0 = 0.5 + R * sin(θ)
        out += hill(x, y, x0, y0, R / 2)
    end
    return 100 * out
end

f = [hills.(x, y)
     for
     x in spline_dimensions[1].sample_points,
y in spline_dimensions[2].sample_points]

heatmap(f)
Example block output

Defining the problem

We will solve the problem with a Jacobian free Newton-Krylov method. To do this, we need to provide a Jacobian-vector product (JVP) function.

using NonlinearSolve
using Enzyme

p = (;
    spline_grid,
    f,
    u = similar(spline_grid.eval),
    ∂₁²u = similar(spline_grid.eval),
    ∂₂²u = similar(spline_grid.eval),
    ∂₃²u = similar(spline_grid.eval)
)

meta_p = (;
    residual = similar(spline_grid.eval),
    p_duplicated = DuplicatedNoNeed(p, make_zero(p))
)

function pde_residual_jvp!(Jv, v, control_points_flat, meta_p)::Nothing
    for val in values(meta_p.p_duplicated.dval)
        make_zero!(val)
    end
    autodiff(
        Forward,
        pde_residual!,
        DuplicatedNoNeed(vec(meta_p.residual), Jv),
        Duplicated(control_points_flat, v),
        meta_p.p_duplicated
    )
    return nothing
end

nonlinear_function = NonlinearFunction(
    (residual, control_points_flat, p_) -> pde_residual!(residual, control_points_flat, p);
    jvp = pde_residual_jvp!
)

# Initial guess
x0 = zeros(Float32, length(spline_grid.control_points))

problem = NonlinearProblem(
    nonlinear_function,
    x0,
    meta_p
)
NonlinearProblem with uType Vector{Float32}. In-place: true
u0: 15625-element Vector{Float32}:
 0.0
 0.0
 0.0
 0.0
 0.0
 0.0
 0.0
 0.0
 0.0
 0.0
 ⋮
 0.0
 0.0
 0.0
 0.0
 0.0
 0.0
 0.0
 0.0
 0.0

Solving the problem

sol = solve(problem, NewtonRaphson(linsolve = KrylovJL_GMRES()))
retcode: MaxIters
u: 15625-element Vector{Float32}:
 7.203033f-5
 0.00012896715
 0.00088540546
 0.004786132
 0.020288704
 0.06699376
 0.17124008
 0.33741593
 0.512363
 0.6007665
 ⋮
 0.0013418678
 0.00020978265
 3.4804158f-5
 1.1672109f-5
 6.2266195f-6
 2.9949101f-6
 1.1428368f-6
 3.4744426f-7
 2.0612349f-7

Viewing the solution

spline_grid.control_points .= reshape(sol.u, size(spline_grid.control_points))
evaluate!(spline_grid)
fig = Figure()
ax, plt = volume(fig[1, 1], log.(spline_grid.eval[:, :, :, 1] .+ 1))
Colorbar(fig[1, 2], plt; label = L"\log(u + 1)")
fig
Example block output