My original code was fairly complex, so distilling down a minimal example was non-trivial, but here it is:
import numpy as np
import ufl
from dolfinx import log, plot, fem
from dolfinx.mesh import CellType, create_unit_square, compute_boundary_facets
from mpi4py import MPI
from petsc4py import PETSc
try:
import pyvista as pv
import pyvistaqt as pvqt
have_pyvista = True
if pv.OFF_SCREEN:
pv.start_xvfb(wait=0.5)
except ModuleNotFoundError:
print("pyvista is required to visualise the solution")
have_pyvista = False
have_pyvista = False
# Save all logging to file
log.set_output_file("log.txt")
#
# Next, various model parameters are defined::
def solve_dh(h, dt=1.0e-2, T=8, decoupled=False, k=2):
domain = create_unit_square(MPI.COMM_WORLD, h, h, CellType.quadrilateral)
P2 = ufl.FiniteElement("S", domain.ufl_cell(), k)
H = fem.FunctionSpace(domain, P2)
x = ufl.SpatialCoordinate(domain)
t = fem.Constant(domain, 0.0)
def phi_e(x):
return np.sin(np.pi * x[0]) * np.cos(np.pi * x[1]) ** 2
def f_3():
return 3 * ufl.pi ** 2 * ufl.cos(ufl.pi * x[1]) ** 2 * ufl.sin(ufl.pi * x[0]) \
- 2 * ufl.pi ** 2 * ufl.sin(ufl.pi * x[0]) * ufl.sin(ufl.pi * x[1]) ** 2
# geometric boundary
def boundary_D(x):
return np.logical_or(np.logical_or(np.isclose(x[0], 0), np.isclose(x[0], 1)),
np.logical_or(np.isclose(x[1], 0), np.isclose(x[1], 1)))
dofs_D = fem.locate_dofs_geometrical(H, boundary_D)
tdim = domain.topology.dim
fdim = tdim - 1
domain.topology.create_connectivity(fdim, tdim)
phi_e_bc = fem.Function(H)
phi_e_bc.interpolate(phi_e)
# Weak statement of the equations
# terms for Eq. 13
phi_e = ufl.TrialFunction(H)
psi = ufl.TestFunction(H)
# Eq. 13
F3 = ufl.inner(ufl.grad(phi_e), ufl.grad(psi)) * ufl.dx \
- f_3() * psi * ufl.dx
a3 = fem.form(ufl.lhs(F3))
L3 = fem.form(ufl.rhs(F3))
A3 = fem.assemble_matrix(a3, bcs=[fem.dirichletbc(phi_e_bc, dofs_D)])
A3.assemble()
b3 = fem.assemble_vector(L3)
phi_e_m1, phi_e_m2 = fem.Function(H), fem.Function(H)
phi_e = fem.Function(H)
# numbered by step, not equation
solver1 = PETSc.KSP().create(domain.comm)
solver1.setOperators(A3)
solver1.setType(PETSc.KSP.Type.PREONLY)
pc1 = solver1.getPC()
pc1.setType(PETSc.PC.Type.LU)
# interpolate exact solution at initial time
t.value = 0.0
phi_e_m2.interpolate(phi_e)
t.value += dt
phi_e_m1.interpolate(phi_e)
# Prepare viewer for plotting solution during the computation
if have_pyvista:
topology, cell_types = plot.create_vtk_topology(domain, domain.topology.dim)
grid = pv.UnstructuredGrid(topology, cell_types, domain.geometry.x)
# glyph_grid = pv.UnstructuredGrid(topology, cell_types, mesh.geometry.x)
grid.point_data["phi_e"] = phi_e.compute_point_values().real
p = pvqt.BackgroundPlotter(title="Plot", auto_update=True, shape=(2, 2))
p.subplot(1, 1)
phi_e_grid = grid.copy()
phi_e_grid.set_active_scalars("phi_e")
p.add_mesh(phi_e_grid, clim=[-1, 1])
#glyph_actor = p.add_mesh(glyphs)
p.view_xy(True)
p.subplot(0, 0)
p.add_mesh(grid, color="k", style="wireframe")
#glyph_actor = p.add_mesh(glyphs)
p.view_xy(True)
p.add_text(f"time: {t}", font_size=10, name="timelabel")
# time stepping
print(f"starting timestepping...")
while t.value < T:
t.value += dt
phi_e_bc.interpolate(phi_e)
A3 = fem.assemble_matrix(a3, bcs=[fem.dirichletbc(phi_e_bc, dofs_D)])
A3.assemble()
solver1.setOperators(A3)
with b3.localForm() as loc_b:
loc_b.set(0)
fem.assemble_vector(b3, L3)
# Apply Dirichlet boundary condition to the vector
fem.apply_lifting(b3, [a3], [[fem.dirichletbc(phi_e_bc, dofs_D)]])
b3.ghostUpdate(addv=PETSc.InsertMode.ADD_VALUES, mode=PETSc.ScatterMode.REVERSE)
fem.set_bc(b3, [fem.dirichletbc(phi_e_bc, dofs_D)])
r = solver1.solve(b3, phi_e.vector)
phi_e.x.scatter_forward()
# Compute L2 error and error at nodes
error_e_L2 = np.sqrt(domain.comm.allreduce(fem.assemble_scalar(fem.form((phi_e - phi_e_bc) ** 2 * ufl.dx)), op=MPI.SUM))
# Update the plot window
if have_pyvista:
p.subplot(1, 1)
p.add_text(f"phi_e L2: {error_e_L2:.3e}", font_size=8, name="phi_e_L2", position=(10, 10))
p.add_text(f"time: {t.value:.2e}", font_size=10, name="timelabel")
phi_e_grid.point_data["phi_e"] = phi_e.compute_point_values().real
p.app.processEvents()
print(f"completed solve with h = {h}")
return None
ns = [2, 4, 8, 16, 32, 64]
errors = [solve_dh(h=N, T=2, dt=1.0/(10*N), decoupled=True, k=2) for N in ns]
This is just a poisson problem as well, but with time-stepping. In this example the boundary conditions do not vary in time, but in general they may. My original problem involves a coupled system of which this is the simplest equation.
I have observed that the failure mode is not always the same as what I stated originally. Occasionally, the apparent heap corruption will be caught by the OS (macOS) rather than by PETSc. When it occurs in the computation also varies substantially.