-
Notifications
You must be signed in to change notification settings - Fork 25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Custom Solver
#673
Comments
Pinging @gabriele-ferrero |
I'm still struggling to understand exactly what would this convergence plot show. Is it: at each time step, we record the final error (absolute and relative) of the Newton solver and keep track of it? or at each time step, we show a plot with the Newton iterations in X and the errors in Y ? |
@RemDelaporteMathurin Another question is related to stationary problems, where there are no time-steps. However, I suppose it is not really needed, but absolute/relative tolerance vs. Newton iterations can be an option. What do you think? |
This sounds very good to me and I don't think it would be super hard to implement. |
Don't know about not super hard) Possibly, the shown below is not what we need. As I see from dolfin, I found hint1 and hint2 on how problem and solver can be adopted for the needs. Maybe, they can help. Also, there is an example with a import fenics as f
import matplotlib.pyplot as plt
mesh = f.IntervalMesh(1000, 0, 1)
class Left(f.SubDomain):
def inside(self, x, on_boundary):
return on_boundary and f.near(x[0], 0)
class Right(f.SubDomain):
def inside(self, x, on_boundary):
return on_boundary and f.near(x[0], 1)
left = Left()
right = Right()
subdomains = f.MeshFunction("size_t", mesh, mesh.topology().dim() - 1)
subdomains.set_all(0)
left.mark(subdomains, 1)
right.mark(subdomains, 2)
ds = f.Measure("ds", domain=mesh, subdomain_data=subdomains)
# ---------- function space ----------------
CG = f.FiniteElement("CG", mesh.ufl_cell(), 1)
V = f.FunctionSpace(mesh, CG)
c = f.Function(V)
c_n = f.Function(V)
v_c = f.TestFunction(V)
# ---------- initial conditions ----------------
c_initial = 1
f.assign(c, f.interpolate(f.Constant(c_initial), V))
f.assign(c_n, f.interpolate(f.Constant(c_initial), V))
# ---------- variational form ----------------
dt = f.Constant(0.001)
F = 0
F += (c - c_n) / dt * v_c * f.dx
F += f.inner(f.grad(c), f.grad(v_c)) * f.dx
J = f.derivative(F, c, f.TrialFunction(c.function_space()))
bcs = [
f.DirichletBC(V, 0.1, subdomains, 1),
f.DirichletBC(V, c_initial, subdomains, 2),
]
# ---------- Problem ----------------
class Problem(f.NonlinearProblem):
def __init__(self, J, F, bcs):
f.NonlinearProblem.__init__(self)
self.bilinear_form = J
self.linear_form = F
self.bcs = bcs
def F(self, b, x):
f.assemble(self.linear_form, tensor=b)
for bc in self.bcs:
bc.apply(b, x)
def J(self, A, x):
f.assemble(self.bilinear_form, tensor=A)
for bc in self.bcs:
bc.apply(A)
problem = Problem(J, F, bcs)
# ------------- Solver ----------------
class CustomSolver(f.NewtonSolver):
def converged(self, r, problem, iteration):
if iteration == 0:
self.r0 = r.norm("l2")
self.current_absolute_residual = r.norm('l2')
self.current_relative_residual = r.norm('l2') / self.r0
return super().converged(r, problem, iteration)
solver = CustomSolver()
# -------------- solve ---------------
fig, ax = plt.subplots()
ax_sub = ax.twinx()
steps = [i for i in range(500)]
abs_res = []
rel_res = []
for i in steps:
nb_it, converged = solver.solve(problem, c.vector())
abs_res.append(solver.current_absolute_residual)
rel_res.append(solver.current_relative_residual)
c_n.assign(c)
ax.plot(steps, abs_res)
ax_sub.plot(steps, rel_res, color = 'tab:red')
ax.set_xlabel('step')
ax.set_ylabel('Absolute tolerance')
ax_sub.set_ylabel('Relative tolerance', color = 'tab:red')
plt.show() |
@KulaginVladimir this is very interesting! (I think on your second plot you meant to label the y axes with absolute error and relative error right?) Do we really need to write our own |
Yes.
I think, a custom if (!newton_solver)
{
dolfin_assert(u->function_space()->mesh());
MPI_Comm comm = u->function_space()->mesh()->mpi_comm();
newton_solver = std::make_shared<NewtonSolver>(comm);
} Though
It appears to me that Btw, I don't really understand how to directly use |
I understand now that the NewtonSolver object created on call in NonLinearVariationalSolver cannot be retrieved outside the class as it's not stored as an attribute. Your solution seems to work on this small case. Maybe the next step is to write a FESTIM script where the |
Is Task 3 of Workshop a suitable test case? |
Sure! |
So, I guess this can be discussed. I used the script from Task 3 of FESTIM Workshop with minor modifications regarding the simulation model. Then I introduced the previously shown import festim as F
import numpy as np
import fenics as f
import matplotlib.pyplot as plt
# Just to collect errors
global a_er, r_er
a_er = []
r_er = []
kwargs = [
{
"ls": "solid",
"color": "royalblue",
"lw": 3,
"label": "festim.Simulation"},
{
"ls": "dashed",
"marker": "x",
"markevery": 20,
"color": "red",
"lw": 1.5,
"label": "CustomSimulation",
},
]
fig, ax = plt.subplots()
ax_sub = ax.twinx()
# ---------- Problem ----------------
class CustomProblem(f.NonlinearProblem):
def __init__(self, J, F, bcs):
self.bilinear_form = J
self.linear_form = F
self.bcs = bcs
f.NonlinearProblem.__init__(self)
def F(self, b, x):
f.assemble(self.linear_form, tensor=b)
for bc in self.bcs:
bc.apply(b, x)
def J(self, A, x):
f.assemble(self.bilinear_form, tensor=A)
for bc in self.bcs:
bc.apply(A)
# ------------- Solver ----------------
class CustomSolver(f.NewtonSolver):
def converged(self, r, problem, iteration):
if iteration == 0:
self.r0 = r.norm("l2")
self.current_absolute_error = r.norm("l2")
self.current_relative_error = r.norm("l2") / self.r0
return super().converged(r, problem, iteration)
# -------------- Custom classes for Simulation ---------------
class CustomHTransportProblem(F.HTransportProblem):
def solve_once(self):
if self.J is None: # Define the Jacobian
du = f.TrialFunction(self.u.function_space())
J = f.derivative(self.F, self.u, du)
else:
J = self.J
problem = CustomProblem(J, self.F, self.bcs)
solver = CustomSolver()
solver.parameters["error_on_nonconvergence"] = False
solver.parameters["absolute_tolerance"] = self.settings.absolute_tolerance
solver.parameters["relative_tolerance"] = self.settings.relative_tolerance
solver.parameters["maximum_iterations"] = self.settings.maximum_iterations
solver.parameters["linear_solver"] = self.settings.linear_solver
nb_it, converged = solver.solve(problem, self.u.vector())
# for a plot
a_er.append(solver.current_absolute_error)
r_er.append(solver.current_relative_error)
return nb_it, converged
class CustomSimulation(F.Simulation):
def initialise(self):
super().initialise()
self.h_transport_problem = CustomHTransportProblem(
self.mobile, self.traps, self.T, self.settings, self.initial_conditions
)
self.attribute_boundary_conditions()
self.attribute_source_terms()
self.h_transport_problem.initialise(self.mesh, self.materials, self.dt)
# -------------- Simulation ---------------
for i, model in enumerate([F.Simulation(), CustomSimulation()]):
my_model = model
my_model.mesh = F.MeshFromVertices(vertices=np.linspace(0, 3e-4, num=1001))
my_model.materials = F.Material(id=1, D_0=1.9e-7, E_D=0.2)
my_model.T = F.Temperature(value=500)
P_up = 200 # Pa
my_model.boundary_conditions = [
F.SievertsBC(surfaces=1, S_0=4.02e21, E_S=1.04, pressure=P_up),
F.DirichletBC(surfaces=2, value=0, field=0),
]
my_model.settings = F.Settings(
absolute_tolerance=1e-4,
relative_tolerance=1e-10,
maximum_iterations=100,
final_time=100, # s
)
my_model.dt = F.Stepsize(
initial_value=0.02,
stepsize_change_ratio=1.05,
max_stepsize=lambda t: None if t < 0.1 else 0.1,
)
# -------------- PP ---------------
derived_quantities = F.DerivedQuantities([F.HydrogenFlux(surface=2)])
my_model.exports = [derived_quantities]
my_model.initialise()
my_model.run()
times = derived_quantities.t
computed_flux = derived_quantities.filter(surfaces=2).data
D = 1.9e-7 * np.exp(-0.2 / F.k_B / 500)
S = 4.02e21 * np.exp(-1.04 / F.k_B / 500)
plt.plot(times, np.abs(computed_flux), **kwargs[i])
plt.ylim(bottom=0)
plt.xlabel("Time (s)")
plt.ylabel("Downstream flux (H/m2/s)")
plt.legend()
plt.show() Solutions with both I also checked that custom classes work with |
Does it compare well in terms of performance? |
I think, yes, This example, 10000 mesh elements on my personal PC: F.Simulation - Ellapsed time: 121.2 s, CustomSimulation - Ellapsed time: 118.6 s. I can perform a more accurate comparison, but I suppose that is not the best problem for tests of the performance. |
I don't think there's a need to test performance accurately here. Do you know how would one setup different solver parameters with this method? @Allentro was interested |
Can you, please, specify what parameters? Meanwhile, does this example help? |
Any parameter really. But I guess it's the same as when you do solver.parameters["error_on_nonconvergence"] = False
solver.parameters["absolute_tolerance"] = self.settings.absolute_tolerance
solver.parameters["relative_tolerance"] = self.settings.relative_tolerance
solver.parameters["maximum_iterations"] = self.settings.maximum_iterations
solver.parameters["linear_solver"] = self.settings.linear_solver In a nutshell, we are interested in having full control (out-of-the-box) over the solver parameters like linear solver, pre-conditionner, linear-solver tolerances etc. |
Oh, if this parameters are the |
Currently in your code, the solver is redefined at every |
@RemDelaporteMathurin suggested to consider implementation of a custom From the private discussion, several points can be highlighted:
@RemDelaporteMathurin, if I missed smth or misunderstood, please correct me. |
This is great! I was looking into this a little over the weekend following the conversation with had with Tim, @RemDelaporteMathurin! I had a look at task 3 from the workshop as a sandbox and also didn't see any clear time efficiency for this using 'mumps'. It would be interesting to explore this scaling on more complex problems, though. Let me know if there is anything I can assist with or test @KulaginVladimir |
I agree that this problem isn't ideal for testing alternative solvers and preconditioners. Both mumps and UMFPACK are direct linear solvers, I'd be interested to investigate pre-conditioned iterative linear solvers. We would need a case that is more expensive with several traps, 3D, a decent amount of cells, several materials, etc |
@KulaginVladimir I think the script would look like my_model.initialise()
class CustomSolver:
.....
custom_solver = CustomSolver()
my_model.h_transport_problem.newton_solver = custom_solver
my_model.run() |
@RemDelaporteMathurin Right. The solver should be an attribute of I only wonder about |
Yes we just have to move away from NonLinearVariationalSolver and use NewtonSolver instead, right? |
I think so
Looks like the model from this recent paper is appropriate. |
@Allentro, @RemDelaporteMathurin, @jhdark I attempted to implement a custom Newton solver. The current version can be found in my repository. Major changes should allow the users to set the solver preconditioner via I performed tests using the model from @RemDelaporteMathurin repository in the transient case with other original settings: my_model.settings = F.Settings(
absolute_tolerance=1e4,
relative_tolerance=1e-5,
maximum_iterations=15,
traps_element_type="DG",
chemical_pot=True,
transient=True,
linear_solver="mumps",
) The results obtained with the custom solver and with FESTIMv1.2.1 (without modifications) correlate in the case when chemical potential conservation is not considered. If In the case of FESTIMv1.2.1, it is: I understand that a reduction of |
@KulaginVladimir do you have a script to reproduce this behaviour? The reason why concentrations are zero is probably because the newton solver converges in zero iterations. Therefore it "solves nothing" and the functions are unchanged (ie equal to the initial conditions). The error is a function of the solution fields. When activating |
Following the discussion, it might be useful to export some metrics, that can characterise the overall error estimation of the solution (or something else), vs. time step (solver iteration). For example, there is some information on "convergence plots" in COMSOL.
The text was updated successfully, but these errors were encountered: