Nested Newton solver for mpc

Dear all,

I am trying to solve a non linear problem with periodic bcs and a Langrange multiplier using ufl.MixedFunctionSpace.

For that, I am trying to adapt the BlockedNewtonSolver from scifem to include a list of mpc from dolfinx_mpc using dolfinx_mpc.assemble_matrix_nest and dolfinx_mpc.create_matrix_nest.

I am still learning and debugging my code, but i run into an error before the very first iteration, when assemble the first residual:

[0]PETSC ERROR: ------------------------------------------------------------------------
[0]PETSC ERROR: Caught signal number 11 SEGV: Segmentation Violation, probably memory access out of range
[0]PETSC ERROR: Try option -start_in_debugger or -on_error_attach_debugger
[0]PETSC ERROR: or see https://petsc.org/release/faq/#valgrind and https://petsc.org/release/faq/
[0]PETSC ERROR: configure using --with-debugging=yes, recompile, link, and run 
[0]PETSC ERROR: to get more information on the crash.
[0]PETSC ERROR: Run with -malloc_debug to check if memory corruption is causing the crash.
Abort(59) on node 0 (rank 0 in comm 0): application called MPI_Abort(MPI_COMM_WORLD, 59) - process 0

Here is my draft code



from __future__ import annotations

import logging
from packaging.version import parse as _v

from petsc4py import PETSc
import ufl
import dolfinx
import dolfinx_mpc

logger = logging.getLogger(__name__)
import numpy     as np

#Original code avaiable at https://github.com/scientificcomputing/scifem/blob/main/src/scifem/solvers.py


def cast_seq_to_nest(dx: PETSc.Vec, x: PETSc.Vec) -> PETSc.Vec:
    """
    Cast a sequential Vec (dx) into a VecNest using the structure of x.

    Parameters:
        dx (PETSc.Vec): Sequential vector to be converted.
        x (PETSc.Vec): Nest vector providing the structure.

    Returns:
        PETSc.Vec: A new VecNest constructed from dx.
    """
    if x.getType() != 'nest':
        raise ValueError("x must be a VecNest type.")
    
    # Extract sub-vector sizes from x
    sub_vecs_x = x.getNestSubVecs()
    sizes = [sub_vec.getSize() for sub_vec in sub_vecs_x]

    # Validate total size of dx
    if dx.getSize() != sum(sizes):
        raise ValueError("The size of dx does not match the total size of x's sub-vectors.")

    # Split dx into sub-vectors based on sizes
    offsets = np.concatenate(([0], np.cumsum(sizes)[:-1]))  # Calculate the offsets
    sub_vecs_dx = []
    for offset, size in zip(offsets, sizes):
        sub_vec = PETSc.Vec().createSeq(size)
        sub_vec.array = dx.array[offset : offset + size]
        sub_vecs_dx.append(sub_vec)

    # Create a new VecNest
    dx_nest = PETSc.Vec().createNest(sub_vecs_dx)
    dx_nest.assemble()

    return dx_nest


