Partial differential equations
The Surfacefun package is capable of solving general variable-coefficient, second-order, linear, elliptic partial differential equations on open and closed surfaces. Consider such a PDE on a given surface \(\Gamma\),
where \(\mathcal{L}_\Gamma\) is an elliptic partial differential operator of the form
with smooth coefficients \(a_{ij}\), \(b_i\), and \(c\). Here, we identify \(\partial_1^\Gamma\), \(\partial_2^\Gamma\), \(\partial_3^\Gamma\) with the Cartesian \(x\)-, \(y\)-, and \(z\)-components of the surface gradient, respectively. If \(\Gamma\) is not a closed surface, the PDE may also be subject to boundary conditions, e.g., \(u(\boldsymbol{x}) = g(\boldsymbol{x})\) for \(\boldsymbol{x} \in \partial\Gamma\) and some function \(g\).
Partial differential operators in Surfacefun are specified as MATLAB structs
with properties defining the coefficients on each term appearing in
\(\mathcal{L}_\Gamma\). The coefficients on the second-order terms are
specified by setting pdo.dxx
, pdo.dyy
, pdo.dzz
, pdo.dxy
,
pdo.dyx
, pdo.dyz
, pdo.dzy
, pdo.dxz
, and pdo.dzx
.
Coefficients on the first-order terms can be set via pdo.dx
, pdo.dy
,
and pdo.dz
. The zeroth-order coefficient is specified via pdo.c
. Each
coefficient may be a constant, a function handle, or a surfacefun
. For
instance, the Laplace-Beltrami operator can be specified via
pdo = [];
pdo.dxx = 1;
pdo.dyy = 1;
pdo.dzz = 1;
or more simply as
pdo = [];
pdo.lap = 1;
This sets the coefficients on the terms \(\partial_x^\Gamma \partial_x^\Gamma\), \(\partial_y^\Gamma \partial_y^\Gamma\), and \(\partial_z^\Gamma \partial_z^\Gamma\) to one and the rest to zero.
Let’s solve a simple Laplace–Beltrami problem on the surface of the sphere. Since the spherical harmonics are eigenfunctions of the Laplace–Beltrami operator on the sphere, we can construct a test problem analytically:
% Make a sphere mesh
p = 16;
nref = 2;
dom = surfacemesh.sphere(p+1, nref);
% Construct the true solution and corresponding righthand side
l = 3; m = 2;
sol = spherefun.sphharm(l, m);
sol = surfacefun(@(x,y,z) sol(x,y,z), dom);
f = -l*(l+1)*sol;
% Specify the partial differential operator to be Laplace-Beltrami
pdo = [];
pdo.lap = 1;
To solve the PDE, we create a surfaceop
. The surfaceop
object
encapsulates the factorization and solution routines corresponding to the fast
direct solver in [1].
L = surfaceop(dom, pdo, f);
It turns out that the Laplace–Beltrami problem on a closed surface is rank deficient (by one). However, it is uniquely solvable under the mean-zero conditions
We can impose this condition by setting
L.rankdef = true;
Now we can solve the PDE:
u = L.solve();
plot(u)
Let’s check the error:
norm(u-sol)
ans = 6.209136661992651e-12
Surface PDEs on surfaces of arbitrary genus may be solved using surfaceop
.
For example, here is the solution to a variable-coefficient surface Helmholtz
equation on a genus-1 stellarator geometry:
% Construct the stellarator geometry
p = 16; nu = 8; nv = 24;
dom = surfacemesh.stellarator(p+1, nu, nv);
% Variable-coefficient surface Helmholtz equation
pdo = [];
pdo.lap = 1;
pdo.c = @(x,y,z) 300*(1-z);
f = -1;
L = surfaceop(dom, pdo, f);
u = L.solve();
plot(u), colorbar
Now let’s solve a problem on an open surface. We’ll create an open surface by extracting a subset of the patches from a closed surface:
rng(0)
p = 16;
nref = 2;
dom = surfacemesh.blob(p+1, nref);
dom = surfacemesh(dom.x(1:16), dom.y(1:16), dom.z(1:16));
plot(dom), view(-110, 30), camlight
We construct a surfaceop
on an open surface in the same way as on a closed
surface, except now the L.solve()
method requires Dirichlet boundary data to
be passed as an argument:
% Specify the righthand side and Dirichlet boundary data
f = -1;
bc = 0;
pdo = [];
pdo.lap = 1;
L = surfaceop(dom, pdo, f);
u = L.solve(bc);
plot(u), view(-110, 30), colorbar
Modifying an existing surfaceop
A surfaceop
is a direct solver for the specified surface PDE. This means
that once a factorization of the problem is constructed, the factorization may
be reused to solve the same PDE with different righthand sides and boundary data
in a manner that is more efficient than creating a new surfaceop
again and
again.
To this end, a surfaceop
may be factorized before it is given any data. This
is performed implicitly when L.solve()
is called, but may be performed
explicitly by calling L.build()
:
L = surfaceop(dom, pdo);
L.build();
The surfaceop
can now be given any righthand side and will efficiently
update its factorization accordingly:
L.rhs = @(x,y,z) sin(x.*y);
u = L.solve(bc);
plot(u), view(-110, 30), colorbar
The surfaceop
object is agnostic to boundary data. This means that an
existing solver may be used with any Dirichlet boundary data by simply passing
it to L.solve()
:
bc = @(x,y,z) z;
u = L.solve(bc);
plot(u), view(-110, 30), colorbar