Skip to content

Commit a7550fd

Browse files
committed
Add PETSc solver to the Mohr-Coulomb tutorial
1 parent 244b306 commit a7550fd

File tree

2 files changed

+143
-36
lines changed

2 files changed

+143
-36
lines changed

doc/demo/demo_plasticity_mohr_coulomb.py

+32-35
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# -*- coding: utf-8 -*-
12
# ---
23
# jupyter:
34
# jupytext:
@@ -84,7 +85,7 @@
8485
import matplotlib.pyplot as plt
8586
import numpy as np
8687
from mpltools import annotation # for slope markers
87-
from solvers import LinearProblem
88+
from solvers import SNESProblem
8889
from utilities import find_cell_by_point
8990

9091
import basix
@@ -655,7 +656,26 @@ def F_ext(v):
655656
# applied to other plasticity models.
656657

657658
# %%
658-
external_operator_problem = LinearProblem(J_replaced, -F_replaced, Du, bcs=bcs)
659+
petsc_options = {
660+
"snes_type": "vinewtonrsls",
661+
"ksp_type": "preonly",
662+
"pc_type": "lu",
663+
"pc_factor_mat_solver_type": "mumps",
664+
"snes_atol": 1.0e-11,
665+
"snes_rtol": 1.0e-11,
666+
"snes_max_it": 50,
667+
"snes_monitor": "",
668+
# "snes_monitor_cancel": "",
669+
}
670+
671+
def constitutive_update():
672+
evaluated_operands = evaluate_operands(F_external_operators)
673+
((_, sigma_new),) = evaluate_external_operators(J_external_operators, evaluated_operands)
674+
# Direct access to the external operator values
675+
sigma.ref_coefficient.x.array[:] = sigma_new
676+
677+
external_operator_problem = SNESProblem(Du, F_replaced, J_replaced, bcs=bcs, petsc_options=petsc_options, system_update=constitutive_update)
678+
659679

660680
# %%
661681
x_point = np.array([[0, H, 0]])
@@ -665,56 +685,33 @@ def F_ext(v):
665685
# parameters of the manual Newton method
666686
max_iterations, relative_tolerance = 200, 1e-8
667687

668-
load_steps_1 = np.linspace(2, 21, 40)
669-
load_steps_2 = np.linspace(21, 22.75, 20)[1:]
670-
load_steps = np.concatenate([load_steps_1, load_steps_2])
688+
# load_steps_1 = np.linspace(2, 21, 40)
689+
# load_steps_2 = np.linspace(21, 22.75, 20)[1:]
690+
# load_steps = np.concatenate([load_steps_1, load_steps_2])
691+
# load_steps = np.concatenate([np.linspace(1.1, 22.3, 100)[:-1]])
692+
load_steps = np.concatenate([np.linspace(1.2, 22.3, 150)[:-1]])
671693
num_increments = len(load_steps)
672694
results = np.zeros((num_increments + 1, 2))
673695

674696
# %% tags=["scroll-output"]
675-
676697
for i, load in enumerate(load_steps):
677698
q.value = load * np.array([0, -gamma])
678-
external_operator_problem.assemble_vector()
679699

680-
residual_0 = external_operator_problem.b.norm()
681-
residual = residual_0
682-
Du.x.array[:] = 0
700+
# Du.x.array[:] = 0
683701

684702
if MPI.COMM_WORLD.rank == 0:
685-
print(f"Load increment #{i}, load: {load}, initial residual: {residual_0}")
686-
687-
for iteration in range(0, max_iterations):
688-
if residual / residual_0 < relative_tolerance:
689-
break
690-
691-
if MPI.COMM_WORLD.rank == 0:
692-
print(f"\tOuter Newton iteration #{iteration}")
693-
external_operator_problem.assemble_matrix()
694-
external_operator_problem.solve(du)
695-
696-
Du.x.petsc_vec.axpy(1.0, du.x.petsc_vec)
697-
Du.x.scatter_forward()
698-
699-
evaluated_operands = evaluate_operands(F_external_operators)
700-
((_, sigma_new),) = evaluate_external_operators(J_external_operators, evaluated_operands)
701-
# Direct access to the external operator values
702-
sigma.ref_coefficient.x.array[:] = sigma_new
703-
704-
external_operator_problem.assemble_vector()
705-
residual = external_operator_problem.b.norm()
706-
707-
if MPI.COMM_WORLD.rank == 0:
708-
print(f"\tResidual: {residual}\n")
703+
print(f"Load increment #{i}, load: {load}")
709704

705+
external_operator_problem.solve()
706+
710707
u.x.petsc_vec.axpy(1.0, Du.x.petsc_vec)
711708
u.x.scatter_forward()
712709

713710
sigma_n.x.array[:] = sigma.ref_coefficient.x.array
714711