class BlockedNewtonSolverMPC(dolfinx.cpp.nls.petsc.NewtonSolver):
    def __init__(
        self,
        F: list[ufl.form.Form],
        u: list[dolfinx.fem.Function],
        mpc: list[dolfinx_mpc.MultiPointConstraint],
        bcs: list[dolfinx.fem.DirichletBC] = [],
        J: list[list[ufl.form.Form]] | None = None,
    ):
        # Initialize base class
        super().__init__(u[0].function_space.mesh.comm)
        self._F = dolfinx.fem.form(F)

        # Create the Jacobian matrix, dF/du
        if J is None:
            du = ufl.TrialFunctions(ufl.MixedFunctionSpace(*[ui.function_space for ui in u]))
            J = ufl.extract_blocks(sum(ufl.derivative(sum(F), u[i], du[i]) for i in range(len(u))))
        
        self.jacobian = J
        self._a = dolfinx.fem.form(J)

        self.mpc = mpc
        self.u_mpc = [dolfinx.fem.Function(m.function_space) for m in self.mpc]
        self._bcs = bcs
        self._u = u

        self._b = dolfinx_mpc.create_vector_nest(self._F, mpc)
        self._J = dolfinx_mpc.create_matrix_nest(self._a, mpc)
        self._dx = dolfinx.fem.petsc.create_vector_nest(self._F)
        self._x = dolfinx.fem.petsc.create_vector_nest(self._F)

        self.setF(self._assemble_residual, self._b)
        self.setJ(self._assemble_jacobian, self._J)
        self.set_update(self._update_function)
        print(type(self._a))
        print("Everything set in solver ")


    @property
    def L(self) -> list[dolfinx.fem.Form]:
        """Compiled linear form (the residual form)"""
        return self._F

    @property
    def a(self) -> list[list[dolfinx.fem.Form]]:
        """Compiled bilinear form (the Jacobian form)"""
        return self._a

    @property
    def u(self):
        return self._u

    def __del__(self):
        self._A.destroy()
        self._b.destroy()
        self._dx.destroy()
        self._x.destroy()

    def _assemble_residual(self, x: PETSc.Vec, b: PETSc.Vec) -> None:
        """Assemble the residual F into the vector b.
        Args:
            x: The vector containing the latest solution
            b: Vector to assemble the residual into
        """

        print("Assembling residual", b.getArray(),x.getArray())
        dolfinx_mpc.assemble_vector_nest(b, self._F, self.mpc)
        
        b_local.array[:] = 0.0
        print("Setting done ")
        with b.localForm() as b_local:
            b_local.set(0.0)

        # x0 = [] if x is None else x.getNestSubVecs()
        # bcs1 = dolfinx.fem.bcs_by_block(dolfinx.fem.extract_function_spaces(self._F), self._bcs)

        for i,(b_sub,x_sub)  in enumerate(zip(b.getNestSubVecs(),x.getNestSubVecs())):
            suba=self._J    
            print(x_sub.getSize(),self._J.getSize())
            print("self.jacobian[j][i] ")
            dolfinx_mpc.apply_lifting(
            b_sub,
            # self.jacobian,
            [self.jacobian[j][i] for j in range(2)],
            bcs=[],
            constraint=self.mpc[i],
            x0=[x_sub],
            scale=dolfinx.default_scalar_type(-1.0),
        )
            b_sub.ghostUpdate(addv=PETSc.InsertMode.ADD, mode=PETSc.ScatterMode.REVERSE)

        bcs0 = dolfinx.fem.bcs_by_block(dolfinx.fem.extract_function_spaces(self._F), self._bcs)
        dolfinx.fem.petsc.set_bc_nest(b, bcs0)



    def _assemble_jacobian(self, x: PETSc.Vec, A: PETSc.Mat) -> None:
        print("ASSEMBLING Jacobian attempt")

        """Assemble the Jacobian matrix.
        Args:
            x: The vector containing the latest solution
        """
        A.zeroEntries()
        dolfinx_mpc.assemble_matrix_nest(A, self._a, self.mpc, self._bcs)
        A.assemble()
        # print(A.getArray())
        print("ASSEMBLING Jacobian done")

    def _update_function(self, solver, dx: PETSc.Vec, x: PETSc.Vec):
        """
        Update and add MPC TODO
        """
        print("Updating...")
        newdx = cast_seq_to_nest(dx,x)
        for i,(subdx,subx) in enumerate(zip(newdx.getNestSubVecs(),x.getNestSubVecs())):
            self.u_mpc[i].x.petsc_vec.array = subx.array_r
            self.u_mpc[i].x.petsc_vec.axpy(-1.0, subdx)
            self.u_mpc[i].x.petsc_vec.ghostUpdate(
                addv=PETSc.InsertMode.INSERT,  # type: ignore
                mode=PETSc.ScatterMode.FORWARD,  # type: ignore
            )  # type: ignore
            self.mpc[i].homogenize(self.u_mpc[i])
            self.mpc[i].backsubstitution(self.u_mpc[i])
            subx.array = self.u_mpc[i].x.petsc_vec.array_r
            subx.ghostUpdate(
                addv=PETSc.InsertMode.INSERT,  # type: ignore
                mode=PETSc.ScatterMode.FORWARD,  # type: ignore
        )
        print("Updating Done")
            
    def solve(self):
        print("Starting solver")
        """Solve non-linear problem into function. Returns the number
        of iterations and if the solver converged."""
        # dolfinx.log.set_log_level(dolfinx.log.LogLevel.INFO)
        print("SOlving with ", self._x,self._x.getArray())

        print("Starting solver")
        print("\n")
        n, converged = super().solve(self._x)
        self._x.ghostUpdate(addv=PETSc.InsertMode.INSERT, mode=PETSc.ScatterMode.FORWARD)
        return n, converged

Thank you very much for your help