Some issues with snes_solvers

hi,I tried to repeat this plastic numerical example, 2.077-Fenicsx-Plasticity/Plasticity_Tests/2d_test_Plasticity_New_Frac_Snes.ipynb at main · jorgenin/2.077-Fenicsx-Plasticity · GitHub but there was a problem. I simplified the problem into a simple elastic problem, and the following error message occurred. May I ask why this is the case? The solver reference is the same as that used in the numerical example above.

x = int(1 / 0.1)
ny = int(1 / 0.1)
domain = dolfinx.mesh.create_unit_square(MPI.COMM_WORLD, nx, ny, cell_type=dolfinx.mesh.CellType.triangle)MPI.COMM_WORLD, 0, 2)
tdim =2
fdim =1

V = fem.functionspace(domain,("CG",1,(2,)))

u_new = fem.Function(V,name = "Displacement_for_updating")
du_limit = fem.Function(V, name="Displacement_limit")
du_max = fem.Function(V, name="Displacement_max")
u_load = fem.Function(V, name="Displacement_load")
v = ufl.TrialFunction(V)
u_ = ufl.TestFunction(V)

du_limit.x.array[:] = -np.inf
du_max.x.array[:] = np.inf
n = ufl.FacetNormal(domain)
loading = fem.Constant(domain,ScalarType(0.0))
def fix(x):
    return np.isclose(x[1],0.0)
def load(x):
    return np.isclose(x[1],1.0)
def F_ext(v):
    return -loading * ufl.inner(n, v) * ds

BC = []
Uimp = fem.Constant(domain,1.0)
dofs_fix = dolfinx.fem.locate_dofs_geometrical(V,fix)
entities = dolfinx.mesh.locate_entities_boundary(domain,fdim,load)
dofs_load = dolfinx.fem.locate_dofs_topological(V.sub(1),fdim,entities)
load_tag = mesh.meshtags(domain,fdim,entities,np.full(len(entities),2))
BC_fix = dolfinx.fem.dirichletbc(dolfinx.default_scalar_type((0.0,0.0)),dofs_fix,V)
BC_load = dolfinx.fem.dirichletbc(Uimp,dofs_load,V.sub(1))

BC = [BC_fix]
u_load.x.array[:] = Uimp.value[()]


E = fem.Constant(domain, 7e3) 
nu = fem.Constant(domain, 0.1)
mu = E / (2.0 * (1.0 + nu))
lamda = E * nu / ((1.0 + nu) * (1.0 - 2.0 * nu))

def eps(u):
    return ufl.sym(ufl.grad(u))
def sigma(u):
    return lamda * ufl.tr(eps(u)) * ufl.Identity(2) + 2 * mu * eps(u)

dx = ufl.Measure("dx",domain=domain)
ds = ufl.Measure("ds",domain = domain,subdomain_data=load_tag)
E_u = ufl.inner(sigma(u_new),eps(v)) * dx
E_u_jacbian = ufl.derivative(E_u,u_new,u_)
problem = SNESSolver(E_u, u_new,J_form = E_u_jacbian, bcs = BC,  petsc_options=petsc_options_SNES)


savedir = "results1/"
if os.path.isdir(savedir):
    shutil.rmtree(savedir)
os.makedirs(savedir + "Displacement", exist_ok=True)
Displacement_files = dolfinx.io.VTKFile(domain.comm, savedir + "Displacement/Displacement.vtk", "w")
Nincr = 50
loading = np.linspace(0, 0.01,Nincr + 1)

Niter_max = 100
tol = 1e-4
tol_plastic = 1e-4
iterations = [0]
Force = [0]

for iter, t in enumerate(loading[1:]):
      print("Load step", iter + 1)
      Uimp.value[()] = t
      problem.solve()
      iterations.append(iter)
      Force.append(fem.assemble_scalar(fem.form(sigma(u_new)[1,1] *ds(2))))
      Displacement_files.write_function(u_new, iter)
plt.figure()
plt.plot(loading, Force)
plt.xlabel("Imposed displacement")
plt.ylabel("Reaction force")
plt.show()     

the snes solver

