Best way to apply time dependent Dirichlet boundary conditions using dolfinx.fem.petsc NonlinearProblem

Hello,

I wonder whether there is a more efficient way to implement Dirichlet boundary conditions using dolfinx.fem.petsc NonlinearProblem. Especially, i think that creating a NonlinearProblem within the time stepping loop is not a good way (see code below). However, since only at the call of NonlinearProblem the parameter bcs=Dirichlet_bcs is handed over, i don’t know how to update it otherwise.

Thank you in advance for your help.

Cheers

Domi

import numpy as np
from mpi4py import MPI
from dolfinx import mesh
from dolfinx.io import XDMFFile
from dolfinx.fem import functionspace, Constant, Function, locate_dofs_topological, dirichletbc, Expression
from dolfinx.fem.petsc import NonlinearProblem
from dolfinx.nls.petsc import NewtonSolver
from basix.ufl import element
from ufl import TestFunction, dx, grad, inner
from petsc4py import PETSc

# 3d geometry parameters
box_length = 1
cells_number_per_dim = 9

# time stepping
t = 0
t_final = 10
dt_init = 1e-3

def generate_mesh(box_length, cells_number_per_dim):
    msh = mesh.create_box(MPI.COMM_WORLD,
                          [[-box_length/2, -box_length/2, -box_length/2], [box_length/2,box_length/2, box_length/2]],
                          [cells_number_per_dim, cells_number_per_dim, cells_number_per_dim])
    return msh

msh = generate_mesh(box_length, cells_number_per_dim)

CG1_elem = element("Lagrange", msh.basix_cell(), 1)

V = functionspace(msh, CG1_elem)

dt = Constant(msh, dt_init)

T_test = TestFunction(V)
T = Function(V)
T_ = Function(V)

tdim = msh.topology.dim
fdim = tdim - 1
msh.topology.create_connectivity(fdim, tdim)

boundary_conditions = [
    (lambda x: np.isclose(x[0], -box_length/2), 'dof_bdry_1'),
    (lambda x: np.isclose(x[0],  box_length/2), 'dof_bdry_2'),
    (lambda x: np.isclose(x[1], -box_length/2), 'dof_bdry_3'),
    (lambda x: np.isclose(x[1],  box_length/2), 'dof_bdry_4'),
    (lambda x: np.isclose(x[2], -box_length/2), 'dof_bdry_5'),
    (lambda x: np.isclose(x[2],  box_length/2), 'dof_bdry_6')
]

dof_boundaries = {}

for marker, dof_name in boundary_conditions:
    facets_bdry = mesh.locate_entities_boundary(msh, dim=fdim, marker=marker)
    dof_boundaries[dof_name] = locate_dofs_topological(V=V, entity_dim=fdim, entities=facets_bdry)


class T_exact_fun():
    def __init__(self, t):
        self.t = t

    def __call__(self, x):
        values = np.zeros((1, x.shape[1]), dtype=PETSc.ScalarType)
        values[0] = -np.exp(-self.t) * (x[0]**2 + 2*x[1]**2)
        return values

# Initial condition
T.interpolate(T_exact_fun(t))
T_.interpolate(T_exact_fun(t))

T_exact = Function(V)
T_fun_value = T_exact_fun(t)
T_exact.interpolate(T_fun_value)

# Weak form
F = T_test * (T - T_) * dx
F += dt * inner( grad(T_test), grad(T) ) * dx

def r_fun(x):
    return  x[0]**2 + 2*x[1]**2 + 6 * np.exp(-t)

r_space = functionspace(msh, CG1_elem)
r = Function(r_space)
r.interpolate(r_fun)

F -= dt * T_test * r * dx

def create_Dirichlet_bc(V, T_fun, t, boundary_key):
    bc_fun = Function(V)
    T_fun_value = T_fun(t)
    bc_fun.interpolate(T_fun_value)
    bc_T = dirichletbc(bc_fun, dof_boundaries[boundary_key])
    return bc_T

