Efficient usage of the Unified Form Language#
As we discussed in Code generation DOLFINx generates code once the user has specified the variational form. This process can be somewhat time consuming, as generating, compiling and linking the code can take time. We start by setting up a simple unit square and a first order Lagrange space.
from mpi4py import MPI
import numpy as np
import dolfinx
import dolfinx.fem.petsc
import ufl
mesh = dolfinx.mesh.create_unit_square(MPI.COMM_WORLD, 30, 30)
V = dolfinx.fem.functionspace(mesh, ("Lagrange", 1))
Problem statement#
Let us consider a simple heat equation,
In this equation, there are several time dependent and spatially varying coefficients.
With a naive implementation of this code in DOLFINx, one would define the variational formulation inside the
temporal loop, and re-compile the variational form for each time step.
In DOLFINx, a form is compiled whenever one calls dolfinx.fem.form
, or initialize a dolfinx.fem.petsc.LinearProblem
.
Time-dependent constants#
To be able to use adaptive time stepping, we define dt
as a dolfinx.fem.Constant
, such that we can re-assign values to the
constant without having to re-compile the code.
dt = dolfinx.fem.Constant(mesh, 0.01)
It is easy to re-assign a value to the constant by calling
dt.value = 0.005
Similarly, we can define the diffusive coefficient k
such as
def k_func(t):
return 0.1 if t < 0.5 else 0.05
k = dolfinx.fem.Constant(mesh, 0.1)
t = dolfinx.fem.Constant(mesh, 0.0)
while t.value < 1:
# Update t
t.value += dt.value
# Update k
k.value = k_func(t.value)
# We reset t
t.value = 0
Conditional values#
Next, we define the spatial and temporal source term using a ufl.conditional
We start by defining the spatially varying parameters of the mesh
x, y = ufl.SpatialCoordinate(mesh)
Next, we create each component of the conditional statement
condition = ufl.lt(x, 0.5)
true_statement = 0.4 * y
false_statement = 0.5 * t
f = ufl.conditional(condition, true_statement, false_statement)
Inside the compile code, this condition will be evaluated at every quadrature point.
Now we have defined all varying functions outside the time loop, and each of them can be updated by updating the constant values
Time-derivatives#
We use a simple backward Euler time stepping scheme to solve the problem.
u = ufl.TrialFunction(V)
u_n = dolfinx.fem.Function(V)
dudt = (u - u_n) / dt
Full variational formulation#
With all the definitions above we can write the full variational form as \(F(u, v)\)
v = ufl.TestFunction(V)
dx = ufl.Measure("dx", domain=mesh)
F = dudt * v * dx + k * ufl.inner(ufl.grad(u), ufl.grad(v)) * dx - f * v * dx
Automatic extraction of the linear and bi-linear form#
We can exploit UFL to extract the linear and bi-linear components of the variational form
a, L = ufl.system(F)
Explicit code generation#
We generate and compile the C code for these expressions using dolfinx.fem.form
a_compiled = dolfinx.fem.form(a)
L_compiled = dolfinx.fem.form(L)
Initial conditions#
We generate the initial condition by using lambda expressions
def u_init(x, sigma=0.1, mu=0.3):
"""
The input function x is a (3, number_of_points) numpy array, which is then
evaluated with the vectorized numpy functions for efficiency
"""
return (
1.0
/ (2 * np.pi * sigma)
* np.exp(-0.5 * ((x[0] - mu) / sigma) ** 2)
* np.exp(-0.5 * ((x[1] - mu) / sigma) ** 2)
)
u_n.interpolate(u_init)
Setting up the linear solver#
As we have now defined a
and L
, we are ready to set up a solver for the arising linear system.
We use PETSc for this, and a direct solver.
uh = dolfinx.fem.Function(V)
petsc_options = {
"ksp_type": "preonly",
"pc_type": "lu",
"pc_factor_mat_solver_type": "mumps",
}
problem = dolfinx.fem.petsc.LinearProblem(a_compiled, L_compiled, u=uh, bcs=[], petsc_options=petsc_options)
The temporal loop#
For each temporal step, we update the time variable and call the solve
command that re-assemble the system
T = 1
while t.value < T:
t.value += dt.value
k.value = k_func(t.value)
problem.solve()
# Update previous solution
u_n.x.array[:] = uh.x.array
Note
Note that there is no definitions of variables within the temporal loop. There is simply copying of data or accumulation of data from the previous time step. This is a very efficient way of solving time-dependent problems in DOLFINx.