-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added polynomial models for stepsize computation
- Loading branch information
Showing
8 changed files
with
478 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
330 changes: 330 additions & 0 deletions
330
cashocs/_optimization/line_search/polynomial_line_search.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,330 @@ | ||
# Copyright (C) 2020-2022 Sebastian Blauth | ||
# | ||
# This file is part of cashocs. | ||
# | ||
# cashocs 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, either version 3 of the License, or | ||
# (at your option) any later version. | ||
# | ||
# cashocs is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU General Public License | ||
# along with cashocs. If not, see <https://www.gnu.org/licenses/>. | ||
|
||
"""Module for the Armijo line search.""" | ||
|
||
|
||
from __future__ import annotations | ||
|
||
import collections | ||
from typing import Deque, List | ||
|
||
import fenics | ||
import numpy as np | ||
from typing_extensions import TYPE_CHECKING | ||
|
||
from cashocs import _exceptions | ||
from cashocs import _loggers | ||
from cashocs._optimization.line_search import line_search | ||
|
||
if TYPE_CHECKING: | ||
from cashocs import types | ||
from cashocs._optimization import optimization_algorithms | ||
|
||
|
||
class PolynomialLineSearch(line_search.LineSearch): | ||
"""Implementation of the Armijo line search procedure.""" | ||
|
||
def __init__( | ||
self, | ||
optimization_problem: types.OptimizationProblem, | ||
) -> None: | ||
"""Initializes self. | ||
Args: | ||
optimization_problem: The corresponding optimization problem. | ||
""" | ||
super().__init__(optimization_problem) | ||
|
||
self.epsilon_armijo: float = self.config.getfloat( | ||
"OptimizationRoutine", "epsilon_armijo" | ||
) | ||
self.beta_armijo: float = self.config.getfloat( | ||
"OptimizationRoutine", "beta_armijo" | ||
) | ||
self.armijo_stepsize_initial = self.stepsize | ||
self.search_direction_inf = 1.0 | ||
self.decrease_measure_w_o_step = 1.0 | ||
|
||
self.factor_low = self.config.getfloat("LineSearch", "factor_low") | ||
self.factor_high = self.config.getfloat("LineSearch", "factor_high") | ||
self.polynomial_model = self.config.get( | ||
"LineSearch", "polynomial_model" | ||
).casefold() | ||
self.f_vals: Deque[float] = collections.deque() | ||
self.alpha_vals: Deque[float] = collections.deque() | ||
|
||
def _check_for_nonconvergence( | ||
self, solver: optimization_algorithms.OptimizationAlgorithm | ||
) -> bool: | ||
"""Checks, whether the line search failed to converge. | ||
Args: | ||
solver: The optimization algorithm, which uses the line search. | ||
Returns: | ||
A boolean, which is True if a termination / cancellation criterion is | ||
satisfied. | ||
""" | ||
if solver.iteration >= solver.maximum_iterations: | ||
solver.remeshing_its = True | ||
return True | ||
|
||
if self.stepsize * self.search_direction_inf <= 1e-8: | ||
_loggers.error("Stepsize too small.") | ||
solver.line_search_broken = True | ||
return True | ||
elif ( | ||
not self.is_newton_like | ||
and not self.is_newton | ||
and self.stepsize / self.armijo_stepsize_initial <= 1e-8 | ||
): | ||
_loggers.error("Stepsize too small.") | ||
solver.line_search_broken = True | ||
return True | ||
|
||
return False | ||
|
||
def search( | ||
self, | ||
solver: optimization_algorithms.OptimizationAlgorithm, | ||
search_direction: List[fenics.Function], | ||
has_curvature_info: bool, | ||
) -> None: | ||
"""Performs the line search. | ||
Args: | ||
solver: The optimization algorithm. | ||
search_direction: The current search direction. | ||
has_curvature_info: A flag, which indicates whether the direction is | ||
(presumably) scaled. | ||
""" | ||
self.search_direction_inf = np.max( | ||
[ | ||
search_direction[i].vector().norm("linf") | ||
for i in range(len(self.gradient)) | ||
] | ||
) | ||
|
||
if has_curvature_info: | ||
self.stepsize = 1.0 | ||
|
||
num_decreases = ( | ||
self.optimization_variable_abstractions.compute_a_priori_decreases( | ||
search_direction, self.stepsize | ||
) | ||
) | ||
self.stepsize /= pow(self.beta_armijo, num_decreases) | ||
|
||
if self.safeguard_stepsize and solver.iteration == 0: | ||
search_direction_norm = np.sqrt( | ||
self.form_handler.scalar_product(search_direction, search_direction) | ||
) | ||
self.stepsize = float( | ||
np.minimum(self.stepsize, 100.0 / (1.0 + search_direction_norm)) | ||
) | ||
|
||
self.f_vals.clear() | ||
self.alpha_vals.clear() | ||
while True: | ||
if self._check_for_nonconvergence(solver): | ||
return None | ||
|
||
if self.is_shape_problem: | ||
self.decrease_measure_w_o_step = ( | ||
self.optimization_variable_abstractions.compute_decrease_measure( | ||
search_direction | ||
) | ||
) | ||
self.stepsize = ( | ||
self.optimization_variable_abstractions.update_optimization_variables( | ||
search_direction, self.stepsize, self.beta_armijo | ||
) | ||
) | ||
self.alpha_vals.append(self.stepsize) | ||
|
||
current_function_value = solver.objective_value | ||
|
||
self.state_problem.has_solution = False | ||
objective_step = self.cost_functional.evaluate() | ||
self.f_vals.append(objective_step) | ||
|
||
decrease_measure = self._compute_decrease_measure(search_direction) | ||
|
||
if ( | ||
objective_step | ||
< current_function_value + self.epsilon_armijo * decrease_measure | ||
): | ||
if self.optimization_variable_abstractions.requires_remeshing(): | ||
solver.requires_remeshing = True | ||
return None | ||
|
||
if solver.iteration == 0: | ||
self.armijo_stepsize_initial = self.stepsize | ||
break | ||
|
||
else: | ||
self.stepsize = self._compute_polynomial_stepsize( | ||
current_function_value, | ||
decrease_measure, | ||
self.f_vals, | ||
self.alpha_vals, | ||
) | ||
self.optimization_variable_abstractions.revert_variable_update() | ||
|
||
solver.stepsize = self.stepsize | ||
|
||
if not has_curvature_info: | ||
self.stepsize /= self.factor_high | ||
|
||
return None | ||
|
||
def _compute_polynomial_stepsize( | ||
self, | ||
f_current: float, | ||
decrease_measure: float, | ||
f_vals: Deque[float], | ||
alpha_vals: Deque[float], | ||
) -> float: | ||
"""Computes a stepsize based on polynomial models. | ||
Args: | ||
f_current: Current function value | ||
decrease_measure: Current directional derivative in descent direction | ||
f_vals: History of trial function values | ||
alpha_vals: History of trial stepsizes | ||
Returns: | ||
A new stepsize based on polynomial interpolation. | ||
""" | ||
if len(f_vals) == 1: | ||
alpha = self._quadratic_stepsize_model( | ||
f_current, decrease_measure, f_vals, alpha_vals | ||
) | ||
elif len(f_vals) == 2: | ||
alpha = self._cubic_stepsize_model( | ||
f_current, decrease_measure, f_vals, alpha_vals | ||
) | ||
else: | ||
raise _exceptions.CashocsException("This code should not be reached.") | ||
|
||
if alpha < self.factor_low * alpha_vals[-1]: | ||
stepsize = self.factor_low * alpha_vals[-1] | ||
elif alpha > self.factor_high * alpha_vals[-1]: | ||
stepsize = self.factor_high * alpha_vals[-1] | ||
else: | ||
stepsize = alpha | ||
|
||
if (self.polynomial_model == "quadratic" and len(f_vals) == 1) or ( | ||
self.polynomial_model == "cubic" and len(f_vals) == 2 | ||
): | ||
f_vals.popleft() | ||
alpha_vals.popleft() | ||
|
||
return float(stepsize) | ||
|
||
def _quadratic_stepsize_model( | ||
self, | ||
f_current: float, | ||
decrease_measure: float, | ||
f_vals: Deque[float], | ||
alpha_vals: Deque[float], | ||
) -> float: | ||
"""Computes a trial stepsize based on a quadratic model. | ||
Args: | ||
f_current: Current function value | ||
decrease_measure: Current directional derivative in descent direction | ||
f_vals: History of trial function values | ||
alpha_vals: History of trial stepsizes | ||
Returns: | ||
A new trial stepsize based on quadratic interpolation | ||
""" | ||
stepsize = -(decrease_measure * self.stepsize**2) / ( | ||
2.0 * (f_vals[0] - f_current - decrease_measure * alpha_vals[0]) | ||
) | ||
return stepsize | ||
|
||
def _cubic_stepsize_model( | ||
self, | ||
f_current: float, | ||
decrease_measure: float, | ||
f_vals: Deque[float], | ||
alpha_vals: Deque[float], | ||
) -> float: | ||
"""Computes a trial stepsize based on a cubic model. | ||
Args: | ||
f_current: Current function value | ||
decrease_measure: Current directional derivative in descent direction | ||
f_vals: History of trial function values | ||
alpha_vals: History of trial stepsizes | ||
Returns: | ||
A new trial stepsize based on cubic interpolation | ||
""" | ||
coeffs = ( | ||
1 | ||
/ ( | ||
alpha_vals[0] ** 2 | ||
* alpha_vals[1] ** 2 | ||
* (alpha_vals[1] - alpha_vals[0]) | ||
) | ||
* np.array( | ||
[ | ||
[alpha_vals[0] ** 2, -alpha_vals[1] ** 2], | ||
[-alpha_vals[0] ** 3, alpha_vals[1] ** 3], | ||
] | ||
) | ||
@ np.array( | ||
[ | ||
f_vals[1] - f_current - decrease_measure * alpha_vals[1], | ||
f_vals[0] - f_current - decrease_measure * alpha_vals[0], | ||
] | ||
) | ||
) | ||
a, b = coeffs | ||
stepsize = (-b + np.sqrt(b**2 - 3 * a * decrease_measure)) / (3 * a) | ||
return float(stepsize) | ||
|
||
def _compute_decrease_measure( | ||
self, search_direction: List[fenics.Function] | ||
) -> float: | ||
"""Computes the decrease measure for use in the Armijo line search. | ||
Args: | ||
search_direction: The current search direction. | ||
Returns: | ||
The computed decrease measure. | ||
""" | ||
if self.is_control_problem: | ||
return self.optimization_variable_abstractions.compute_decrease_measure( | ||
search_direction | ||
) | ||
elif self.is_shape_problem: | ||
return self.decrease_measure_w_o_step * self.stepsize | ||
else: | ||
return float("inf") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.