while (t < t_final):

    bc_T_1 = create_Dirichlet_bc(V, T_exact_fun, t, 'dof_bdry_1')
    bc_T_2 = create_Dirichlet_bc(V, T_exact_fun, t, 'dof_bdry_2')
    bc_T_3 = create_Dirichlet_bc(V, T_exact_fun, t, 'dof_bdry_3')
    bc_T_4 = create_Dirichlet_bc(V, T_exact_fun, t, 'dof_bdry_4')
    bc_T_5 = create_Dirichlet_bc(V, T_exact_fun, t, 'dof_bdry_5')
    bc_T_6 = create_Dirichlet_bc(V, T_exact_fun, t, 'dof_bdry_6')

    Dirichlet_bcs = [bc_T_1, bc_T_2, bc_T_3, bc_T_4, bc_T_5, bc_T_6]

    problem = NonlinearProblem(F, T, bcs=Dirichlet_bcs)
    solver = NewtonSolver(MPI.COMM_WORLD, problem)
    solver.convergence_criterion = "residual"  # 'residual' or 'incremental'

    t += dt.value
    n, converged = solver.solve(T)
    dt.value *= 1.05

See for instance

where you create the functions

Once per bc and then use interpolate to update it within the loop.

This seems to work, thanks a lot. :slight_smile:

However, there is one thing that still bothers me. The example I’m considering is a simple heat equation \partial_t T - \nabla^2T =r. Using T= e^{-t}\cdot (x^2+2y^2) I obtain r = -e^{-t}\cdot(x^2+2y^2+6) (method of manufactured solution). Visual comparison of the calculated and the exact solution looks ok. However, when refining the mesh, the L2 error remains constant, which is not what I expect. E.g. 8 cells per dimension gives 3.57e-03, wheres 16 gives 3.66e-03. Any ideas on why this is?

import numpy as np
from mpi4py import MPI
from dolfinx import mesh
from dolfinx.io import XDMFFile
from dolfinx.fem import (functionspace, Constant, Function, locate_dofs_topological, dirichletbc, Expression,
                         assemble_scalar, form)
from dolfinx.fem.petsc import NonlinearProblem
from dolfinx.nls.petsc import NewtonSolver
from basix.ufl import element
from ufl import TestFunction, dx, grad, inner
from petsc4py import PETSc

# 3d geometry parameters
box_length = 1
cells_number_per_dim = 8

# time stepping
t = 0
t_final = 10
dt_init = 1.

# diagnostics
output_folder = './output'


msh = mesh.create_box(MPI.COMM_WORLD,[[-box_length/2, -box_length/2, -box_length/2], [box_length/2,box_length/2, box_length/2]],
                      [cells_number_per_dim, cells_number_per_dim, cells_number_per_dim])

CG1_elem = element("Lagrange", msh.basix_cell(), 1)

V = functionspace(msh, CG1_elem)

dt = Constant(msh, dt_init)

T_test = TestFunction(V)
T = Function(V)
T_ = Function(V)

# Dirichlet boundary conditions on cube surface
tdim = msh.topology.dim
fdim = tdim - 1
msh.topology.create_connectivity(fdim, tdim)

boundary_conditions = [
    (lambda x: np.isclose(x[0], -box_length/2), 'dof_bdry_1'),
    (lambda x: np.isclose(x[0],  box_length/2), 'dof_bdry_2'),
    (lambda x: np.isclose(x[1], -box_length/2), 'dof_bdry_3'),
    (lambda x: np.isclose(x[1],  box_length/2), 'dof_bdry_4'),
    (lambda x: np.isclose(x[2], -box_length/2), 'dof_bdry_5'),
    (lambda x: np.isclose(x[2],  box_length/2), 'dof_bdry_6')
]

dof_boundaries = {}

for marker, dof_name in boundary_conditions:
    facets_bdry = mesh.locate_entities_boundary(msh, dim=fdim, marker=marker)
    dof_boundaries[dof_name] = locate_dofs_topological(V=V, entity_dim=fdim, entities=facets_bdry)


class T_exact_fun():
    def __init__(self, t):
        self.t = t

    def __call__(self, x):
        values = np.zeros((1, x.shape[1]), dtype=PETSc.ScalarType)
        values[0] = np.exp(-self.t) * (x[0]**2 + 2*x[1]**2)
        return values

