In this tutorial we solve the optimal control problem
$$\min J(y, u) = \frac{1}{2} \int_{\Omega} |v - v_d|^2 dx + \frac{\alpha}{2} \int_{\Gamma_2} |u|^2 ds$$ s.t. $$\begin{cases} - \nu \Delta v + v \cdot \nabla v + \nabla p = f & \text{in } \Omega\\ \text{div} v = 0 & \text{in } \Omega\\ v = 0 & \text{on } \Gamma_1\\ pn - \nu \partial_n v = u & \text{on } \Gamma_2\\ v = 0 & \text{on } \Gamma_3\\ v = 0 & \text{on } \Gamma_4 \end{cases}$$
where $$\begin{align*} & \Omega & \text{unit square}\\ & \Gamma_1 & \text{bottom boundary of the square}\\ & \Gamma_2 & \text{left boundary of the square}\\ & \Gamma_3 & \text{top boundary of the square}\\ & \Gamma_4 & \text{right boundary of the square}\\ & u \in [L^2(\Gamma_2)]^2 & \text{control variable}\\ & v \in [H^1(\Omega)]^2 & \text{state velocity variable}\\ & p \in L^2(\Omega) & \text{state pressure variable}\\ & \alpha > 0 & \text{penalization parameter}\\ & v_d & \text{desired state}\\ & \nu & \text{kinematic viscosity}\\ & f & \text{forcing term} \end{align*}$$ using an adjoint formulation solved by a one shot approach
import dolfinx.fem
import dolfinx.fem.petsc
import dolfinx.io
import dolfinx.mesh
import mpi4py.MPI
import numpy as np
import numpy.typing as npt
import petsc4py.PETSc
import sympy
import ufl
import viskex
import multiphenicsx.fem
import multiphenicsx.fem.petsc
mesh = dolfinx.mesh.create_unit_square(mpi4py.MPI.COMM_WORLD, 32, 32)
# Create connectivities required by the rest of the code
mesh.topology.create_connectivity(mesh.topology.dim - 1, mesh.topology.dim)
def bottom(x: npt.NDArray[np.float64]) -> npt.NDArray[np.bool_]:
"""Condition that defines the bottom boundary."""
return abs(x[1] - 0.) < np.finfo(float).eps # type: ignore[no-any-return]
def left(x: npt.NDArray[np.float64]) -> npt.NDArray[np.bool_]:
"""Condition that defines the left boundary."""
return abs(x[0] - 0.) < np.finfo(float).eps # type: ignore[no-any-return]
def top(x: npt.NDArray[np.float64]) -> npt.NDArray[np.bool_]:
"""Condition that defines the top boundary."""
return abs(x[1] - 1.) < np.finfo(float).eps # type: ignore[no-any-return]
def right(x: npt.NDArray[np.float64]) -> npt.NDArray[np.bool_]:
"""Condition that defines the right boundary."""
return abs(x[0] - 1.) < np.finfo(float).eps # type: ignore[no-any-return]
boundaries_entities = dict()
boundaries_values = dict()
for (boundary, boundary_id) in zip((bottom, left, top, right), (1, 2, 3, 4)):
boundaries_entities[boundary_id] = dolfinx.mesh.locate_entities_boundary(
mesh, mesh.topology.dim - 1, boundary)
boundaries_values[boundary_id] = np.full(
boundaries_entities[boundary_id].shape, boundary_id, dtype=np.int32)
boundaries_entities_unsorted = np.hstack(list(boundaries_entities.values()))
boundaries_values_unsorted = np.hstack(list(boundaries_values.values()))
boundaries_entities_argsort = np.argsort(boundaries_entities_unsorted)
boundaries_entities_sorted = boundaries_entities_unsorted[boundaries_entities_argsort]
boundaries_values_sorted = boundaries_values_unsorted[boundaries_entities_argsort]
boundaries = dolfinx.mesh.meshtags(
mesh, mesh.topology.dim - 1,
boundaries_entities_sorted, boundaries_values_sorted)
boundaries.name = "boundaries"
boundaries_134 = boundaries.indices[np.isin(boundaries.values, (1, 3, 4))]
boundaries_2 = boundaries.indices[boundaries.values == 2]
# Define associated measures
ds = ufl.Measure("ds", subdomain_data=boundaries)
viskex.dolfinx.plot_mesh(mesh)
viskex.dolfinx.plot_mesh_tags(mesh, boundaries, "boundaries")
Y_velocity = dolfinx.fem.functionspace(mesh, ("Lagrange", 2, (mesh.geometry.dim, )))
Y_pressure = dolfinx.fem.functionspace(mesh, ("Lagrange", 1))
U = dolfinx.fem.functionspace(mesh, ("Lagrange", 2, (mesh.geometry.dim, )))
Q_velocity = Y_velocity.clone()
Q_pressure = Y_pressure.clone()
dofs_Y_velocity = np.arange(0, Y_velocity.dofmap.index_map.size_local + Y_velocity.dofmap.index_map.num_ghosts)
dofs_Y_pressure = np.arange(0, Y_pressure.dofmap.index_map.size_local + Y_pressure.dofmap.index_map.num_ghosts)
dofs_U = dolfinx.fem.locate_dofs_topological(U, boundaries.dim, boundaries_2)
dofs_Q_velocity = dofs_Y_velocity
dofs_Q_pressure = dofs_Y_pressure
restriction_Y_velocity = multiphenicsx.fem.DofMapRestriction(Y_velocity.dofmap, dofs_Y_velocity)
restriction_Y_pressure = multiphenicsx.fem.DofMapRestriction(Y_pressure.dofmap, dofs_Y_pressure)
restriction_U = multiphenicsx.fem.DofMapRestriction(U.dofmap, dofs_U)
restriction_Q_velocity = multiphenicsx.fem.DofMapRestriction(Q_velocity.dofmap, dofs_Q_velocity)
restriction_Q_pressure = multiphenicsx.fem.DofMapRestriction(Q_pressure.dofmap, dofs_Q_pressure)
restriction = [
restriction_Y_velocity, restriction_Y_pressure, restriction_U, restriction_Q_velocity, restriction_Q_pressure]
(dv, dp) = (ufl.TrialFunction(Y_velocity), ufl.TrialFunction(Y_pressure))
(w, q) = (ufl.TestFunction(Y_velocity), ufl.TestFunction(Y_pressure))
du = ufl.TrialFunction(U)
r = ufl.TestFunction(U)
(dz, db) = (ufl.TrialFunction(Q_velocity), ufl.TrialFunction(Q_pressure))
(s, d) = (ufl.TestFunction(Q_velocity), ufl.TestFunction(Q_pressure))
(v, p) = (dolfinx.fem.Function(Y_velocity), dolfinx.fem.Function(Y_pressure))
u = dolfinx.fem.Function(U)
(z, b) = (dolfinx.fem.Function(Q_velocity), dolfinx.fem.Function(Q_pressure))
alpha = 1.e-5
x, y = sympy.symbols("x[0], x[1]")
psi_d = 10 * (1 - sympy.cos(0.8 * np.pi * x)) * (1 - sympy.cos(0.8 * np.pi * y)) * (1 - x)**2 * (1 - y)**2
v_d_x = sympy.lambdify([x, y], psi_d.diff(y, 1))
v_d_y = sympy.lambdify([x, y], - psi_d.diff(x, 1))
v_d = dolfinx.fem.Function(Y_velocity)
v_d.interpolate(lambda x: np.stack((v_d_x(x[0], x[1]), v_d_y(x[0], x[1])), axis=0))
nu = 0.1
zero_scalar = petsc4py.PETSc.ScalarType(0) # type: ignore[attr-defined]
zero_vector = np.zeros((2, ), dtype=petsc4py.PETSc.ScalarType) # type: ignore[attr-defined]
ff = dolfinx.fem.Constant(mesh, zero_vector)
F = [nu * ufl.inner(ufl.grad(z), ufl.grad(w)) * ufl.dx
+ ufl.inner(z, ufl.grad(w) * v) * ufl.dx + ufl.inner(z, ufl.grad(v) * w) * ufl.dx
- ufl.inner(b, ufl.div(w)) * ufl.dx + ufl.inner(v - v_d, w) * ufl.dx,
- ufl.inner(ufl.div(z), q) * ufl.dx,
alpha * ufl.inner(u, r) * ds(2) - ufl.inner(z, r) * ds(2),
nu * ufl.inner(ufl.grad(v), ufl.grad(s)) * ufl.dx + ufl.inner(ufl.grad(v) * v, s) * ufl.dx
- ufl.inner(p, ufl.div(s)) * ufl.dx - ufl.inner(ff, s) * ufl.dx - ufl.inner(u, s) * ds(2),
- ufl.inner(ufl.div(v), d) * ufl.dx]
dF = [[ufl.derivative(F_i, u_j, du_j) for (u_j, du_j) in zip((v, p, u, z, b), (dv, dp, du, dz, db))] for F_i in F]
dF[3][3] = dolfinx.fem.Constant(mesh, zero_scalar) * ufl.inner(dz, s) * (ds(1) + ds(3) + ds(4))
bdofs_Y_velocity_134 = dolfinx.fem.locate_dofs_topological(
Y_velocity, mesh.topology.dim - 1, boundaries_134)
bdofs_Q_velocity_134 = dolfinx.fem.locate_dofs_topological(
Q_velocity, mesh.topology.dim - 1, boundaries_134)
bc = [dolfinx.fem.dirichletbc(zero_vector, bdofs_Y_velocity_134, Y_velocity),
dolfinx.fem.dirichletbc(zero_vector, bdofs_Q_velocity_134, Q_velocity)]
J = 0.5 * ufl.inner(v - v_d, v - v_d) * ufl.dx + 0.5 * alpha * ufl.inner(u, u) * ds(2)
J_cpp = dolfinx.fem.form(J)
# Create problem by extracting state forms from the optimality conditions
F_state = [ufl.replace(F[i], {
s: w, d: q,
u: dolfinx.fem.Constant(mesh, zero_vector)}) for i in (3, 4)]
dF_state = [[ufl.derivative(Fs_i, u_j, du_j) for (u_j, du_j) in zip((v, p), (dv, dp))] for Fs_i in F_state]
dF_state[1][1] = dolfinx.fem.Constant(mesh, zero_scalar) * ufl.inner(dp, q) * ufl.dx
bc_state = [bc[0]]
restriction_state = [restriction[i] for i in (3, 4)]
petsc_options = {
"ksp_type": "preonly",
"pc_type": "lu",
"pc_factor_mat_solver_type": "mumps",
"ksp_error_if_not_converged": True,
"snes_monitor": None,
"snes_error_if_not_converged": True
}
problem_state = multiphenicsx.fem.petsc.NonlinearProblem(
F_state, (v, p), bcs=bc_state, J=dF_state,
petsc_options_prefix="tutorial_8_navier_stokes_neumann_control_state_", petsc_options=petsc_options,
kind="mpi", restriction=restriction_state
)
problem_state.solve()
del problem_state
0 SNES Function norm 0.000000000000e+00
J_uncontrolled = mesh.comm.allreduce(dolfinx.fem.assemble_scalar(J_cpp), op=mpi4py.MPI.SUM)
print("Uncontrolled J =", J_uncontrolled)
assert np.isclose(J_uncontrolled, 0.1784542)
Uncontrolled J = 0.17845361493420545
viskex.dolfinx.plot_vector_field(v, "uncontrolled state velocity", glyph_factor=1e-1)
viskex.dolfinx.plot_scalar_field(p, "uncontrolled state pressure")
# PYTEST_XFAIL_AND_SKIP_NEXT: ufl.derivative(adjoint of the trilinear term) introduces spurious conj(trial)
"""Expect this cell to fail.
assert not np.issubdtype(petsc4py.PETSc.ScalarType, np.complexfloating) # type: ignore[attr-defined]
"""
'Expect this cell to fail.\n\nassert not np.issubdtype(petsc4py.PETSc.ScalarType, np.complexfloating) # type: ignore[attr-defined]\n'
"""Skip cell due to a previously xfailed cell.
problem = multiphenicsx.fem.petsc.NonlinearProblem(
F, (v, p, u, z, b), bcs=bc, J=dF,
petsc_options_prefix="tutorial_8_navier_stokes_neumann_control_", petsc_options=petsc_options,
kind="mpi", restriction=restriction
)
problem.solve()
del problem
"""
'Skip cell due to a previously xfailed cell.\n\nproblem = multiphenicsx.fem.petsc.NonlinearProblem(\n F, (v, p, u, z, b), bcs=bc, J=dF,\n petsc_options_prefix="tutorial_8_navier_stokes_neumann_control_", petsc_options=petsc_options,\n kind="mpi", restriction=restriction\n)\nproblem.solve()\ndel problem\n'
"""Skip cell due to a previously xfailed cell.
J_controlled = mesh.comm.allreduce(dolfinx.fem.assemble_scalar(J_cpp), op=mpi4py.MPI.SUM)
print("Optimal J =", J_controlled)
assert np.isclose(J_controlled, 0.1249381)
"""
'Skip cell due to a previously xfailed cell.\n\nJ_controlled = mesh.comm.allreduce(dolfinx.fem.assemble_scalar(J_cpp), op=mpi4py.MPI.SUM)\nprint("Optimal J =", J_controlled)\nassert np.isclose(J_controlled, 0.1249381)\n'
"""Skip cell due to a previously xfailed cell.
viskex.dolfinx.plot_vector_field(v, "state velocity", glyph_factor=1e-1)
"""
'Skip cell due to a previously xfailed cell.\n\nviskex.dolfinx.plot_vector_field(v, "state velocity", glyph_factor=1e-1)\n'
"""Skip cell due to a previously xfailed cell.
viskex.dolfinx.plot_scalar_field(p, "state pressure")
"""
'Skip cell due to a previously xfailed cell.\n\nviskex.dolfinx.plot_scalar_field(p, "state pressure")\n'
"""Skip cell due to a previously xfailed cell.
viskex.dolfinx.plot_vector_field(u, "control", glyph_factor=1e-1)
"""
'Skip cell due to a previously xfailed cell.\n\nviskex.dolfinx.plot_vector_field(u, "control", glyph_factor=1e-1)\n'
"""Skip cell due to a previously xfailed cell.
viskex.dolfinx.plot_vector_field(z, "adjoint velocity", glyph_factor=1)
"""
'Skip cell due to a previously xfailed cell.\n\nviskex.dolfinx.plot_vector_field(z, "adjoint velocity", glyph_factor=1)\n'
"""Skip cell due to a previously xfailed cell.
viskex.dolfinx.plot_scalar_field(b, "adjoint pressure")
"""
'Skip cell due to a previously xfailed cell.\n\nviskex.dolfinx.plot_scalar_field(b, "adjoint pressure")\n'