Source code for soupy.solver.newtonBacktrackSolver

# Copyright (c) 2023, The University of Texas at Austin 
# & Georgia Institute of Technology
#
# All Rights reserved.
# See file COPYRIGHT for details.
#
# This file is part of the SOUPy package. For more information see
# https://github.com/hippylib/soupy/
#
# SOUPy is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License (as published by the Free
# Software Foundation) version 3.0 dated June 2007.


import sys
import math
import time 

import dolfin as dl 
import numpy as np 
from mpi4py import MPI 

import hippylib as hp 

[docs] def apply_BC(bcs, u): """ Applies a single or a list of :code:`dolfin.DirichletBC` to the vector :code:`u` """ if bcs is None: pass elif isinstance(bcs, list): for bc in bcs: bc.apply(u) else: bcs.apply(u)
[docs] def homogenize_BC(bcs): """ Converts a single or a list of :code:`dolfin.DirichletBC` to their homogenized (zeroed) forms """ if bcs is None: return None elif isinstance(bcs, list): bcs0 = [] for bc in bcs: bc0 = dl.DirichletBC(bc) bc0.homogenize() bcs0.append(bc0) return bcs0 else: bc0 = dl.DirichletBC(bcs) bc0.homogenize() return bc0
[docs] class NewtonBacktrackSolver: """ Backtracking Newton solver for the nonlinear variational system .. math:: \\text{Find } u \in U \qquad r(u,v) = 0 \quad \\forall v \in V The user must provide the variational forms for the residual form, \ an initial guess, boundary conditions, and optionally, \ variational forms for the Jacobian and backtracking cost functional. """ termination_reasons = [ "Maximum number of Iteration reached", #0 "Norm of the gradient less than tolerance", #1 "Maximum number of backtracking reached", #2 "Norm of (g, da) less than tolerance" #3 ] def __init__(self): """ Initializes the :code:`NewtonBacktrackSolver` with the following parameters. - :code:`rel_tolerance` --> we converge when :math:`\|r\|_2/\|r_0\|_2 \leq` :code:`rel_tolerance` - :code:`abs_tolerance` --> we converge when :math:`\|r\|_2 \leq` :code:`abs_tolerance` - :code:`maxximum_iterations` --> maximum number of iterations - :code:`c_armijo` --> Armijo constant for sufficient reduction - :code:`max_backtracking_iter` --> Maximum number of backtracking iterations - :code:`print_level` --> Print info on screen """ self.parameters = {} self.parameters["rel_tolerance"] = 1e-8 self.parameters["abs_tolerance"] = 1e-12 self.parameters["maximum_iterations"] = 20 self.parameters["c_armijo"] = 1e-4 self.parameters["max_backtracking_iter"] = 20 self.parameters["print_level"] = 1 self.parameters["lu_method"] = "mumps"
[docs] def solve(self, residual_form, u, bcs=None, J_form = None, energy_form=None, solver=None): """ Solve the nonlinear variational system .. math:: \\text{Find } u \in U \qquad r(u,v) = 0 \quad \\forall v \in V given using a backtracking Newton method with supplied initial guess :param residual_form: Variational form for the residual :param u: Initial guess for the solution and funciton used in :code:`residual_form` :type u: :py:class:`dolfin.Function` :param bcs: List of boundary conditions :type bcs: list :param J_form: Variational form for the Jacobian of the residual :param energy_form: Optional variational form for the energy functional. If supplied, uses this as the backtracking cost :param solver: Optional linear solver with method :code:`solve(A, x, b)` Will initialize a solver if :code:`solver` is :code:`None`. """ self.it = 0 self.converged = False self.reason = 0 self.final_grad_norm = 0 if J_form is None: J_form = dl.derivative(residual_form, u) mpi_comm = u.vector().mpi_comm() rtol = self.parameters["rel_tolerance"] atol = self.parameters["abs_tolerance"] max_iter = self.parameters["maximum_iterations"] c_armijo = self.parameters["c_armijo"] max_backtrack = self.parameters["max_backtracking_iter"] print_level = self.parameters["print_level"] lu_method = self.parameters["lu_method"] my_rank = MPI.COMM_WORLD.Get_rank() # Apply BC to initial guess apply_BC(bcs, u.vector()) # Homogenize the dirichlet BCs if they exist bcs0 = homogenize_BC(bcs) # Assemble initial residual residual = dl.assemble(residual_form) # apply_BC(bcs0, residual) r_norm = residual.norm("l2") tol = max(r_norm*rtol, atol) if energy_form is not None: energy = dl.assemble(energy_form) du = dl.Vector(u.vector()) u_current = dl.Vector(u.vector()) if print_level > 0 and my_rank == 0: if energy_form is not None: print("{0:3} {1:3} {2:15} {3:15} {4:15} {5:15} ".format( "MPI", "It", "Energy", "||r||", "(r,du)", "alpha")) print("{0:3d} {1:3d} {2:15f} {3:15f} {4:15f} {5:15f}".format( my_rank, self.it, energy, r_norm, 0, 0)) else: print("{0:3} {1:3} {2:15} {3:15} {4:15}".format( "MPI", "It", "||r||", "(r,du)", "alpha")) print("{0:3d} {1:3d} {2:15f} {3:15f} {4:15f}".format( my_rank, self.it, r_norm, 0, 0)) if solver is None: solver = hp.PETScLUSolver(mpi_comm, method=lu_method) sys.stdout.flush() while self.it < max_iter: # Make Newton system J, r = dl.assemble_system(J_form, residual_form, bcs=bcs0) solver.set_operator(J) # Solving the newton system. Note -du is the descent direction tsolve0 = time.time() solver.solve(du, r) tsolve1 = time.time() if print_level > 0 and my_rank == 0: print("Proc %d Linear solve time = %.3g s" %(my_rank, tsolve1 - tsolve0)) sys.stdout.flush() du_rn = residual.inner(du) i_backtrack = 0 backtrack_converged = False alpha = 1.0 u_current.zero() u_current.axpy(1.0, u.vector()) while not backtrack_converged and i_backtrack < max_backtrack: u.vector().zero() u.vector().axpy(1.0, u_current) u.vector().axpy(-alpha, du) if energy_form is not None: # Backtrack on the energy energy_new = dl.assemble(energy_form) if energy_new < energy: # if energy_new < energy + alpha * c_armijo * du_rn: backtrack_converged = True energy = energy_new else: alpha = alpha/2 else: # Backtrack on residual norm residual_new = dl.assemble(residual_form) apply_BC(bcs0, residual_new) r_new_norm = residual_new.norm('l2') if r_new_norm < r_norm: backtrack_converged = True r_norm = r_new_norm else: alpha = alpha/2 i_backtrack += 1 # Done with backtracking attempts if not backtrack_converged: self.reason = 2 u.vector().zero() u.vector().axpy(1.0, u_current) break # Back tracking is successful, increment iteration self.it += 1 # Compute residual norm residual = dl.assemble(residual_form) apply_BC(bcs0, residual) r_norm = residual.norm("l2") # Print new residual norms if print_level > 0 and my_rank == 0: if energy_form is not None: print("{0:3d} {1:3d} {2:15f} {3:15f} {4:15f} {5:15f}".format( my_rank, self.it, energy, r_norm, du_rn, alpha)) else: print("{0:3d} {1:3d} {2:15f} {3:15f} {4:15f}".format( my_rank, self.it, r_norm, du_rn, alpha)) if r_norm < tol: self.converged = True self.reason = 1 break sys.stdout.flush() # Finished optimization loop self.final_residual_norm = r_norm if print_level > -1 and my_rank == 0: print(self.termination_reasons[self.reason]) if self.converged: print("Proc %d: Backtracking Newton converged after %d iterations" %(my_rank, self.it)) else: print("Proc %d: Backtracking Newton did not converge after %d iterations" %(my_rank, self.it)) print("Proc %d: Final norm of the residual: %.5e" %(my_rank, r_norm)) if energy_form is not None: print("Proc %d: Final value of energy: %.5e" %(my_rank, energy)) sys.stdout.flush() return self.it, self.converged