# Copyright (C) 2020-2026 Fraunhofer ITWM and 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 collections.abc import Callable
import copy
import pathlib
from typing import Literal, TYPE_CHECKING
import fenics
import numpy as np
try:
import ufl_legacy as ufl
except ImportError:
import ufl
from cashocs import _utils
from cashocs import io
from cashocs import log
from cashocs._constraints import solvers
from cashocs._database import database
from cashocs._optimization import optimal_control
from cashocs._optimization import shape_optimization
if TYPE_CHECKING:
from cashocs import _typing
class ConstrainedOptimizationProblem(abc.ABC):
"""An optimization problem with additional equality / inequality constraints."""
solver: solvers.AugmentedLagrangianMethod | solvers.QuadraticPenaltyMethod
def __init__(
self,
state_forms: list[ufl.Form] | ufl.Form,
bcs_list: (
list[list[fenics.DirichletBC]]
| list[fenics.DirichletBC]
| fenics.DirichletBC
),
cost_functional_form: list[_typing.CostFunctional] | _typing.CostFunctional,
states: fenics.Function | list[fenics.Function],
adjoints: fenics.Function | list[fenics.Function],
constraint_list: list[_typing.Constraint] | _typing.Constraint,
config: io.Config | None = None,
initial_guess: list[fenics.Function] | None = None,
ksp_options: _typing.KspOption | list[_typing.KspOption] | None = None,
adjoint_ksp_options: _typing.KspOption | list[_typing.KspOption] | None = None,
gradient_ksp_options: _typing.KspOption | list[_typing.KspOption] | None = None,
preconditioner_forms: list[ufl.Form] | None = None,
pre_callback: (
Callable[[], None] | Callable[[_typing.OptimizationProblem], None] | None
) = None,
post_callback: (
Callable[[], None] | Callable[[_typing.OptimizationProblem], None] | None
) = None,
linear_solver: _utils.linalg.LinearSolver | None = None,
adjoint_linear_solver: _utils.linalg.LinearSolver | None = None,
newton_linearizations: ufl.Form | list[ufl.Form] | None = None,
excluded_from_time_derivative: list[int] | list[list[int]] | None = 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.
excluded_from_time_derivative: For each state equation, a list of indices
which are not part of the first order time derivative for pseudo time
stepping. Example: Pressure for incompressible flow. Default is None.
"""
self.state_forms = _utils.enlist(state_forms)
self.bcs_list = _utils.check_and_enlist_bcs(bcs_list)
self.states = _utils.enlist(states)
self.adjoints = _utils.enlist(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.excluded_from_time_derivative = excluded_from_time_derivative
self.current_function_value = 0.0
self._pre_callback: (
Callable[[], None] | Callable[[_typing.OptimizationProblem], None] | None
) = None
self._post_callback: (
Callable[[], None] | Callable[[_typing.OptimizationProblem], None] | 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
self.db = database.Database(
self.config,
self.states,
self.adjoints,
self.ksp_options, # type: ignore
self.adjoint_ksp_options, # type: ignore
self.gradient_ksp_options, # type: ignore
self.cost_functional_form_initial,
self.state_forms,
self.bcs_list,
self.preconditioner_forms, # type: ignore
)
self.output_manager = io.output.OutputManager(self.db)
def solve(
self,
method: Literal[
"Augmented Lagrangian", "AL", "Quadratic Penalty", "QP"
] = "Augmented Lagrangian",
tol: float = 1e-2,
max_iter: int = 25,
inner_rtol: float | None = None,
inner_atol: float | None = None,
constraint_tol: float | None = None,
mu_0: float | None = None,
lambda_0: list[float] | None = 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"]:
log.begin(
"Solving the constrained problem with an augmented Lagrangian method."
)
self.solver = solvers.AugmentedLagrangianMethod(
self, mu_0=mu_0, lambda_0=lambda_0
)
elif method.casefold() in ["quadratic penalty", "qp"]:
log.begin(
"Solving the constrained problem with a quadratic penalty method."
)
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,
)
log.end()
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: float | None = None,
inner_atol: float | None = None,
iteration: int = 0,
) -> 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.
iteration: The current outer iteration count.
"""
self.rtol = inner_rtol or tol
def inject_pre_callback(
self,
function: (
Callable[[], None] | Callable[[_typing.OptimizationProblem], None] | 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: (
Callable[[], None] | Callable[[_typing.OptimizationProblem], None] | 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: (
Callable[[], None] | Callable[[_typing.OptimizationProblem], None] | None
),
post_function: (
Callable[[], None] | Callable[[_typing.OptimizationProblem], None] | 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: ufl.Form | list[ufl.Form],
bcs_list: (
fenics.DirichletBC
| list[fenics.DirichletBC]
| list[list[fenics.DirichletBC]]
),
cost_functional_form: list[_typing.CostFunctional] | _typing.CostFunctional,
states: fenics.Function | list[fenics.Function],
controls: fenics.Function | list[fenics.Function],
adjoints: fenics.Function | list[fenics.Function],
constraint_list: _typing.Constraint | list[_typing.Constraint],
config: io.Config | None = None,
riesz_scalar_products: ufl.Form | list[ufl.Form] | None = None,
control_constraints: list[list[float | fenics.Function]] | None = None,
initial_guess: list[fenics.Function] | None = None,
ksp_options: _typing.KspOption | list[_typing.KspOption] | None = None,
adjoint_ksp_options: _typing.KspOption | list[_typing.KspOption] | None = None,
gradient_ksp_options: _typing.KspOption | list[_typing.KspOption] | None = None,
control_bcs_list: (
fenics.DirichletBC
| list[fenics.DirichletBC]
| list[list[fenics.DirichletBC]]
| None
) = None,
preconditioner_forms: list[ufl.Form] | None = None,
pre_callback: Callable | None = None,
post_callback: Callable | None = None,
linear_solver: _utils.linalg.LinearSolver | None = None,
adjoint_linear_solver: _utils.linalg.LinearSolver | None = None,
newton_linearizations: ufl.Form | list[ufl.Form] | None = None,
excluded_from_time_derivative: list[int] | list[list[int]] | None = 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.
excluded_from_time_derivative: For each state equation, a list of indices
which are not part of the first order time derivative for pseudo time
stepping. Example: Pressure for incompressible flow. Default is None.
"""
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,
excluded_from_time_derivative=excluded_from_time_derivative,
)
self.controls = _utils.enlist(controls)
self.riesz_scalar_products = riesz_scalar_products
self.control_bcs_list = control_bcs_list
self.control_constraints = control_constraints
self.db.function_db.controls = self.controls
self.db.function_db.control_spaces = [
x.function_space() for x in self.db.function_db.controls
]
self.db.function_db.gradient = _utils.create_function_list(
self.db.function_db.control_spaces
)
self.db.parameter_db.problem_type = "control"
def _solve_inner_problem(
self,
tol: float = 1e-2,
inner_rtol: float | None = None,
inner_atol: float | None = None,
iteration: int = 0,
) -> 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.
iteration: The current outer iteration count
"""
super()._solve_inner_problem(
tol=tol, inner_rtol=inner_rtol, inner_atol=inner_atol, iteration=iteration
)
config = copy.deepcopy(self.config)
output_path = pathlib.Path(self.config.get("Output", "result_dir"))
config.set("Output", "result_dir", str(output_path / f"subproblem_{iteration}"))
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=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,
excluded_from_time_derivative=self.excluded_from_time_derivative,
)
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
config = copy.deepcopy(self.config)
config.set("Output", "save_state", "False")
config.set("Output", "save_adjoint", "False")
config.set("Output", "save_gradient", "False")
temp_problem = optimal_control.OptimalControlProblem(
self.state_forms,
self.bcs_list,
self.cost_functional_form_initial,
self.states,
self.controls,
self.adjoints,
config=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,
excluded_from_time_derivative=self.excluded_from_time_derivative,
)
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: ufl.Form | list[ufl.Form],
bcs_list: (
fenics.DirichletBC
| list[fenics.DirichletBC]
| list[list[fenics.DirichletBC]]
| None
),
cost_functional_form: list[_typing.CostFunctional] | _typing.CostFunctional,
states: fenics.Function | list[fenics.Function],
adjoints: fenics.Function | list[fenics.Function],
boundaries: fenics.MeshFunction,
constraint_list: _typing.Constraint | list[_typing.Constraint],
config: io.Config | None = None,
shape_scalar_product: ufl.Form | None = None,
initial_guess: list[fenics.Function] | None = None,
ksp_options: _typing.KspOption | list[_typing.KspOption] | None = None,
adjoint_ksp_options: _typing.KspOption | list[_typing.KspOption] | None = None,
gradient_ksp_options: _typing.KspOption | list[_typing.KspOption] | None = None,
preconditioner_forms: list[ufl.Form] | None = None,
pre_callback: Callable | None = None,
post_callback: Callable | None = None,
linear_solver: _utils.linalg.LinearSolver | None = None,
adjoint_linear_solver: _utils.linalg.LinearSolver | None = None,
newton_linearizations: ufl.Form | list[ufl.Form] | None = None,
excluded_from_time_derivative: list[int] | list[list[int]] | None = 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.
excluded_from_time_derivative: For each state equation, a list of indices
which are not part of the first order time derivative for pseudo time
stepping. Example: Pressure for incompressible flow. Default is None.
"""
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,
excluded_from_time_derivative=excluded_from_time_derivative,
)
self.boundaries = boundaries
self.shape_scalar_product = shape_scalar_product
if shape_scalar_product is None:
deformation_space: fenics.FunctionSpace = fenics.VectorFunctionSpace(
self.db.geometry_db.mesh, "CG", 1
)
else:
deformation_space = shape_scalar_product.arguments()[0].ufl_function_space()
self.db.function_db.control_spaces = [deformation_space]
self.db.function_db.gradient = [
fenics.Function(self.db.function_db.control_spaces[0])
]
self.db.parameter_db.problem_type = "shape"
def _solve_inner_problem(
self,
tol: float = 1e-2,
inner_rtol: float | None = None,
inner_atol: float | None = None,
iteration: int = 0,
) -> 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.
iteration: The current outer iteration count.
"""
super()._solve_inner_problem(
tol=tol, inner_rtol=inner_rtol, inner_atol=inner_atol, iteration=iteration
)
config = copy.deepcopy(self.config)
output_path = pathlib.Path(self.config.get("Output", "result_dir"))
config.set("Output", "result_dir", str(output_path / f"subproblem_{iteration}"))
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=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,
excluded_from_time_derivative=self.excluded_from_time_derivative,
)
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
config = copy.deepcopy(self.config)
config.set("Output", "save_state", "False")
config.set("Output", "save_adjoint", "False")
config.set("Output", "save_gradient", "False")
temp_problem = shape_optimization.ShapeOptimizationProblem(
self.state_forms,
self.bcs_list,
self.cost_functional_form_initial,
self.states,
self.adjoints,
self.boundaries,
config=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,
excluded_from_time_derivative=self.excluded_from_time_derivative,
)
temp_problem.state_problem.has_solution = True
self.current_function_value = temp_problem.reduced_cost_functional.evaluate()