class r_fun():
    def __init__(self, t):
        self.t = t

    def __call__(self, x):
        values = np.zeros((1, x.shape[1]), dtype=PETSc.ScalarType)
        values[0] = -np.exp(-self.t) * (x[0]**2 + 2*x[1]**2 + 6)
        return values

# At t = 0
T.interpolate(T_exact_fun(t))
T_.interpolate(T_exact_fun(t))

T_exact = Function(V)
T_exact.interpolate(T_exact_fun(t))

r = Function(V)
r.interpolate(r_fun(t))

# Weak form
F = T_test * (T - T_) * dx + dt * inner( grad(T_test), grad(T) ) * dx - dt * T_test * r * dx

with XDMFFile(msh.comm, output_folder + "/T_sol.xdmf", "w") as T_sol_plot:
    T_sol_plot.write_mesh(msh)
with XDMFFile(msh.comm, output_folder + "/T_exact.xdmf", "w") as T_exact_plot:
    T_exact_plot.write_mesh(msh)

bc1_fun = Function(V)
bc1_fun.interpolate(T_exact_fun(t))
bc1 = dirichletbc(bc1_fun, dof_boundaries['dof_bdry_1'])

bc2_fun = Function(V)
bc2_fun.interpolate(T_exact_fun(t))
bc2 = dirichletbc(bc2_fun, dof_boundaries['dof_bdry_2'])

bc3_fun = Function(V)
bc3_fun.interpolate(T_exact_fun(t))
bc3 = dirichletbc(bc3_fun, dof_boundaries['dof_bdry_3'])

bc4_fun = Function(V)
bc4_fun.interpolate(T_exact_fun(t))
bc4 = dirichletbc(bc4_fun, dof_boundaries['dof_bdry_4'])

bc5_fun = Function(V)
bc5_fun.interpolate(T_exact_fun(t))
bc5 = dirichletbc(bc5_fun, dof_boundaries['dof_bdry_5'])

bc6_fun = Function(V)
bc6_fun.interpolate(T_exact_fun(t))
bc6 = dirichletbc(bc6_fun, dof_boundaries['dof_bdry_6'])

Dirichlet_bcs = [bc1, bc2, bc3, bc4, bc5, bc6]

problem = NonlinearProblem(F, T, bcs=Dirichlet_bcs)
solver = NewtonSolver(MPI.COMM_WORLD, problem)
solver.convergence_criterion = "incremental"  # 'residual' or 'incremental'

# write solution at t = 0
T_sol_plot.write_function(T, t)
T_exact_plot.write_function(T_exact, t)

while (t < t_final):

    t += dt.value

    # update functions and boundary conditions
    r.interpolate(r_fun(t))
    bc1_fun.interpolate(T_exact_fun(t))
    bc2_fun.interpolate(T_exact_fun(t))
    bc3_fun.interpolate(T_exact_fun(t))
    bc4_fun.interpolate(T_exact_fun(t))
    bc5_fun.interpolate(T_exact_fun(t))
    bc6_fun.interpolate(T_exact_fun(t))
    T_exact.interpolate(T_exact_fun(t))

    n, converged = solver.solve(T)

    T_sol_plot.write_function(T, t)
    T_exact_plot.write_function(T_exact, t)

    print(t)

# Compute L2 error and error at nodes
error_L2 = np.sqrt(msh.comm.allreduce(assemble_scalar(form((T - T_exact)**2 * dx)), op=MPI.SUM))
if msh.comm.rank == 0:
    print(f"L2-error: {error_L2:.2e}")

# Compute values at mesh vertices
error_max = msh.comm.allreduce(np.max(np.abs(T.x.array - T_exact.x.array)), op=MPI.MAX)
if msh.comm.rank == 0:
    print(f"Error_max: {error_max:.2e}")

I would suggest reducing deltar, your time step, as you have both a spatial and temporal error, and it is likely that the temporal error dominates

In order to exclude the temporal error, I solve the stationary version of the above PDE, i. e. -\nabla^2T=r, where T=x^2+2y^2 and consequently r=6.