from dolfinx import mesh, fem, io
import dolfinx.fem.petsc
from petsc4py import PETSc
from dolfinx.fem.petsc import NonlinearProblem
from dolfinx.nls.petsc import NewtonSolver
import os, shutil
from petsc4py.PETSc import ScalarType
from dolfinx.cpp.log import LogLevel, log
petsc_options_SNES = {
    "snes_type": "vinewtonrsls",
    "snes_linesearch_type": "basic",
    "ksp_type": "preonly",
    "pc_type": "lu",
    "pc_factor_mat_solver_type": "mumps",
    "snes_atol": 1.0e-08,
    "snes_rtol": 1.0e-09,
    "snes_stol": 0.0,
    "snes_max_it": 50,
    "snes_monitor": "",
    # "snes_monitor_cancel": "",
}
class SNESSolver:
    """
    Problem class for elasticity, compatible with PETSC.SNES solvers.
    """
    def __init__(
        self,
        F_form: ufl.Form,
        u: fem.Function,
        bcs=[],
        J_form: ufl.Form = None,
        bounds=None,
        petsc_options={},
        form_compiler_parameters={},
        jit_parameters={},
        monitor=None,
        prefix=None,
    ):
        self.u = u
        self.bcs = bcs
        self.bounds = bounds
        # Give PETSc solver options a unique prefix
        if prefix is None:
            prefix = "snes_{}".format(str(id(self))[0:4])
        self.prefix = prefix
        if self.bounds is not None:
            self.lb = bounds[0]
            self.ub = bounds[1]
        V = self.u.function_space
        self.comm = V.mesh.comm 
        self.F_form = fem.form(F_form)
        if J_form is None:
            J_form = ufl.derivative(F_form, self.u, ufl.TrialFunction(V))
        self.J_form = fem.form(J_form)
        self.petsc_options = petsc_options
        self.monitor = monitor
        self.solver = self.solver_setup()


    def set_petsc_options(self, debug=False):
        # Set PETSc options
        opts = PETSc.Options()
        opts.prefixPush(self.prefix)
        if debug is True:
            ColorPrint.print_info(self.petsc_options)
        for k, v in self.petsc_options.items():
            opts[k] = v
        opts.prefixPop()


    def solver_setup(self):
        # Create nonlinear solver
        snes = PETSc.SNES().create(self.comm)
        # Set options
        snes.setOptionsPrefix(self.prefix)
        self.set_petsc_options()
        snes.setFromOptions()
        self.b = fem.petsc.create_vector(self.F_form)
        self.a = fem.petsc.create_matrix(self.J_form)
        snes.setFunction(self.F, self.b)
        snes.setJacobian(self.J, self.a)
        # We set the bound (Note: they are passed as reference and not as values)
        if self.monitor is not None:
            snes.setMonitor(self.monitor)
        if self.bounds is not None:
            snes.setVariableBounds(self.lb.vector, self.ub.vector)
        return snes


    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.petsc_vec)
        self.u.x.scatter_forward()
        # Zero the residual vector
        b.array[:] = 0
        fem.petsc.assemble_vector(b, self.F_form)
        # this is a nasty workaround to include the force term with the bug https://github.com/FEniCS/dolfinx/issues/2664
        force_form = fem.form(-F_ext(v))
        b_ds = fem.petsc.create_vector(force_form)
        fem.petsc.assemble_vector(b_ds,force_form)
        b.array[:] += b_ds.array
        
        # Apply boundary conditions
        fem.petsc.apply_lifting(b, [self.J_form], [self.bcs], [x], -1.0)
        b.ghostUpdate(addv=PETSc.InsertMode.ADD, mode=PETSc.ScatterMode.REVERSE)
        fem.petsc.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()
        fem.petsc.assemble_matrix(A, self.J_form, self.bcs)
        A.assemble()


    def solve(self):
        log(LogLevel.INFO, f"Solving {self.prefix}")
        try:
            self.solver.solve(None, self.u.x.petsc_vec)
            self.u.x.scatter_forward()
            return (self.solver.getIterationNumber(), self.solver.getConvergedReason())
        except Warning:
            log(
                LogLevel.WARNING,
                f"WARNING: {self.prefix} solver failed to converge, what's next?",
            )
            raise RuntimeError(f"{self.prefix} solvers did not converge")
type or paste code here

the problem would be like that

 b_ds = fem.petsc.create_vector(force_form)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/petsc/lib/python3/dist-packages/dolfinx/fem/petsc.py", line 112, in create_vector
    dofmap = L.function_spaces[0].dofmap
             ^^^^^^^^^^^^^^^^^
AttributeError: 'list' object has no attribute 'function_spaces'
type or paste code here

This is because you are using the Python variable closure (Python Closures | GeeksforGeeks)
i.e.

where I guess what you expected was that loading is equal to:

However, as you redefine loading later in your code:

This is what is passed in to F_ext.

Thus, what you should do is call the latter one something else, for instance load, and assign it to loading if you want it to update, for instance by calling loading.value=load.

Thank you, Professor. I have a few points that are not very clear regarding the solution of this plasticity problem. I will try to explain my questions in a simpler way:

First of all, referring to the call of the SNES interface for Dolfinx v0.9 you posted before
dolfinx/python/test/unit/fem/test_nonlinear_assembler.py at 160ed13eb476df99944072aec70bd46a6fcb9bcf · FEniCS/dolfinx · GitHub,
a more intuitively noticeable difference is the lack of the introduction of upper and lower bounds. However, when I used the previous SNES to solve a simple elasticity problem (I set the lower bound as -np.inf and the upper bound as np.inf), there were obvious oscillations in the load - displacement curve.

But when using this interface, the effect seems to be very good and the above - mentioned problem doesn’t occur. I wonder what the reason is.

Secondly, regarding the solution of plasticity, is it feasible for me to use the integration of non - orthogonal elements and adopt 1st - order CG elements? Because in your code, you used 2nd - order CG elements.

V = fem.functionspace(msh, ("Lagrange", deg_u, (2,)))

Please forgive my very limited knowledge of plasticity theory!