Thank you very much for your reply!
I figured it out by myself, but I wanted to verify the solution before posting it to this entry. However, an error occurs applying the class NonlinearMPCProblem with TypeError: NonlinearProblem.__init__() got an unexpected keyword argument 'form_compiler_options'
.
Does anyone have an idea what I am doing wrong?
Here is my MWE (the mesh files can be found here):
# Load required libraries
#########################
import dolfinx
from dolfinx import fem
from dolfinx.fem import FunctionSpace, Function, locate_dofs_geometrical, dirichletbc, Constant
from dolfinx.mesh import locate_entities_boundary, meshtags
from dolfinx.io import XDMFFile
import dolfinx_mpc
import numpy as np
from mpi4py import MPI
import ufl
from typing import List
import pytest
from petsc4py import PETSc
##############################################################################
# Classes for nonlinear MPC problems #
##############################################################################
class NonlinearMPCProblem(dolfinx.fem.petsc.NonlinearProblem):
def __init__(self, F, u, mpc, bcs=[], J=None, form_compiler_options={},
jit_options={}):
self.mpc = mpc
super().__init__(F, u, bcs=bcs, J=J,
form_compiler_options=form_compiler_options,
jit_options=jit_options)
def F(self, x: PETSc.Vec, F: PETSc.Vec):
with F.localForm() as F_local:
F_local.set(0.0)
dolfinx_mpc.assemble_vector(self._L, self.mpc, b=F)
# Apply boundary condition
dolfinx_mpc.apply_lifting(F, [self._a], bcs=[self.bcs],
constraint=self.mpc, x0=[x], scale=-1.0)
F.ghostUpdate(addv=PETSc.InsertMode.ADD, mode=PETSc.ScatterMode.REVERSE)
dolfinx.fem.petsc.set_bc(F, self.bcs, x, -1.0)
def J(self, x: PETSc.Vec, A: PETSc.Mat):
A.zeroEntries()
dolfinx_mpc.assemble_matrix(self._a, self.mpc, bcs=self.bcs, A=A)
A.assemble()
class NewtonSolverMPC(dolfinx.cpp.nls.petsc.NewtonSolver):
def __init__(self, comm: MPI.Intracomm, problem: NonlinearMPCProblem,
mpc: dolfinx_mpc.MultiPointConstraint):
"""A Newton solver for non-linear MPC problems."""
super().__init__(comm)
self.mpc = mpc
self.u_mpc = dolfinx.fem.Function(mpc.function_space)
# Create matrix and vector to be used for assembly of the non-linear
# MPC problem
self._A = dolfinx_mpc.cpp.mpc.create_matrix(
problem.a, mpc._cpp_object)
self._b = dolfinx.cpp.la.petsc.create_vector(
mpc.function_space.dofmap.index_map,
mpc.function_space.dofmap.index_map_bs)
self.setF(problem.F, self._b)
self.setJ(problem.J, self._A)
self.set_form(problem.form)
self.set_update(self.update)
def update(self, solver: dolfinx.nls.petsc.NewtonSolver,
dx: PETSc.Vec, x: PETSc.Vec):
# We need to use a vector created on the MPC's space to update ghosts
self.u_mpc.vector.array = x.array_r
self.u_mpc.vector.axpy(-1.0, dx)
self.u_mpc.vector.ghostUpdate(addv=PETSc.InsertMode.INSERT,
mode=PETSc.ScatterMode.FORWARD)
self.mpc.homogenize(self.u_mpc.vector)
self.mpc.backsubstitution(self.u_mpc.vector)
x.array = self.u_mpc.vector.array_r
x.ghostUpdate(addv=PETSc.InsertMode.INSERT,
mode=PETSc.ScatterMode.FORWARD)
def solve(self, u: dolfinx.fem.Function):
"""Solve non-linear problem into function u. Returns the number
of iterations and if the solver converged."""
n, converged = super().solve(u.vector)
u.x.scatter_forward()
return n, converged
@property
def A(self) -> PETSc.Mat:
"""Jacobian matrix"""
return self._A
@property
def b(self) -> PETSc.Vec:
"""Residual vector"""
return self._b
@pytest.mark.skipif(np.issubdtype(PETSc.ScalarType, np.complexfloating),
reason="This test does not work in complex mode.")
@pytest.mark.parametrize("poly_order", [1, 2, 3])
def test_nonlinear_possion(poly_order):
# Solve a standard Poisson problem with known solution which has
# rotational symmetry of pi/2 at (x, y) = (0.5, 0.5). Therefore we may
# impose MPCs on those DoFs which lie on the symmetry plane(s) and test
# our numerical approximation. We do not impose any constraints at the
# rotationally degenerate point (x, y) = (0.5, 0.5).
N_vals = np.array([4, 8, 16], dtype=np.int32)
l2_error = np.zeros_like(N_vals, dtype=np.double)
for run_no, N in enumerate(N_vals):
mesh = dolfinx.mesh.create_unit_square(MPI.COMM_WORLD, N, N)
V = dolfinx.fem.FunctionSpace(mesh, ("Lagrange", poly_order))
def boundary(x):
return np.ones_like(x[0], dtype=np.int8)
u_bc = dolfinx.fem.Function(V)
with u_bc.vector.localForm() as u_local:
u_local.set(0.0)
facets = dolfinx.mesh.locate_entities_boundary(mesh, 1, boundary)
topological_dofs = dolfinx.fem.locate_dofs_topological(V, 1, facets)
zero = np.array(0, dtype=PETSc.ScalarType)
bc = dolfinx.fem.dirichletbc(zero, topological_dofs, V)
bcs = [bc]
# Define variational problem
u = dolfinx.fem.Function(V)
v = ufl.TestFunction(V)
x = ufl.SpatialCoordinate(mesh)
u_soln = ufl.sin(ufl.pi * x[0]) * ufl.sin(ufl.pi * x[1])
f = -ufl.div((1 + u_soln**2) * ufl.grad(u_soln))
F = ufl.inner((1 + u**2) * ufl.grad(u), ufl.grad(v)) * ufl.dx \
- ufl.inner(f, v) * ufl.dx
J = ufl.derivative(F, u)
# -- Impose the pi/2 rotational symmetry of the solution as a constraint,
# -- except at the centre DoF
def periodic_boundary(x):
eps = 1e-10
return np.isclose(x[0], 0.5) & (
(x[1] < 0.5 - eps) | (x[1] > 0.5 + eps))
def periodic_relation(x):
out_x = np.zeros(x.shape)
out_x[0] = x[1]
out_x[1] = x[0]
out_x[2] = x[2]
return out_x
mpc = dolfinx_mpc.MultiPointConstraint(V)
mpc.create_periodic_constraint_geometrical(
V, periodic_boundary, periodic_relation, bcs)
mpc.finalize()
# Sanity check that the MPC class has some constraints to impose
num_slaves_global = mesh.comm.allreduce(len(mpc.slaves), op=MPI.SUM)
num_masters_global = mesh.comm.allreduce(
len(mpc.masters.array), op=MPI.SUM)
assert num_slaves_global > 0
assert num_masters_global == num_slaves_global
problem = NonlinearMPCProblem(F, u, mpc, bcs=bcs, J=J)
solver = NewtonSolverMPC(mesh.comm, problem, mpc)
# Ensure the solver works with nonzero initial guess
u.interpolate(lambda x: x[0]**2 * x[1]**2)
solver.solve(u)
l2_error_local = dolfinx.fem.assemble_scalar(
dolfinx.fem.form((u - u_soln) ** 2 * ufl.dx))
l2_error_global = mesh.comm.allreduce(l2_error_local, op=MPI.SUM)
l2_error[run_no] = l2_error_global**0.5
rates = np.log(l2_error[:-1] / l2_error[1:]) / np.log(2.0)
assert np.all(rates > poly_order + 0.9)
@pytest.mark.parametrize("element", [ufl.FiniteElement, ufl.VectorElement])
@pytest.mark.parametrize("poly_order", [1, 2, 3])
def test_homogenize(element, poly_order):
mesh = dolfinx.mesh.create_unit_square(MPI.COMM_WORLD, 8, 8)
V = dolfinx.fem.FunctionSpace(
mesh, element("CG", mesh.ufl_cell(), poly_order))
def periodic_boundary(x):
return np.isclose(x[0], 0.0)
def periodic_relation(x):
out_x = np.zeros(x.shape)
out_x[0] = 1.0 - x[0]
out_x[1] = x[1]
out_x[2] = x[2]
return out_x
mpc = dolfinx_mpc.MultiPointConstraint(V)
mpc.create_periodic_constraint_geometrical(
V, periodic_boundary, periodic_relation, [])
mpc.finalize()
# Sanity check that the MPC class has some constraints to impose
num_slaves_global = mesh.comm.allreduce(len(mpc.slaves), op=MPI.SUM)
assert num_slaves_global > 0
u = dolfinx.fem.Function(V)
u.vector.set(1.0)
assert np.isclose(u.vector.min()[1], u.vector.max()[1])
assert np.isclose(u.vector.array_r[0], 1.0)
mpc.homogenize(u.vector)
with u.vector.localForm() as u_:
for i in range(V.dofmap.index_map.size_local * V.dofmap.index_map_bs):
if i in mpc.slaves:
assert np.isclose(u_.array_r[i], 0.0)
else:
assert np.isclose(u_.array_r[i], 1.0)
##############################################################################
# Define mesh #
##############################################################################
comm = MPI.COMM_WORLD
meshFile = "2D_OneInclusion_v3" # mesh file name
# read meshfile and define the mesh(domain), meshtags of the domain (cell_tag) and of the facets (facet_tag)
with XDMFFile(comm, "mesh/"+meshFile+"_mesh.xdmf", "r") as xdmf:
domain = xdmf.read_mesh(name="Grid") # reads the mesh
cell_tag = xdmf.read_meshtags(domain, name="Grid") # cell tags
domain.topology.create_connectivity(domain.topology.dim, domain.topology.dim-1)
with XDMFFile(comm, "mesh/"+meshFile+"_facets.xdmf", "r") as xdmf:
facet_tag = xdmf.read_meshtags(domain, name="Grid")
dim = domain.topology.dim # Dimension
## indices of the mesh
# number of cells
num_cells = domain.topology.index_map(domain.topology.dim).size_local
print("Number of cells",num_cells)
# number of vertices
print(f"Number of global vertices: {domain.topology.index_map(0).size_global}")
# Normal vector
n = ufl.FacetNormal(domain)
# Different measures
dx = ufl.Measure('dx', domain=domain, subdomain_data=cell_tag) # surfaces
ds = ufl.Measure('ds', domain=domain, subdomain_data=facet_tag) # boundaries
dInt = ufl.Measure("dS", domain, subdomain_data=facet_tag) # interfaces
# spatial coordinates (can also be used in variational formulation)
x_coor = ufl.SpatialCoordinate(domain)
##############################################################################
# Definition of elements, functions and spaces #
##############################################################################
# Elements
P1 = ufl.FiniteElement("CG", domain.ufl_cell(), 1)
mel = ufl.MixedElement([P1, P1])
# Define FunctionSpace for coupled mixed space
V = FunctionSpace(domain, mel)
# Define TestFunctions
(dv_1, dv_2) = ufl.TestFunction(V)
# Define Function for solution and split into subspaces
v_sol = Function(V)
(v_1, v_2) = ufl.split(v_sol)
# Trial function for Jacobian
dv_sol = ufl.TrialFunction(V)
##############################################################################
# Material Properties #
##############################################################################
V_DG = FunctionSpace(domain, ("DG", 0))
k_1 = Function(V_DG)
k_2 = Function(V_DG)
for i in range(num_cells):
if cell_tag.values[i] == 1: # matrix
k_1.x.array[i] = 1 # arbitrary material parameters
k_2.x.array[i] = 3
elif cell_tag.values[i] == 2: # inclusions
k_1.x.array[i] = 5
k_1.x.array[i] = 7
##############################################################################
# periodic boundary conditions #
##############################################################################
def dirichlet_and_periodic_bcs(domain, funcspace, boundary_condition: List[str] = ["dirichlet", "periodic"], dbc_value = 0):
"""
Function to set either dirichlet or periodic boundary conditions
----------
boundary_condition
First item describes b.c. on {x=0} and {x=1}
Second item describes b.c. on {y=0} and {y=1}
"""
fdim = domain.topology.dim - 1
bcs = []
pbc_directions = []
pbc_slave_tags = []
pbc_is_slave = []
pbc_is_master = []
pbc_meshtags = []
pbc_slave_to_master_maps = []
# number of subspaces in functionspace
N_subspaces = funcspace.num_sub_spaces
# Create MultiPointConstraint object
mpc = dolfinx_mpc.MultiPointConstraint(funcspace)
def generate_pbc_slave_to_master_map(i):
def pbc_slave_to_master_map(x):
out_x = x.copy()
out_x[i] = x[i] - domain.geometry.x.max()
return out_x
return pbc_slave_to_master_map
def generate_pbc_is_slave(i):
return lambda x: np.isclose(x[i], domain.geometry.x.max())
def generate_pbc_is_master(i):
return lambda x: np.isclose(x[i], domain.geometry.x.min())
# Parse boundary conditions
def parse_bcs():
for i, bc_type in enumerate(boundary_condition):
if bc_type == "dirichlet":
u_bc = fem.Function(functionspace)
u_bc.x.array[:] = dbc_value # value of dirichlet bc needs to be passed into this function!
def dirichletboundary(x):
return np.logical_or(np.isclose(x[i], domain.geometry.x.min()), np.isclose(x[i], domain.geometry.x.max()))
facets = locate_entities_boundary(domain, fdim, dirichletboundary)
topological_dofs = fem.locate_dofs_topological(functionspace, fdim, facets)
bcs.append(fem.dirichletbc(u_bc, topological_dofs))
elif bc_type == "periodic":
pbc_directions.append(i)
pbc_slave_tags.append(i + 2)
pbc_is_slave.append(generate_pbc_is_slave(i))
pbc_is_master.append(generate_pbc_is_master(i))
pbc_slave_to_master_maps.append(generate_pbc_slave_to_master_map(i))
facets = locate_entities_boundary(domain, fdim, pbc_is_slave[-1])
arg_sort = np.argsort(facets)
pbc_meshtags.append(meshtags(domain,
fdim,
facets[arg_sort],
np.full(len(facets), pbc_slave_tags[-1], dtype=np.int32)))
N_pbc = len(pbc_directions)
for i in range(N_pbc):
if N_pbc > 1:
def pbc_slave_to_master_map(x):
out_x = pbc_slave_to_master_maps[i](x)
idx = pbc_is_slave[(i + 1) % N_pbc](x)
out_x[pbc_directions[i]][idx] = np.nan
return out_x
else:
pbc_slave_to_master_map = pbc_slave_to_master_maps[i]
mpc.create_periodic_constraint_topological(functionspace, pbc_meshtags[i],
pbc_slave_tags[i],
pbc_slave_to_master_map,
bcs)
if len(pbc_directions) > 1:
# Map intersection(slaves_x, slaves_y) to intersection(masters_x, masters_y),
# i.e. map the slave dof at (1, 1) to the master dof at (0, 0)
def pbc_slave_to_master_map(x):
out_x = x.copy()
out_x[0] = x[0] - domain.geometry.x.max()
out_x[1] = x[1] - domain.geometry.x.max()
idx = np.logical_and(pbc_is_slave[0](x), pbc_is_slave[1](x))
out_x[0][~idx] = np.nan
out_x[1][~idx] = np.nan
return out_x
mpc.create_periodic_constraint_topological(functionspace, pbc_meshtags[1],
pbc_slave_tags[1],
pbc_slave_to_master_map,
bcs)
if N_subspaces == 0:
functionspace = funcspace
parse_bcs()
else:
for j in range(N_subspaces):
functionspace = funcspace.sub(j)
parse_bcs()
mpc.finalize()
return mpc, bcs
mpc, bcs = dirichlet_and_periodic_bcs(domain, V, ["periodic", "periodic"])
### fix fluctuation at one point
def boundary_node(x):
return np.logical_and( np.isclose(x[0], domain.geometry.x.min()), np.isclose(x[1], domain.geometry.x.min()))
dofs_node_sub0 = locate_dofs_geometrical(V.sub(0).collapse()[0], boundary_node)
dofs_node_sub1 = locate_dofs_geometrical(V.sub(1).collapse()[0], boundary_node)
bc_node_sub0 = dirichletbc(PETSc.ScalarType(0.0), dofs_node_sub0, V.sub(0))
bc_node_sub1 = dirichletbc(PETSc.ScalarType(0.0), dofs_node_sub1, V.sub(1))
bcs.append(bc_node_sub0), bcs.append(bc_node_sub1)
### constant gradients for computational homogenization approach
gradient = Constant(domain, PETSc.ScalarType((1,0)))
gradient2 = Constant(domain, PETSc.ScalarType((0,1)))
##############################################################################
# Variational problem #
##############################################################################
# weak formulation
eqn1 = k_1 * ufl.dot(ufl.grad(ufl.dot(gradient , x_coor) + v_1), ufl.grad(dv_1)) * dx
eqn2 = k_2 * ufl.dot(ufl.grad(ufl.dot(gradient2, x_coor) + v_2), ufl.grad(dv_2)) * dx
eqn = eqn1 + eqn2
# Jacobi determinant
Jac = ufl.derivative(eqn, v_sol, dv_sol)
##############################################################################
# Solve #
##############################################################################
problem = NonlinearMPCProblem(eqn, v_sol, mpc, bcs=bcs, J = Jac) # define nonlinear problem
solver = NewtonSolverMPC(comm, problem, mpc) # define solver
# Set Solver Configurations
solver.convergence_criterion = "incremental"
solver.rtol = 1e-8
ksp = solver.krylov_solver
opts = PETSc.Options()
option_prefix = ksp.getOptionsPrefix()
opts[f"{option_prefix}ksp_type"] = "preonly"
opts[f"{option_prefix}pc_type"] = "lu"
ksp.setFromOptions()
# solve
number, converged = solver.solve(v_sol) # solve the set of PDEs
assert(converged)