By visual inspection of the output fields, I see that the calculated and exact solution are almost identical. The solver log reads:

[info] Newton iteration 0: r (abs) = 0.16705409960388973 (tol = 1e-10), r (rel) = inf (tol = 1e-09)
[info] PETSc Krylov solver starting to solve system.
[info] Newton iteration 1: r (abs) = 4.719147780319117e-15 (tol = 1e-10), r (rel) = 2.588306779401889e-16 (tol = 1e-09)
[info] Newton solver finished in 1 iterations and 1 linear solver iterations.
L2-error: 2.82e-01
Error_max: 6.40e-01

Even though both absolute and relative tolerances are very small (1e-15 and 1e-16, resp.), the calculated L2-error and error_max are around 1e-1. Besides that, I see the same unexpected behavior as in the time dependent case, i.e. when refining the mesh, L2-error and error-max do not decrease. Maybe I’m doing something wrong in the calculation of L2-error? I attached the code for stationary version as well below.

import numpy as np
from mpi4py import MPI
from dolfinx import mesh, log
from dolfinx.io import XDMFFile
from dolfinx.fem import (functionspace, Constant, Function, locate_dofs_topological, dirichletbc, Expression,
                         assemble_scalar, form)
from dolfinx.fem.petsc import NonlinearProblem
from dolfinx.nls.petsc import NewtonSolver
from basix.ufl import element
from ufl import TestFunction, dx, grad, inner
from petsc4py import PETSc

# 3d geometry parameters
box_length = 1
cells_number_per_dim = 16

# stationary
t = 0

# diagnostics
output_folder = './output'

msh = mesh.create_box(MPI.COMM_WORLD,[[-box_length/2, -box_length/2, -box_length/2], [box_length/2,box_length/2, box_length/2]],
                      [cells_number_per_dim, cells_number_per_dim, cells_number_per_dim])

CG1_elem = element("Lagrange", msh.basix_cell(), 1)

V = functionspace(msh, CG1_elem)

T_test = TestFunction(V)
T = Function(V)

# Dirichlet boundary conditions on cube surface
tdim = msh.topology.dim
fdim = tdim - 1
msh.topology.create_connectivity(fdim, tdim)

boundary_conditions = [
    (lambda x: np.isclose(x[0], -box_length/2), 'dof_bdry_1'),
    (lambda x: np.isclose(x[0],  box_length/2), 'dof_bdry_2'),
    (lambda x: np.isclose(x[1], -box_length/2), 'dof_bdry_3'),
    (lambda x: np.isclose(x[1],  box_length/2), 'dof_bdry_4'),
    (lambda x: np.isclose(x[2], -box_length/2), 'dof_bdry_5'),
    (lambda x: np.isclose(x[2],  box_length/2), 'dof_bdry_6')
]

dof_boundaries = {}

for marker, dof_name in boundary_conditions:
    facets_bdry = mesh.locate_entities_boundary(msh, dim=fdim, marker=marker)
    dof_boundaries[dof_name] = locate_dofs_topological(V=V, entity_dim=fdim, entities=facets_bdry)


class T_exact_fun():
    def __init__(self, t):
        self.t = t

    def __call__(self, x):
        values = np.zeros((1, x.shape[1]), dtype=PETSc.ScalarType)
        values[0] = np.exp(-self.t) * (x[0]**2 + 2*x[1]**2)
        return values

class r_fun():
    def __init__(self, t):
        self.t = t

    def __call__(self, x):
        values = np.zeros((1, x.shape[1]), dtype=PETSc.ScalarType)
        values[0] = 6
        return values

def T_0(x):
    values = np.zeros((1, x.shape[1]))
    values[0] = 1
    return values

# At t = 0
#T.interpolate(T_0)
T.interpolate(T_exact_fun(t))

T_exact = Function(V)
T_exact.interpolate(T_exact_fun(t))

r = Function(V)
r.interpolate(r_fun(t))

# Weak form
F = T_test * (T) * dx + inner( grad(T_test), grad(T) ) * dx - T_test * r * dx

