# Copyright (C) 2020-2026 Fraunhofer ITWM, Sebastian Blauth and
# Leon Baeck
#
# 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/>.
"""Deflation procedure for topology optimization and optimal control."""
from __future__ import annotations
import abc
from collections.abc import Callable
import copy
import pathlib
from typing import TYPE_CHECKING
import fenics
try:
import ufl_legacy as ufl
except ImportError:
import ufl
from cashocs import _utils
from cashocs import io
from cashocs import log
from cashocs._database import database
from cashocs._optimization import cost_functional
from cashocs._optimization import optimal_control
from cashocs._optimization import topology_optimization
from cashocs.io import output
if TYPE_CHECKING:
from cashocs import _typing
class DeflatedProblem(abc.ABC):
"""Base class for deflated problems for topology optimization."""
def __init__( # pylint: disable=unused-argument
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: list[fenics.Function] | fenics.Function,
adjoints: list[fenics.Function] | fenics.Function,
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:
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.
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.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 = output.OutputManager(self.db)
self.abstract_control = fenics.Function
self.abstract_control_init = fenics.function
self.abstract_control_mapped = fenics.function
self.control_list_mapped: list[fenics.Function] = []
self.control_list_mapped_restart: list[fenics.Function] = []
self.control_list_mapped_final: list[fenics.Function] = []
self.control_list_final: list[fenics.Function] = []
self.dx: ufl.measure.Measure
self.initial_norm = 0.0
def distance_shapes(self) -> bool:
"""Computes the distance of a minimizer to previous ones.
Returns:
True if the distance of the new local minimizer to all previous
found ones exceeds the threshold gamma. False if at least one
distance does not meet the threshold (no new minimizer of the
actual problem found).
"""
for i in range(0, len(self.control_list_mapped_final) - 1):
dist = fenics.assemble(
fenics.inner(
self.control_list_mapped_final[i]
- self.control_list_mapped_restart[-1],
self.control_list_mapped_final[i]
- self.control_list_mapped_restart[-1],
)
* self.dx
)
if dist < 0.1 * self.gamma:
return False
return True
@abc.abstractmethod
def _solve_inner_problem(
self,
tol: float = 1e-2,
inner_rtol: float | None = None,
inner_atol: float | None = None,
iteration: int = 0,
angle_tol: float | None = 1.0,
restart: bool | None = False,
) -> None:
"""Solves the inner (unpenalized) 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.
angle_tol: The absolute tolerance for the angle between topological
derivative and levelset function. If this is ``None``, then
the value provided in the config file is used. Default is ``None``.
restart: If True a restart of the unpenalized optimization problem with
starting value as the minimizer of the perturbed optimization problem
is initiated.
"""
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)
def check_for_restart(self) -> bool:
"""Checks if a restart of the optimization of the unpenalized problem is needed.
Returns:
Bool that indicates if a restart of the unpenalized problem with
the local minimizer of the deflated problem is needed. This is the
case if not all penality functions vanish in the local minimizer of
the deflated problem.
"""
for i in range(1, len(self.cost_functional_form_deflation)):
val = self.cost_functional_form_deflation[i].evaluate()
if val > 1e-6:
return True
return False
def reset_starting_value(self) -> None:
"""Resets the starting value for the optimization problem."""
self.abstract_control.vector().vec().aypx(
0.0, self.abstract_control_init.vector().vec()
)
self.abstract_control.vector().apply("")
@abc.abstractmethod
def construct_penalty_functions(
self,
gamma: int | float,
delta: int | float,
) -> None:
"""Constructs the penalty functions for the deflation procedure.
Args:
gamma: Threshold value for the local support of the penalty function. If the
distance of two shapes is larger than this threshold, the penalty
function vanishes.
delta: Penalty parameter of the penalty function.
"""
pass
@abc.abstractmethod
def map_abstract_control(self) -> fenics.Function:
"""Maps the abstract control for the optimization problem.
Returns:
abstract_control_mapped: A Fenics.function.
"""
pass
def save_functions(self, argument: str | list[str]) -> None:
"""Stores the newly computed local minimizers during the deflation procedure.
Args:
argument: str that indicates in which list the abstract_control of the
newly computed minimizer gets stored. 'solution': local minimizer of the
deflated optimization problems. 'restart': local minimizers of the
restart procedure. 'final': (distinct) local minimizers of the
undeflated optimization problem.
"""
arg_list = _utils.enlist(argument)
abstract_control_temp = fenics.Function(self.abstract_control.function_space())
abstract_control_temp.vector().vec().aypx(
0.0, self.abstract_control.vector().vec()
)
abstract_control_temp.vector().apply("")
abstract_control_mapped_temp = self.map_abstract_control()
if "solution" in arg_list:
self.control_list_mapped.append(abstract_control_mapped_temp)
if "restart" in arg_list:
self.control_list_mapped_restart.append(abstract_control_mapped_temp)
if "final" in arg_list:
self.control_list_mapped_final.append(abstract_control_mapped_temp)
self.control_list_final.append(abstract_control_temp)
def solve(
self,
tol: float = 1e-2,
it_deflation: int = 5,
gamma: float = 0.5,
delta: float = 1.0,
inner_rtol: float | None = None,
inner_atol: float | None = None,
angle_tol: float | None = 1.0,
) -> None:
"""Deflation procedure to find multiple local minimizers.
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.
it_deflation: Number of performed deflation loops. Default is 5.
gamma: Parameter to control the support of the penalty functions for the
deflation procedure.
delta: Penalty parameter of the penalty functions for the deflation
procedure.
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.
angle_tol: Absolute tolerance for the inner topology optimization problem.
Default is ``None``.
"""
self.gamma = gamma
self.delta = delta
self.reset_starting_value()
log.begin("Begin of the deflation procedure.", level=log.INFO)
log.info("Iteration 0 of Deflation loop:")
self._solve_inner_problem(tol, inner_rtol, inner_atol, 0, angle_tol)
self.save_functions(["solution", "restart", "final"])
for num_defl in range(1, it_deflation + 1):
log.info(f"Iteration {num_defl} of Deflation loop:")
self.cost_functional_form_deflation = (
self.cost_functional_form_initial.copy()
)
self.construct_penalty_functions(gamma, delta)
self.reset_starting_value()
self._solve_inner_problem(tol, inner_rtol, inner_atol, num_defl, angle_tol)
self.save_functions("solution")
restart = self.check_for_restart()
self.cost_functional_form_deflation = (
self.cost_functional_form_initial.copy()
)
if restart:
log.info(
"Performing a Restart (At least one penalty function did "
"not vanish)"
)
self._solve_inner_problem(
tol, inner_rtol, inner_atol, num_defl, angle_tol, restart=True
)
self.save_functions("restart")
distance = self.distance_shapes()
if distance:
log.info("New local Minimizer found")
self.save_functions("final")
else:
log.info("Minimizer was already computed before")
log.end()
[docs]
class DeflatedTopologyOptimizationProblem(DeflatedProblem):
"""A deflated topology optimization problem."""
def __init__( # pylint: disable=unused-argument
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: list[fenics.Function] | fenics.Function,
adjoints: list[fenics.Function] | fenics.Function,
levelset_function: fenics.Function,
topological_derivative_neg: fenics.Function | ufl.Form,
topological_derivative_pos: fenics.Function | ufl.Form,
update_levelset: Callable,
volume_restriction: float | tuple[float, float] | None = None,
config: io.Config | None = None,
riesz_scalar_products: list[ufl.Form] | 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] | 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.
adjoints: The adjoint variable(s), can either be a
:py:class:`fenics.Function`, or a (ordered) list of these.
levelset_function: A :py:class:`fenics.Function` which represents the
levelset function.
topological_derivative_neg: The topological derivative inside the domain,
where the levelset function is negative.
topological_derivative_pos: The topological derivative inside the domain,
where the levelset function is positive.
update_levelset: A python function (without arguments) which is called to
update the coefficients etc. when the levelset function is changed.
volume_restriction: A volume restriction for the optimization problem.
A single float describes an equality constraint and a tuple of floats
an inequality constraint.
config: The config file for the problem, generated via
:py:func:`cashocs.create_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`` or a single UFL form. If it is ``None``, the
:math:`L^2(\Omega)` product is used (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 strings 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 strings 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,
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.abstract_control: fenics.Function = levelset_function
self.abstract_control_init = fenics.Function(
self.abstract_control.function_space()
)
self.abstract_control_init.vector().vec().aypx(
0.0, self.abstract_control.vector().vec()
)
self.abstract_control_init.vector().apply("")
self.topological_derivative_pos: fenics.Function | ufl.Form = (
topological_derivative_pos
)
self.topological_derivative_neg: fenics.Function | ufl.Form = (
topological_derivative_neg
)
self.update_levelset: Callable = update_levelset
self.riesz_scalar_products = riesz_scalar_products
self.volume_restriction = volume_restriction
self.db.function_db.controls = _utils.enlist(self.abstract_control)
self.db.function_db.control_spaces = [
x.function_space() for x in self.db.function_db.controls
]
self.db.parameter_db.problem_type = "topology"
self.mesh = self.abstract_control.function_space().mesh()
self.dg0_space = fenics.FunctionSpace(self.mesh, "DG", 0)
self.cost_functional_form_deflation = self.cost_functional_form_initial.copy()
self.dx = fenics.Measure("dx", self.abstract_control.function_space().mesh())
self.abstract_control_mapped = fenics.Function(self.dg0_space)
[docs]
def construct_penalty_functions(
self,
gamma: int | float,
delta: int | float,
) -> None:
"""Constructs the penalty functions for the deflation procedure.
Args:
gamma: Threshold value for the local support of the penalty function. If the
distance of two shapes is larger than this threshold, the penalty
function vanishes.
delta: Penalty parameter of the penalty function.
"""
self.abstract_control_mapped = fenics.Function(self.dg0_space)
for i in range(0, len(self.control_list_mapped)):
cost_function_deflation = cost_functional.DeflationFunctional(
gamma,
fenics.inner(
self.abstract_control_mapped - self.control_list_mapped[i],
self.abstract_control_mapped - self.control_list_mapped[i],
)
* self.dx,
(1 - 2 * self.control_list_mapped[i]),
delta,
)
self.cost_functional_form_deflation.append(cost_function_deflation)
[docs]
def map_abstract_control(self) -> fenics.Function:
"""Maps the abstract control for the optimization problem.
Returns:
abstract_control_mapped: The mapped abstract control is defined as the
characteristic function of the domain that is given by the level-set
function (abstract_control).
"""
abstract_control_mapped_temp = fenics.Function(self.dg0_space)
_utils.interpolate_levelset_function_to_cells(
self.abstract_control, 1.0, 0.0, abstract_control_mapped_temp
)
return abstract_control_mapped_temp
[docs]
def update_level_set_deflation(self) -> None:
"""update_levelset function for the deflated topology optimization problem."""
self.update_levelset()
_utils.interpolate_levelset_function_to_cells(
self.abstract_control, 1.0, 0.0, self.abstract_control_mapped
)
def _solve_inner_problem(
self,
tol: float = 1e-2,
inner_rtol: float | None = None,
inner_atol: float | None = None,
iteration: int = 0,
angle_tol: float | None = 1.0,
restart: float | None = False,
) -> None:
"""Solves the inner (unpenalized) 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
angle_tol: The absolute tolerance for the angle between topological
derivative and levelset function. If this is ``None``, then
the value provided in the config file is used. Default is ``None``.
restart: If True a restart of the unpenalized optimization problem with
starting value as the minimizer of the perturbed optimization problem
is initiated.
"""
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"))
if restart:
config.set(
"Output",
"result_dir",
str(output_path / f"subproblem_{iteration}_restart"),
)
else:
config.set(
"Output", "result_dir", str(output_path / f"subproblem_{iteration}")
)
topology_optimization_problem_inner = (
topology_optimization.TopologyOptimizationProblem(
self.state_forms,
self.bcs_list,
self.cost_functional_form_deflation,
self.states,
self.adjoints,
self.abstract_control,
self.topological_derivative_neg,
self.topological_derivative_pos,
self.update_level_set_deflation,
volume_restriction=self.volume_restriction,
config=config,
riesz_scalar_products=self.riesz_scalar_products,
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,
)
)
topology_optimization_problem_inner.inject_pre_post_callback(
self._pre_callback, self._post_callback
)
if inner_atol is not None:
atol = inner_atol
else:
atol = self.initial_norm * tol / 10.0
topology_optimization_problem_inner.solve(
rtol=self.rtol, atol=atol, angle_tol=angle_tol
)
[docs]
class DeflatedOptimalControlProblem(DeflatedProblem):
"""A deflated optimal control problem."""
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],
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.
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,
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.abstract_control = self.controls[0]
self.abstract_control_mapped = self.abstract_control
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"
self.abstract_control_init = fenics.Function(
self.abstract_control.function_space()
)
self.abstract_control_init.vector().vec().aypx(
0.0, self.abstract_control.vector().vec()
)
self.abstract_control_init.vector().apply("")
self.cost_functional_form_deflation = self.cost_functional_form_initial.copy()
self.dx = fenics.Measure("dx", self.abstract_control.function_space().mesh())
[docs]
def construct_penalty_functions(
self,
gamma: int | float,
delta: int | float,
) -> None:
"""Constructs the penalty functions for the deflation procedure.
Args:
gamma: Threshold value for the local support of the penalty function. If the
distance of two shapes is larger than this threshold, the penalty
function vanishes.
delta: Penalty parameter of the penalty function.
"""
for i in range(0, len(self.control_list_mapped)):
cost_function_deflation = cost_functional.DeflationFunctional(
self.gamma,
fenics.inner(
self.abstract_control - self.control_list_mapped[i],
self.abstract_control - self.control_list_mapped[i],
)
* self.dx,
ufl.Form,
self.delta,
)
self.cost_functional_form_deflation.append(cost_function_deflation)
[docs]
def map_abstract_control(self) -> fenics.Function:
"""Maps the abstract control for the optimization problem.
Returns:
abstract_control_mapped: The mapped abstract control coincides with the
abstract control.
"""
abstract_control_mapped_temp = fenics.Function(
self.abstract_control.function_space()
)
abstract_control_mapped_temp.vector().vec().aypx(
0.0, self.abstract_control.vector().vec()
)
abstract_control_mapped_temp.vector().apply("")
return abstract_control_mapped_temp
def _solve_inner_problem(
self,
tol: float = 1e-2,
inner_rtol: float | None = None,
inner_atol: float | None = None,
iteration: int = 0,
angle_tol: float | None = 1.0,
restart: bool | None = False,
) -> None:
"""Solves the inner (unpenalized) 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
angle_tol: The absolute tolerance for the angle between topological
derivative and levelset function. If this is ``None``, then
the value provided in the config file is used. Default is ``None``.
restart: If True a restart of the unpenalized optimization problem with
starting value as the minimizer of the perturbed optimization problem
is initiated.
"""
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"))
if restart:
config.set(
"Output",
"result_dir",
str(output_path / f"subproblem_{iteration}_restart"),
)
else:
config.set(
"Output", "result_dir", str(output_path / f"subproblem_{iteration}")
)
optimal_control_problem = optimal_control.OptimalControlProblem(
self.state_forms,
self.bcs_list,
self.cost_functional_form_deflation,
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
)
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)