Hi,
I have a phase-field formulation which can reach arbitrary negative and positive values, yet I want it to be constrained to [0, 1]. I looked at different approaches to enforce this, and since pyadjoint isn’t implemented for dolfinx, I believe that the SNES VI solver might be best.
I’m using this tutorial on VI in dolfinx as reference for the solver, but it is giving me several issues in getting it to work with dolfinx 0.8.0. Here are two examples:
- dolfinx.fem.petsc.create_matrix
AttributeError: 'Form' object has no attribute '_cpp_object'
I’ve seen this post mentioning the need for a fem.form, but I believe that is not the issue.
- dolfinx.cpp.la.create_vector
AttributeError: module 'dolfinx.cpp.la' has no attribute 'create_vector'
And I expect that there will be more issues if these are resolved.
Here is a MWE, with a commented working unconstrained example, and the constrained example failing.
Any help is much appreciated!
import numpy as np
import dolfinx.fem.petsc
from dolfinx import mesh, fem
from dolfinx.fem.petsc import NonlinearProblem
from dolfinx.nls.petsc import NewtonSolver
from mpi4py import MPI
from dolfinx.io import XDMFFile
import ufl
from petsc4py import PETSc
from typing import List
print(f"dolfinx version: {dolfinx.__version__}")
dt = 2e-1
epsilon = 1e-2
mesh_domain = mesh.create_unit_square(MPI.COMM_WORLD, 50, 50, mesh.CellType.triangle)
"""
Function spaces
"""
V = fem.functionspace(mesh_domain, ("CG", 1)) # works as expected
phi = fem.Function(V, name="phase")
phi_prev = fem.Function(V, name="phase_old")
v = ufl.TestFunction(V)
"""
Phase field formulation
"""
phi_transformed = (phi + 0.2)/1.4 # Rescaled phi for the double-well term to make minima at -0.2, 1.2 -> to test whether the constrained optimization keeps phi in [0, 1]
F = (phi - phi_prev) / dt * v * ufl.dx + epsilon**2 * ufl.inner(ufl.grad(phi), ufl.grad(v)) * ufl.dx + phi_transformed * (1 - phi_transformed)*(1 - 2*phi_transformed) * v * ufl.dx
"""
Initial conditions
"""
rng = np.random.default_rng(42)
phi.interpolate(lambda x: 0.5 + 0.5 * (0.5 - rng.random(x.shape[1])))
"""
Unconstrained solver setup: Working
"""
# problem = NonlinearProblem(F, phi)
# solver = NewtonSolver(MPI.COMM_WORLD, problem)
"""
Constrained solver setup: Not working
"""
dF = ufl.derivative(F, phi, ufl.TestFunction(V))
ddF = ufl.derivative(dF, phi, ufl.TrialFunction(V))
class SNESProblem:
"""Nonlinear problem class compatible with PETSC.SNES solver."""
def __init__(self, F: ufl.form.Form, J: ufl.form.Form, u: dolfinx.fem.function.Function, bcs: List[dolfinx.fem.bcs.DirichletBC] = []):
"""This class set up structures for solving a non-linear problem using Newton's method.
Parameters
==========
F: Residual.
J: Jacobian.
u: Solution.
bcs: Dirichlet boundary conditions.
"""
self.L = F
self.a = J
self.bcs = bcs
self.u = u
# Create matrix and vector to be used for assembly
# of the non-linear problem
self.A = dolfinx.fem.petsc.create_matrix(self.a)
self.b = dolfinx.fem.petsc.create_vector(self.L)
def F(self, snes: PETSc.SNES, x: PETSc.Vec, b: PETSc.Vec):
"""Assemble the residual F into the vector b.
Parameters
==========
snes: the snes object
x: Vector containing the latest solution.
b: Vector to assemble the residual into.
"""
# We need to assign the vector to the function
x.ghostUpdate(addv=PETSc.InsertMode.INSERT, mode=PETSc.ScatterMode.FORWARD)
x.copy(self.u.x)
self.u.x.ghostUpdate(addv=PETSc.InsertMode.INSERT, mode=PETSc.ScatterMode.FORWARD)
# Zero the residual vector
with b.localForm() as b_local:
b_local.set(0.0)
dolfinx.fem.assemble_vector(b, self.L)
# Apply boundary conditions
dolfinx.fem.apply_lifting(b, [self.a], [self.bcs], [x], -1.0)
b.ghostUpdate(addv=PETSc.InsertMode.ADD, mode=PETSc.ScatterMode.REVERSE)
dolfinx.fem.set_bc(b, self.bcs, x, -1.0)
def J(self, snes, x: PETSc.Vec, A: PETSc.Mat, P: PETSc.Mat):
"""Assemble the Jacobian matrix.
Parameters
==========
x: Vector containing the latest solution.
A: Matrix to assemble the Jacobian into.
"""
A.zeroEntries()
dolfinx.fem.assemble_matrix(A, self.a, self.bcs)
A.assemble()
snes_problem = SNESProblem(dF, ddF, phi)
b = dolfinx.cpp.la.create_vector(V.dofmap.index_map, V.dofmap.index_map_bs)
J = dolfinx.fem.petsc.create_matrix(snes_problem.a)
# Create Newton solver and solve
solver_snes = PETSc.SNES().create()
solver_snes.setType("vinewtonrsls")
solver_snes.setFunction(snes_problem.F, b)
solver_snes.setJacobian(snes_problem.J, J)
solver_snes.setTolerances(rtol=1.0e-9, max_it=50)
solver_snes.getKSP().setType("preonly")
solver_snes.getKSP().setTolerances(rtol=1.0e-9)
solver_snes.getKSP().getPC().setType("lu")
# We set the bound (Note: they are passed as reference and not as values)
zero = dolfinx.fem.function.Function(V)
with zero.x.localForm() as loc:
loc.set(0.0)
one = dolfinx.fem.function.Function(V)
with one.x.localForm() as loc:
loc.set(1.0)
solver_snes.setVariableBounds(zero.x,one.x)
"""
Solving
"""
file = XDMFFile(MPI.COMM_WORLD, "phasefield_constrained.xdmf", "w")
file.write_mesh(mesh_domain)
t = 0.0
timesteps = 50
file.write_function(phi, 0)
for i in range(timesteps):
phi_prev.x.array[:] = phi.x.array
t += dt
# Unconstrained solver
# r = solver.solve(phi)
# print(f"Step {int(t / dt)}: num iterations: {r[0]}")
# Constrained solver
r = solver_snes.solve(None, phi.x)
# Process & save solution
file.write_function(phi, i+1)
file.close()