Source code for cashocs._constraints.constrained_problems

# Copyright (C) 2020-2024 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/>.

"""Constrained optimization problems."""

from __future__ import annotations

import abc
from typing import Callable, List, Optional, TYPE_CHECKING, Union

import fenics
import numpy as np
from typing_extensions import Literal

try:
    import ufl_legacy as ufl
except ImportError:
    import ufl

from cashocs import _utils
from cashocs._constraints import solvers
from cashocs._optimization import optimal_control
from cashocs._optimization import shape_optimization

if TYPE_CHECKING:
    from cashocs import _typing
    from cashocs import io


class ConstrainedOptimizationProblem(abc.ABC):
    """An optimization problem with additional equality / inequality constraints."""

    solver: Union[solvers.AugmentedLagrangianMethod, solvers.QuadraticPenaltyMethod]

    def __init__(
        self,
        state_forms: Union[List[ufl.Form], ufl.Form],
        bcs_list: Union[
            List[List[fenics.DirichletBC]], List[fenics.DirichletBC], fenics.DirichletBC
        ],
        cost_functional_form: Union[
            List[_typing.CostFunctional], _typing.CostFunctional
        ],
        states: Union[fenics.Function, List[fenics.Function]],
        adjoints: Union[fenics.Function, List[fenics.Function]],
        constraint_list: Union[List[_typing.Constraint], _typing.Constraint],
        config: Optional[io.Config] = None,
        initial_guess: Optional[List[fenics.Function]] = None,
        ksp_options: Optional[Union[_typing.KspOption, List[_typing.KspOption]]] = None,
        adjoint_ksp_options: Optional[
            Union[_typing.KspOption, List[_typing.KspOption]]
        ] = None,
        gradient_ksp_options: Optional[
            Union[_typing.KspOption, List[_typing.KspOption]]
        ] = None,
        preconditioner_forms: Optional[List[ufl.Form]] = None,
        pre_callback: Optional[
            Union[Callable[[], None], Callable[[_typing.OptimizationProblem], None]]
        ] = None,
        post_callback: Optional[
            Union[Callable[[], None], Callable[[_typing.OptimizationProblem], None]]
        ] = None,
        linear_solver: Optional[_utils.linalg.LinearSolver] = None,
        adjoint_linear_solver: Optional[_utils.linalg.LinearSolver] = None,
        newton_linearizations: Optional[Union[ufl.Form, List[ufl.Form]]] = None,
    ) -> None:
        """Initializes self.

        Args:
            state_forms: The weak form of the state equation (user implemented). Can be
                either a single UFL form, or a (ordered) list of UFL forms.
            bcs_list: The list of :py:class:`fenics.DirichletBC` objects describing
                Dirichlet (essential) boundary conditions. If this is ``None``, then no
                Dirichlet boundary conditions are imposed.
            cost_functional_form: UFL form of the cost functional. Can also be a list of
                summands of the cost functional
            states: The state variable(s), can either be a :py:class:`fenics.Function`,
                or a list of these.
            adjoints: The adjoint variable(s), can either be a
                :py:class:`fenics.Function`, or a (ordered) list of these.
            constraint_list: (A list of) additional equality and inequality constraints
                for the problem.
            config: config: The config file for the problem, generated via
                :py:func:`cashocs.load_config`. Alternatively, this can also be
                ``None``, in which case the default configurations are used, except for
                the optimization algorithm. This has then to be specified in the
                :py:meth:`solve <cashocs.OptimalControlProblem.solve>` method. The
                default is ``None``.
            initial_guess: List of functions that act as initial guess for the state
                variables, should be valid input for :py:func:`fenics.assign`. Defaults
                to ``None``, which means a zero initial guess.
            ksp_options: A list of dicts corresponding to command line options for
                PETSc, used to solve the state systems. If this is ``None``, then the
                direct solver mumps is used (default is ``None``).
            adjoint_ksp_options: A list of dicts corresponding to command line options
                for PETSc, used to solve the adjoint systems. If this is ``None``, then
                the same options as for the state systems are used (default is
                ``None``).
            gradient_ksp_options: A list of dicts corresponding to command line options
                for PETSc, used to compute the (shape) gradient. If this is ``None``,
                either a direct or an iterative method is used (depending on the
                configuration, section OptimizationRoutine, key gradient_method).
            preconditioner_forms: The list of forms for the preconditioner. The default
                is `None`, so that the preconditioner matrix is the same as the system
                matrix.
            pre_callback: A function (without arguments) that will be called before each
                solve of the state system
            post_callback: A function (without arguments) that will be called after the
                computation of the gradient.
            linear_solver: The linear solver (KSP) which is used to solve the linear
                systems arising from the discretized PDE.
            adjoint_linear_solver: The linear solver (KSP) which is used to solve the
                (linear) adjoint system.
            newton_linearizations: A (list of) UFL forms describing which (alternative)
                linearizations should be used for the (nonlinear) state equations when
                solving them (with Newton's method). The default is `None`, so that the
                Jacobian of the supplied state forms is used.

        """
        self.state_forms = state_forms
        self.bcs_list = bcs_list
        self.states = states
        self.adjoints = adjoints

        if config is not None:
            self.config = config
        else:
            self.config = io.Config()
        self.config.validate_config()

        self.initial_guess = initial_guess
        self.ksp_options = ksp_options
        self.adjoint_ksp_options = adjoint_ksp_options
        self.gradient_ksp_options = gradient_ksp_options
        self.preconditioner_forms = preconditioner_forms
        self.pre_callback = pre_callback
        self.post_callback = post_callback
        self.linear_solver = linear_solver
        self.adjoint_linear_solver = adjoint_linear_solver
        self.newton_linearizations = newton_linearizations

        self.current_function_value = 0.0

        self._pre_callback: Optional[
            Union[Callable[[], None], Callable[[_typing.OptimizationProblem], None]]
        ] = None
        self._post_callback: Optional[
            Union[Callable[[], None], Callable[[_typing.OptimizationProblem], None]]
        ] = None

        self.cost_functional_form_initial: List[_typing.CostFunctional] = _utils.enlist(
            cost_functional_form
        )
        self.constraint_list: List[_typing.Constraint] = _utils.enlist(constraint_list)

        self.constraint_dim = len(self.constraint_list)

        self.initial_norm = 0.0
        self.constraint_violation = 0.0
        self.constraint_violation_prev = 0.0

        self.cost_functional_shift = 0.0

    def solve(
        self,
        method: Literal[
            "Augmented Lagrangian", "AL", "Quadratic Penalty", "QP"
        ] = "Augmented Lagrangian",
        tol: float = 1e-2,
        max_iter: int = 25,
        inner_rtol: Optional[float] = None,
        inner_atol: Optional[float] = None,
        constraint_tol: Optional[float] = None,
        mu_0: Optional[float] = None,
        lambda_0: Optional[List[float]] = None,
    ) -> None:
        """Solves the constrained optimization problem.

        Args:
            method: The solution algorithm, either an augmented Lagrangian method
                ("Augmented Lagrangian", "AL") or quadratic penalty method
                ("Quadratic Penalty", "QP")
            tol: An overall tolerance to be used in the algorithm. This will set the
                relative tolerance for the inner optimization problems to ``tol``.
                Default is 1e-2.
            max_iter: Maximum number of iterations for the outer solver. Default is 25.
            inner_rtol: Relative tolerance for the inner problem. Default is ``None``,
            so that ``inner_rtol = tol`` is used.
            inner_atol: Absolute tolerance for the inner problem. Default is ``None``,
                so that ``inner_atol = tol/10`` is used.
            constraint_tol: The tolerance for the constraint violation, which is
                desired. If this is ``None`` (default), then this is specified as
                ``tol/10``.
            mu_0: Initial value of the penalty parameter. Default is ``None``, which
                means that ``mu_0 = 1`` is used.
            lambda_0: Initial guess for the Lagrange multipliers. Default is ``None``,
                which corresponds to a zero guess.

        """
        if method.casefold() in ["augmented lagrangian", "al"]:
            self.solver = solvers.AugmentedLagrangianMethod(
                self, mu_0=mu_0, lambda_0=lambda_0
            )
        elif method.casefold() in ["quadratic penalty", "qp"]:
            self.solver = solvers.QuadraticPenaltyMethod(
                self, mu_0=mu_0, lambda_0=lambda_0
            )

        self.solver.solve(
            tol=tol,
            max_iter=max_iter,
            inner_rtol=inner_rtol,
            inner_atol=inner_atol,
            constraint_tol=constraint_tol,
        )

    def total_constraint_violation(self) -> float:
        """Computes the total constraint violation.

        Returns:
            The 2-norm of the total constraint violation.

        """
        s = 0.0
        for constraint in self.constraint_list:
            s += pow(constraint.constraint_violation(), 2)

        violation: float = np.sqrt(s)
        return violation

    @abc.abstractmethod
    def _solve_inner_problem(
        self,
        tol: float = 1e-2,
        inner_rtol: Optional[float] = None,
        inner_atol: Optional[float] = None,
    ) -> None:
        """Solves the inner (unconstrained) optimization problem.

        Args:
            tol: An overall tolerance to be used in the algorithm. This will set the
                relative tolerance for the inner optimization problems to ``tol``.
                Default is 1e-2.
            inner_rtol: Relative tolerance for the inner problem. Default is ``None``,
                so that ``inner_rtol = tol`` is used.
            inner_atol: Absolute tolerance for the inner problem. Default is ``None``,
                so that ``inner_atol = tol/10`` is used.

        """
        self.rtol = inner_rtol or tol

    def inject_pre_callback(
        self,
        function: Optional[
            Union[Callable[[], None], Callable[[_typing.OptimizationProblem], None]]
        ],
    ) -> None:
        """Changes the a-priori callback of the OptimizationProblem.

        Args:
            function: A custom function without arguments, which will be called before
                each solve of the state system

        """
        self._pre_callback = function

    def inject_post_callback(
        self,
        function: Optional[
            Union[Callable[[], None], Callable[[_typing.OptimizationProblem], None]]
        ],
    ) -> None:
        """Changes the a-posteriori callback of the OptimizationProblem.

        Args:
            function: A custom function without arguments, which will be called after
                the computation of the gradient(s)

        """
        self._post_callback = function

    def inject_pre_post_callback(
        self,
        pre_function: Optional[
            Union[Callable[[], None], Callable[[_typing.OptimizationProblem], None]]
        ],
        post_function: Optional[
            Union[Callable[[], None], Callable[[_typing.OptimizationProblem], None]]
        ],
    ) -> None:
        """Changes the a-priori (pre) and a-posteriori (post) callbacks of the problem.

        Args:
            pre_function: A function without arguments, which is to be called before
                each solve of the state system
            post_function: A function without arguments, which is to be called after
                each computation of the (shape) gradient

        """
        self.inject_pre_callback(pre_function)
        self.inject_post_callback(post_function)


