# 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/>.
"""Utilities for UFL forms."""
from __future__ import annotations
from typing import Any, TypeVar
import fenics
import numpy as np
try:
import ufl_legacy as ufl
except ImportError:
import ufl
from cashocs import _exceptions
from cashocs import _utils
from cashocs import log
from cashocs.geometry.mesh import CashocsMesh
T = TypeVar("T")
def summation(x: list[T]) -> T | fenics.Constant:
"""Sums elements of a list in a UFL friendly fashion.
This can be used to sum, e.g., UFL forms, or UFL expressions that can be used in UFL
forms.
Args:
x: The list of entries that shall be summed.
Returns:
Sum of input (same type as entries of input).
Notes:
For "usual" summation of integers or floats, the built-in sum function
of python or the numpy variant are recommended. Still, they are
incompatible with FEniCS objects, so this function should be used for
the latter.
"""
if len(x) == 0:
y = fenics.Constant(0.0)
log.warning("Empty list handed to summation, returning 0.")
else:
y = x[0]
for item in x[1:]:
y += item # type: ignore
return y
def multiplication(x: list[T]) -> T | fenics.Constant:
"""Multiplies the elements of a list in a UFL friendly fashion.
Used to build the product of certain UFL expressions to construct a UFL form.
Args:
x: The list whose entries shall be multiplied.
Returns:
The result of the multiplication.
"""
if len(x) == 0:
y = fenics.Constant(1.0)
log.warning("Empty list handed to multiplication, returning 1.")
else:
y = x[0]
for item in x[1:]:
y *= item # type: ignore
return y
def max_(a: float | fenics.Function, b: float | fenics.Function) -> ufl.core.expr.Expr:
"""Computes the maximum of a and b.
Args:
a: The first parameter.
b: The second parameter.
Returns:
The maximum of a and b.
"""
return (a + b + abs(a - b)) / fenics.Constant(2.0)
def min_(a: float | fenics.Function, b: float | fenics.Function) -> ufl.core.expr.Expr:
"""Computes the minimum of a and b.
Args:
a: The first parameter.
b: The second parameter.
Returns:
The minimum of a and b.
"""
return (a + b - abs(a - b)) / fenics.Constant(2.0)
def moreau_yosida_regularization(
term: ufl.core.expr.Expr,
gamma: float,
measure: ufl.Measure,
lower_threshold: float | fenics.Function | None = None,
upper_threshold: float | fenics.Function | None = None,
shift_lower: float | fenics.Function | None = None,
shift_upper: float | fenics.Function | None = None,
) -> ufl.Form:
r"""Implements a Moreau-Yosida regularization of an inequality constraint.
The general form of the inequality is of the form ::
lower_threshold <= term <= upper_threshold
which is defined over the region specified in ``measure``.
In case ``lower_threshold`` or ``upper_threshold`` are ``None``, they are set to
:math:`-\infty` and :math:`\infty`, respectively.
Args:
term: The term inside the inequality constraint.
gamma: The weighting factor of the regularization.
measure: The measure over which the inequality constraint is defined.
lower_threshold: The lower threshold for the inequality constraint. In case this
is ``None``, the lower bound is set to :math:`-\infty`. The default is
``None``
upper_threshold: The upper threshold for the inequality constraint. In case this
is ``None``, the upper bound is set to :math:`\infty`. The default is
``None``
shift_lower: A shift function for the lower bound of the Moreau-Yosida
regularization. Should be non-positive. In case this is ``None``, it is set
to 0. Default is ``None``.
shift_upper: A shift function for the upper bound of the Moreau-Yosida
regularization. Should be non-negative. In case this is ``None``, it is set
to 0. Default is ``None``.
Returns:
The ufl form of the Moreau-Yosida regularization, to be used in the cost
functional.
"""
reg_lower = None
reg_upper = None
if lower_threshold is None and upper_threshold is None:
raise _exceptions.InputError(
"cashocs._utils.moreau_yosida_regularization",
"upper_threshold, lower_threshold",
"At least one of the threshold parameters has to be defined.",
)
if shift_lower is None:
shift_lower = fenics.Constant(0.0)
if shift_upper is None:
shift_upper = fenics.Constant(0.0)
reg = []
if lower_threshold is not None:
reg_lower = (
fenics.Constant(1 / (2 * gamma))
* pow(
min_(
shift_lower + fenics.Constant(gamma) * (term - lower_threshold),
fenics.Constant(0.0),
),
2,
)
* measure
)
reg.append(reg_lower)
if upper_threshold is not None:
reg_upper = (
fenics.Constant(1 / (2 * gamma))
* pow(
max_(
shift_upper + fenics.Constant(gamma) * (term - upper_threshold),
fenics.Constant(0.0),
),
2,
)
* measure
)
reg.append(reg_upper)
return summation(reg)
[docs]
def create_dirichlet_bcs(
function_space: fenics.FunctionSpace,
value: fenics.Constant | fenics.Expression | fenics.Function | float | tuple[float],
boundaries: fenics.MeshFunction,
idcs: list[int | str] | int | str,
**kwargs: Any,
) -> list[fenics.DirichletBC]:
"""Create several Dirichlet boundary conditions at once.
Wraps multiple Dirichlet boundary conditions into a list, in case
they have the same value but are to be defined for multiple boundaries
with different markers. Particularly useful for defining homogeneous
boundary conditions.
Args:
function_space: The function space onto which the BCs should be imposed on.
value: The value of the boundary condition. Has to be compatible with the
function_space, so that it could also be used as
``fenics.DirichletBC(function_space, value, ...)``.
boundaries: The :py:class:`fenics.MeshFunction` object representing the
boundaries.
idcs: A list of indices / boundary markers that determine the boundaries
onto which the Dirichlet boundary conditions should be applied to.
Can also be a single entry for a single boundary. If your mesh file
is named, then you can also use the names of the boundaries to define the
boundary conditions.
**kwargs: Keyword arguments for fenics.DirichletBC
Returns:
A list of DirichletBC objects that represent the boundary conditions.
Examples:
Generate homogeneous Dirichlet boundary conditions for all 4 sides of
the unit square ::
import fenics
import cashocs
mesh, _, _, _, _, _ = cashocs.regular_mesh(25)
V = fenics.FunctionSpace(mesh, 'CG', 1)
bcs = cashocs.create_dirichlet_bcs(V, fenics.Constant(0), boundaries,
[1,2,3,4])
"""
mesh = function_space.mesh()
if not isinstance(idcs, list):
idcs = [idcs]
bcs_list = []
for entry in idcs:
int_tag = _utils.tag_to_int(mesh.physical_groups, entry, "ds")
if int_tag:
bcs_list.append(
fenics.DirichletBC(function_space, value, boundaries, int_tag, **kwargs)
)
return bcs_list
def bilinear_boundary_form_modification(forms: list[ufl.Form]) -> list[ufl.Form]:
"""Modifies a bilinear form for the case it is given on the boundary only.
This avoids a bug in fenics.SystemAssembler where the matrices' sparsity pattern
is not initialized correctly.
"""
mod_forms = []
for form in forms:
trial, test = form.arguments()
mesh = trial.function_space().mesh()
dx = ufl.Measure("dx", domain=mesh)
mod_forms.append(form + fenics.Constant(0.0) * ufl.dot(trial, test) * dx)
return mod_forms
def _get_subdomain_ids_from_tag(
tag: tuple[str | int] | str | int, physical_groups: dict
) -> list[int]:
if isinstance(tag, tuple):
subdomain_idx = []
for t in tag:
tag_int = _utils.tag_to_int(physical_groups, t, "dx")
if tag_int is not None:
subdomain_idx.append(tag_int)
else:
tag_int = _utils.tag_to_int(physical_groups, tag, "dx")
subdomain_idx = []
if tag_int is not None:
subdomain_idx.append(tag_int)
return subdomain_idx
[docs]
def create_material_parameter(
material_dict: dict,
mesh: CashocsMesh,
dg0_space: fenics.FunctionSpace | None = None,
) -> ufl.core.expr.Expr:
r"""Creates a material parameter that can vary for different subdomains.
Args:
material_dict: The dictionary that contains the material parameters.
The keys are the indices of the subdomains (as generated with Gmsh) and
the values are the values of the material parameter on the respective
subdomain. If multiple subdomains share the same value, they can also
be put inside a tuple and used as a single key for the dictionary.
mesh: The finite element mesh.
dg0_space: The space of DG0 elements on the mesh. If this is None, then the
space is created internally.
Returns:
A UFL expression that can be used as material parameter.
Examples:
Consider the following Poisson problem
.. math::
- \nabla \cdot (\mu \nabla u) = f \text{ in } \Omega \quad u = 0
\text{ on } \Gamma,
where the parameter :math:`\mu` varies between two subdomains, i.e.,
:math:`\mu = \mu_{in}` in :math:`\Omega_{in}` and :math:`\mu = \mu_{out}` in
:math:`\Omega_{out}`, where :math:`\Omega = \Omega_{in} \cup \Omega_{out}`.
The parameter :math:`\mu` can be created with the help of this function.
Assume that :math:`\Omega_{in}` has the index `1` and that
:math:`\Omega_{out}` has index `2`, then we can use the following code ::
mesh, subdomains, boundaries, dx, ds, dS = cashocs.import_mesh(...)
mu_in = 1.0 # example
mu_out = 10.0 # example
material_dict = {1: mu_in, 2: mu_out}
mu = cashocs.create_material_parameter(material_dict)
"""
indicator_dict = {}
if dg0_space is None:
dg0_space = fenics.FunctionSpace(mesh, "DG", 0)
for subdomain_tag in material_dict.keys():
indicator_function = fenics.Function(dg0_space)
subdomain_ids = _get_subdomain_ids_from_tag(subdomain_tag, mesh.physical_groups)
dg0_idx = np.flatnonzero(np.isin(mesh.subdomains.array(), subdomain_ids))
indicator_function.vector()[dg0_idx] = 1.0
indicator_function.vector().apply("")
indicator_dict[subdomain_tag] = indicator_function
material_parameter = _utils.summation(
[indicator_dict[key] * material_dict[key] for key in material_dict.keys()]
)
return material_parameter