715712
if len(points_on_process) > 0:
716713
results[i + 1, :] = (-u.eval(points_on_process, cells)[0], load)
717-
714+
718715
print(f"Slope stability factor: {-q.value[-1] * H / c}")
719716

720717
# %% [markdown]

doc/demo/solvers.py

+111-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import dolfinx.fem.petsc # noqa: F401
44
import ufl
55
from dolfinx import fem
6-
6+
from typing import List, Union, Dict, Optional, Callable, Tuple
77

88
class LinearProblem:
99
def __init__(self, dR: ufl.Form, R: ufl.Form, u: fem.Function, bcs: list[fem.dirichletbc] | None = None):
@@ -63,3 +63,113 @@ def __del__(self):
6363
self.solver.destroy()
6464
self.A.destroy()
6565
self.b.destroy()
66+
67+
class SNESProblem:
68+
"""Solves a nonlinear problem via PETSc.SNES.
69+
F(u) = 0
70+
J = dF/du
71+
b = assemble(F)
72+
A = assemble(J)
73+
Ax = b
74+
"""
75+
def __init__(
76+
self,
77+
u: fem.Function,
78+
F: ufl.Form,
79+
J: ufl.Form,
80+
bcs=[],
81+
petsc_options={},
82+
prefix=None,
83+
system_update: Optional[Callable] = None,
84+
):
85+
self.u = u
86+
V = self.u.function_space
87+
self.comm = V.mesh.comm
88+
89+
self.F_form = fem.form(F)
90+
# J = ufl.derivative(F_form, self.u, ufl.TrialFunction(V))
91+
self.J_form = fem.form(J)
92+
self.b = fem.petsc.create_vector(self.F_form)
93+
self.A = fem.petsc.create_matrix(self.J_form)
94+
95+
self.bcs = bcs
96+
97+
# Give PETSc solver options a unique prefix
98+
if prefix is None:
99+
prefix = "snes_{}".format(str(id(self))[0:4])
100+
101+
self.prefix = prefix
102+
self.petsc_options = petsc_options
103+
self.system_update = system_update
104+
105+
self.solver = self.solver_setup()
106+
107+
def set_petsc_options(self):
108+
# Set PETSc options
109+
opts = PETSc.Options()
110+
opts.prefixPush(self.prefix)
111+
112+
for k, v in self.petsc_options.items():
113+
opts[k] = v
114+
115+
opts.prefixPop()
116+
117+
def solver_setup(self):
118+
# Create nonlinear solver
119+
snes = PETSc.SNES().create(self.comm)
120+
121+
snes.setOptionsPrefix(self.prefix)
122+
self.set_petsc_options()
123+
snes.setFromOptions()
124+
125+
snes.setFunction(self.F, self.b)
126+
snes.setJacobian(self.J, self.A)
127+
128+
return snes
129+
130+
def F(self, snes: PETSc.SNES, x: PETSc.Vec, b: PETSc.Vec) -> None:
131+
"""Assemble the residual F into the vector b.
132+
133+
Parameters
134+
==========
135+
snes: the snes object
136+
x: Vector containing the latest solution.
137+
b: Vector to assemble the residual into.
138+
"""
139+
x.ghostUpdate(addv=PETSc.InsertMode.INSERT, mode=PETSc.ScatterMode.FORWARD)
140+
x.copy(self.u.x.petsc_vec)
141+
self.u.x.scatter_forward()
142+
143+
#TODO: SNES makes the iteration #0, where it calculates the b norm.
144+
#`system_update()` can be omitted in that case
145+
self.system_update()
146+
147+
with b.localForm() as b_local:
148+
b_local.set(0.0)
149+
fem.petsc.assemble_vector(b, self.F_form)
150+
151+
fem.petsc.apply_lifting(b, [self.J_form], [self.bcs], [x], -1.0)
152+
b.ghostUpdate(addv=PETSc.InsertMode.ADD, mode=PETSc.ScatterMode.REVERSE)
153+
fem.petsc.set_bc(b, self.bcs, x, -1.0)
154+
155+
def J(self, snes, x: PETSc.Vec, A: PETSc.Mat, P: PETSc.Mat) -> None:
156+
"""Assemble the Jacobian matrix.
157+
158+
Parameters
159+
==========
160+
x: Vector containing the latest solution.
161+
A: Matrix to assemble the Jacobian into.
162+
"""
163+
A.zeroEntries()
164+
fem.petsc.assemble_matrix(A, self.J_form, self.bcs)
165+
A.assemble()
166+
167+
def solve(self,) -> Tuple[int, int]:
168+
self.solver.solve(None, self.u.x.petsc_vec)
169+
self.u.x.scatter_forward()
170+
return (self.solver.getIterationNumber(), self.solver.getConvergedReason())
171+
172+
def __del__(self):
173+
self.solver.destroy()
174+
self.b.destroy()
175+
self.A.destroy()

0 commit comments

Comments
 (0)