[docs] class ConstrainedOptimalControlProblem(ConstrainedOptimizationProblem): """An optimal control problem with additional (in-)equality constraints.""" def __init__( self, state_forms: Union[ufl.Form, List[ufl.Form]], bcs_list: Union[ fenics.DirichletBC, List[fenics.DirichletBC], List[List[fenics.DirichletBC]] ], cost_functional_form: Union[ List[_typing.CostFunctional], _typing.CostFunctional ], states: Union[fenics.Function, List[fenics.Function]], controls: Union[fenics.Function, List[fenics.Function]], adjoints: Union[fenics.Function, List[fenics.Function]], constraint_list: Union[_typing.Constraint, List[_typing.Constraint]], config: Optional[io.Config] = None, riesz_scalar_products: Optional[Union[ufl.Form, List[ufl.Form]]] = None, control_constraints: Optional[List[List[Union[float, fenics.Function]]]] = None, initial_guess: Optional[List[fenics.Function]] = None, ksp_options: Optional[Union[_typing.KspOption, List[_typing.KspOption]]] = None, adjoint_ksp_options: Optional[ Union[_typing.KspOption, List[_typing.KspOption]] ] = None, gradient_ksp_options: Optional[ Union[_typing.KspOption, List[_typing.KspOption]] ] = None, control_bcs_list: Optional[ Union[ fenics.DirichletBC, List[fenics.DirichletBC], List[List[fenics.DirichletBC]], ] ] = None, preconditioner_forms: Optional[List[ufl.Form]] = None, pre_callback: Optional[Callable] = None, post_callback: Optional[Callable] = None, linear_solver: Optional[_utils.linalg.LinearSolver] = None, adjoint_linear_solver: Optional[_utils.linalg.LinearSolver] = None, newton_linearizations: Optional[Union[ufl.Form, List[ufl.Form]]] = None, ) -> None: r"""Initializes self. Args: state_forms: The weak form of the state equation (user implemented). Can be either a single UFL form, or a (ordered) list of UFL forms. bcs_list: The list of :py:class:`fenics.DirichletBC` objects describing Dirichlet (essential) boundary conditions. If this is ``None``, then no Dirichlet boundary conditions are imposed. cost_functional_form: UFL form of the cost functional. Can also be a list of summands of the cost functional states: The state variable(s), can either be a :py:class:`fenics.Function`, or a list of these. controls: The control variable(s), can either be a :py:class:`fenics.Function`, or a list of these. adjoints: The adjoint variable(s), can either be a :py:class:`fenics.Function`, or a (ordered) list of these. constraint_list: (A list of) additional equality and inequality constraints for the problem. config: config: The config file for the problem, generated via :py:func:`cashocs.load_config`. Alternatively, this can also be ``None``, in which case the default configurations are used, except for the optimization algorithm. This has then to be specified in the :py:meth:`solve <cashocs.OptimalControlProblem.solve>` method. The default is ``None``. riesz_scalar_products: The scalar products of the control space. Can either be None, a single UFL form, or a (ordered) list of UFL forms. If ``None``, the :math:`L^2(\Omega)` product is used (default is ``None``). control_constraints: Box constraints posed on the control, ``None`` means that there are none (default is ``None``). The (inner) lists should contain two elements of the form ``[u_a, u_b]``, where ``u_a`` is the lower, and ``u_b`` the upper bound. initial_guess: List of functions that act as initial guess for the state variables, should be valid input for :py:func:`fenics.assign`. Defaults to ``None``, which means a zero initial guess. ksp_options: A list of dicts corresponding to command line options for PETSc, used to solve the state systems. If this is ``None``, then the direct solver mumps is used (default is ``None``). adjoint_ksp_options: A list of dicts corresponding to command line options for PETSc, used to solve the adjoint systems. If this is ``None``, then the same options as for the state systems are used (default is ``None``). gradient_ksp_options: A list of dicts corresponding to command line options for PETSc, used to compute the (shape) gradient. If this is ``None``, either a direct or an iterative method is used (depending on the configuration, section OptimizationRoutine, key gradient_method). control_bcs_list: A list of boundary conditions for the control variables. This is passed analogously to ``bcs_list``. Default is ``None``. preconditioner_forms: The list of forms for the preconditioner. The default is `None`, so that the preconditioner matrix is the same as the system matrix. pre_callback: A function (without arguments) that will be called before each solve of the state system post_callback: A function (without arguments) that will be called after the computation of the gradient. linear_solver: The linear solver (KSP) which is used to solve the linear systems arising from the discretized PDE. adjoint_linear_solver: The linear solver (KSP) which is used to solve the (linear) adjoint system. newton_linearizations: A (list of) UFL forms describing which (alternative) linearizations should be used for the (nonlinear) state equations when solving them (with Newton's method). The default is `None`, so that the Jacobian of the supplied state forms is used. """ super().__init__( state_forms, bcs_list, cost_functional_form, states, adjoints, constraint_list, config=config, initial_guess=initial_guess, ksp_options=ksp_options, adjoint_ksp_options=adjoint_ksp_options, gradient_ksp_options=gradient_ksp_options, preconditioner_forms=preconditioner_forms, pre_callback=pre_callback, post_callback=post_callback, linear_solver=linear_solver, adjoint_linear_solver=adjoint_linear_solver, newton_linearizations=newton_linearizations, ) self.controls = controls self.riesz_scalar_products = riesz_scalar_products self.control_bcs_list = control_bcs_list self.control_constraints = control_constraints def _solve_inner_problem( self, tol: float = 1e-2, inner_rtol: Optional[float] = None, inner_atol: Optional[float] = None, ) -> None: """Solves the inner (unconstrained) optimization problem. Args: tol: An overall tolerance to be used in the algorithm. This will set the relative tolerance for the inner optimization problems to ``tol``. Default is 1e-2. inner_rtol: Relative tolerance for the inner problem. Default is ``None``, so that ``inner_rtol = tol`` is used. inner_atol: Absolute tolerance for the inner problem. Default is ``None``, so that ``inner_atol = tol/10`` is used. """ super()._solve_inner_problem(tol, inner_rtol, inner_atol) optimal_control_problem = optimal_control.OptimalControlProblem( self.state_forms, self.bcs_list, self.solver.inner_cost_functional_form, self.states, self.controls, self.adjoints, config=self.config, riesz_scalar_products=self.riesz_scalar_products, control_constraints=self.control_constraints, initial_guess=self.initial_guess, ksp_options=self.ksp_options, adjoint_ksp_options=self.adjoint_ksp_options, gradient_ksp_options=self.gradient_ksp_options, control_bcs_list=self.control_bcs_list, preconditioner_forms=self.preconditioner_forms, pre_callback=self.pre_callback, post_callback=self.post_callback, linear_solver=self.linear_solver, adjoint_linear_solver=self.adjoint_linear_solver, newton_linearizations=self.newton_linearizations, ) optimal_control_problem.inject_pre_post_callback( self._pre_callback, self._post_callback ) optimal_control_problem.shift_cost_functional( self.solver.inner_cost_functional_shift ) if inner_atol is not None: atol = inner_atol else: atol = self.initial_norm * tol / 10.0 optimal_control_problem.solve(rtol=self.rtol, atol=atol) if self.solver.iterations == 1: self.initial_norm = optimal_control_problem.solver.gradient_norm_initial temp_problem = optimal_control.OptimalControlProblem( self.state_forms, self.bcs_list, self.cost_functional_form_initial, self.states, self.controls, self.adjoints, config=self.config, riesz_scalar_products=self.riesz_scalar_products, control_constraints=self.control_constraints, initial_guess=self.initial_guess, ksp_options=self.ksp_options, adjoint_ksp_options=self.adjoint_ksp_options, gradient_ksp_options=self.gradient_ksp_options, preconditioner_forms=self.preconditioner_forms, pre_callback=self.pre_callback, post_callback=self.post_callback, linear_solver=self.linear_solver, adjoint_linear_solver=self.adjoint_linear_solver, newton_linearizations=self.newton_linearizations, ) temp_problem.state_problem.has_solution = True self.current_function_value = temp_problem.reduced_cost_functional.evaluate()
[docs] class ConstrainedShapeOptimizationProblem(ConstrainedOptimizationProblem): """A shape optimization problem with additional (in-)equality constraints.""" def __init__( self, state_forms: Union[ufl.Form, List[ufl.Form]], bcs_list: Union[ fenics.DirichletBC, List[fenics.DirichletBC], List[List[fenics.DirichletBC]], None, ], cost_functional_form: Union[ List[_typing.CostFunctional], _typing.CostFunctional ], states: Union[fenics.Function, List[fenics.Function]], adjoints: Union[fenics.Function, List[fenics.Function]], boundaries: fenics.MeshFunction, constraint_list: Union[_typing.Constraint, List[_typing.Constraint]], config: Optional[io.Config] = None, shape_scalar_product: Optional[ufl.Form] = None, initial_guess: Optional[List[fenics.Function]] = None, ksp_options: Optional[Union[_typing.KspOption, List[_typing.KspOption]]] = None, adjoint_ksp_options: Optional[ Union[_typing.KspOption, List[_typing.KspOption]] ] = None, gradient_ksp_options: Optional[ Union[_typing.KspOption, List[_typing.KspOption]] ] = None, preconditioner_forms: Optional[List[ufl.Form]] = None, pre_callback: Optional[Callable] = None, post_callback: Optional[Callable] = None, linear_solver: Optional[_utils.linalg.LinearSolver] = None, adjoint_linear_solver: Optional[_utils.linalg.LinearSolver] = None, newton_linearizations: Optional[Union[ufl.Form, List[ufl.Form]]] = None, ) -> None: """Initializes self. Args: state_forms: The weak form of the state equation (user implemented). Can be either a single UFL form, or a (ordered) list of UFL forms. bcs_list: The list of :py:class:`fenics.DirichletBC` objects describing Dirichlet (essential) boundary conditions. If this is ``None``, then no Dirichlet boundary conditions are imposed. cost_functional_form: UFL form of the cost functional. Can also be a list of summands of the cost functional states: The state variable(s), can either be a :py:class:`fenics.Function`, or a list of these. adjoints: The adjoint variable(s), can either be a :py:class:`fenics.Function`, or a (ordered) list of these. boundaries: A :py:class:`fenics.MeshFunction` that indicates the boundary markers. constraint_list: (A list of) additional equality and inequality constraints for the problem. config: config: The config file for the problem, generated via :py:func:`cashocs.load_config`. Alternatively, this can also be ``None``, in which case the default configurations are used, except for the optimization algorithm. This has then to be specified in the :py:meth:`solve <cashocs.OptimalControlProblem.solve>` method. The default is ``None``. shape_scalar_product: The bilinear form for computing the shape gradient (or gradient deformation). This has to use :py:class:`fenics.TrialFunction` and :py:class:`fenics.TestFunction` objects to define the weak form, which have to be in a :py:class:`fenics.VectorFunctionSpace` of continuous, linear Lagrange finite elements. Moreover, this form is required to be symmetric. initial_guess: List of functions that act as initial guess for the state variables, should be valid input for :py:func:`fenics.assign`. Defaults to ``None``, which means a zero initial guess. ksp_options: A list of dicts corresponding to command line options for PETSc, used to solve the state systems. If this is ``None``, then the direct solver mumps is used (default is ``None``). adjoint_ksp_options: A list of dicts corresponding to command line options for PETSc, used to solve the adjoint systems. If this is ``None``, then the same options as for the state systems are used (default is ``None``). gradient_ksp_options: A list of dicts corresponding to command line options for PETSc, used to compute the (shape) gradient. If this is ``None``, either a direct or an iterative method is used (depending on the configuration, section OptimizationRoutine, key gradient_method). preconditioner_forms: The list of forms for the preconditioner. The default is `None`, so that the preconditioner matrix is the same as the system matrix. pre_callback: A function (without arguments) that will be called before each solve of the state system post_callback: A function (without arguments) that will be called after the computation of the gradient. linear_solver: The linear solver (KSP) which is used to solve the linear systems arising from the discretized PDE. adjoint_linear_solver: The linear solver (KSP) which is used to solve the (linear) adjoint system. newton_linearizations: A (list of) UFL forms describing which (alternative) linearizations should be used for the (nonlinear) state equations when solving them (with Newton's method). The default is `None`, so that the Jacobian of the supplied state forms is used. """ super().__init__( state_forms, bcs_list, cost_functional_form, states, adjoints, constraint_list, config=config, initial_guess=initial_guess, ksp_options=ksp_options, adjoint_ksp_options=adjoint_ksp_options, gradient_ksp_options=gradient_ksp_options, preconditioner_forms=preconditioner_forms, pre_callback=pre_callback, post_callback=post_callback, linear_solver=linear_solver, adjoint_linear_solver=adjoint_linear_solver, newton_linearizations=newton_linearizations, ) self.boundaries = boundaries self.shape_scalar_product = shape_scalar_product def _solve_inner_problem( self, tol: float = 1e-2, inner_rtol: Optional[float] = None, inner_atol: Optional[float] = None, ) -> None: """Solves the inner (unconstrained) optimization problem. Args: tol: An overall tolerance to be used in the algorithm. This will set the relative tolerance for the inner optimization problems to ``tol``. Default is 1e-2. inner_rtol: Relative tolerance for the inner problem. Default is ``None``, so that ``inner_rtol = tol`` is used. inner_atol: Absolute tolerance for the inner problem. Default is ``None``, so that ``inner_atol = tol/10`` is used. """ super()._solve_inner_problem(tol, inner_rtol, inner_atol) shape_optimization_problem = shape_optimization.ShapeOptimizationProblem( self.state_forms, self.bcs_list, self.solver.inner_cost_functional_form, self.states, self.adjoints, self.boundaries, config=self.config, shape_scalar_product=self.shape_scalar_product, initial_guess=self.initial_guess, ksp_options=self.ksp_options, adjoint_ksp_options=self.adjoint_ksp_options, gradient_ksp_options=self.gradient_ksp_options, preconditioner_forms=self.preconditioner_forms, pre_callback=self.pre_callback, post_callback=self.post_callback, linear_solver=self.linear_solver, adjoint_linear_solver=self.adjoint_linear_solver, newton_linearizations=self.newton_linearizations, ) shape_optimization_problem.inject_pre_post_callback( self._pre_callback, self._post_callback ) shape_optimization_problem.shift_cost_functional( self.solver.inner_cost_functional_shift ) if inner_atol is not None: atol = inner_atol else: atol = self.initial_norm * tol / 10.0 shape_optimization_problem.solve(rtol=self.rtol, atol=atol) if self.solver.iterations == 1: self.initial_norm = shape_optimization_problem.solver.gradient_norm_initial temp_problem = shape_optimization.ShapeOptimizationProblem( self.state_forms, self.bcs_list, self.cost_functional_form_initial, self.states, self.adjoints, self.boundaries, config=self.config, shape_scalar_product=self.shape_scalar_product, initial_guess=self.initial_guess, ksp_options=self.ksp_options, adjoint_ksp_options=self.adjoint_ksp_options, gradient_ksp_options=self.gradient_ksp_options, preconditioner_forms=self.preconditioner_forms, pre_callback=self.pre_callback, post_callback=self.post_callback, linear_solver=self.linear_solver, adjoint_linear_solver=self.adjoint_linear_solver, newton_linearizations=self.newton_linearizations, ) temp_problem.state_problem.has_solution = True self.current_function_value = temp_problem.reduced_cost_functional.evaluate()