with XDMFFile(msh.comm, output_folder + "/T_sol.xdmf", "w") as T_sol_plot:
    T_sol_plot.write_mesh(msh)
with XDMFFile(msh.comm, output_folder + "/T_exact.xdmf", "w") as T_exact_plot:
    T_exact_plot.write_mesh(msh)

bc1_fun = Function(V)
bc1_fun.interpolate(T_exact_fun(t))
bc1 = dirichletbc(bc1_fun, dof_boundaries['dof_bdry_1'])

bc2_fun = Function(V)
bc2_fun.interpolate(T_exact_fun(t))
bc2 = dirichletbc(bc2_fun, dof_boundaries['dof_bdry_2'])

bc3_fun = Function(V)
bc3_fun.interpolate(T_exact_fun(t))
bc3 = dirichletbc(bc3_fun, dof_boundaries['dof_bdry_3'])

bc4_fun = Function(V)
bc4_fun.interpolate(T_exact_fun(t))
bc4 = dirichletbc(bc4_fun, dof_boundaries['dof_bdry_4'])

bc5_fun = Function(V)
bc5_fun.interpolate(T_exact_fun(t))
bc5 = dirichletbc(bc5_fun, dof_boundaries['dof_bdry_5'])

bc6_fun = Function(V)
bc6_fun.interpolate(T_exact_fun(t))
bc6 = dirichletbc(bc6_fun, dof_boundaries['dof_bdry_6'])

Dirichlet_bcs = [bc1, bc2, bc3, bc4, bc5, bc6]

problem = NonlinearProblem(F, T, bcs=Dirichlet_bcs)
solver = NewtonSolver(MPI.COMM_WORLD, problem)
solver.convergence_criterion = "residual"  # 'residual' or 'incremental'
log.set_log_level(log.LogLevel.INFO) # INFO, WARNING or ERROR


n, converged = solver.solve(T)

T_sol_plot.write_function(T, t)
T_exact_plot.write_function(T_exact, t)

# Compute L2 error and error at nodes
error_L2 = np.sqrt(msh.comm.allreduce(assemble_scalar(form((T - T_exact)**2 * dx)), op=MPI.SUM))
if msh.comm.rank == 0:
    print(f"L2-error: {error_L2:.2e}")

# Compute values at mesh vertices
error_max = msh.comm.allreduce(np.max(np.abs(T.x.array - T_exact.x.array)), op=MPI.MAX)
if msh.comm.rank == 0:
    print(f"Error_max: {error_max:.2e}")```

Sorry, I realized I messed up the sign or r and the weak form . Changing the former to r= -6 and the latter to

F = inner( grad(T_test), grad(T) ) * dx - T_test * r * dx

yields

[info] Newton iteration 0: r (abs) = 4.635332162466504e-17 (tol = 1e-10), r (rel) = inf (tol = 1e-09)
[info] Newton solver finished in 0 iterations and 0 linear solver iterations.
L2-error: 0.00e+00
Error_max: 0.00e+00

i.e. the solver seems to realize that the initial guess is already the solution and does not even start.

However, when changing to uniform zero initial guess by using

T.interpolate(T_0)

instead of

T.interpolate(T_exact_fun(t))

I still get the unexpected behavior of the L2 error:

cells_number_per_dim = 4: L2-error: 1.41e-16
cells_number_per_dim = 8: L2-error: 3.59e-16
cells_number_per_dim = 16: L2-error: 1.49e-15

Any ideas on why L2-error is not decreasing?

The L2 error is of machine precision. This happens when your analytical solution is in the discrete finite element space.

In your case, the solution does not, but you are not computing the error correctly, a you are comparing Th with the best approximation of T_exact in your space.

Use ufl.SpatialCoordinate to represent the exact solution in error computations, as done in

The following code produces the expected scaling:

x = SpatialCoordinate(msh)
T_ufl = x[0]**2 + 2*x[1]**2


# Compute L2 error and error at nodes
error_L2 = np.sqrt(msh.comm.allreduce(assemble_scalar(form((T - T_ufl)**2 * dx)), op=MPI.SUM))

Thank you again for your help.