Source code for cashocs.geometry.boundary_distance

# 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/>.

"""Compute the distance to the boundary."""

from __future__ import annotations

import copy
from typing import TYPE_CHECKING

import fenics

try:
    import ufl_legacy as ufl
except ImportError:
    import ufl
import numpy as np

from cashocs import _exceptions
from cashocs import _utils
from cashocs import nonlinear_solvers
from cashocs.geometry import measure

if TYPE_CHECKING:
    from cashocs import _typing


[docs] def compute_boundary_distance( mesh: fenics.Mesh, boundaries: fenics.MeshFunction | None = None, boundary_idcs: list[int | str] | None = None, tol: float = 1e-1, max_iter: int = 10, minimum_distance: float = 0.0, method: str = "eikonal", ) -> fenics.Function: """Computes (an approximation of) the distance to the boundary. The user can specify which boundaries are considered for the distance computation by specifying the parameters `boundaries` and `boundary_idcs`. Default is to consider all boundaries. Args: mesh: The dolfin mesh object, representing the computational domain. boundaries: A meshfunction for the boundaries, which is needed in case specific boundaries are targeted for the distance computation (while others are ignored), default is `None` (all boundaries are used). boundary_idcs: A list of indices which indicate, which parts of the boundaries should be used for the distance computation, default is `None` (all boundaries are used). tol: A tolerance for the iterative solution of the eikonal equation. Default is 1e-1. max_iter: Number of iterations for the iterative solution of the eikonal equation. Default is 10. minimum_distance: The distance of the mesh boundary to the (physical) wall. This should be set to 0.0 for most applications, which is also the default. One exception is for turbulence modeling and wall functions. method: Which method should be used to compute the boundary distance. Can either be 'poisson' or 'eikonal'. The default is 'eikonal', which is more accurate. The poisson approach is less accurate, but more robust and gives a very good approximation close to the wall. Returns: A fenics function representing an approximation of the distance to the boundary. """ if method == "poisson": return compute_boundary_distance_poisson( mesh, boundaries=boundaries, boundary_idcs=boundary_idcs, minimum_distance=minimum_distance, ) elif method == "eikonal": return compute_boundary_distance_eikonal( mesh, boundaries=boundaries, boundary_idcs=boundary_idcs, tol=tol, max_iter=max_iter, ) else: raise _exceptions.InputError( "compute_boundary_distance", "method", "The method can only be 'poisson' or 'eikonal'.", )
[docs] def compute_boundary_distance_poisson( mesh: fenics.Mesh, boundaries: fenics.MeshFunction | None = None, boundary_idcs: list[int | str] | None = None, minimum_distance: float = 0.0, ) -> fenics.Function: """Computes the distance to the boundary with a Poisson approach. The user can specify which boundaries are considered for the distance computation by specifying the parameters `boundaries` and `boundary_idcs`. Default is to consider all boundaries. The approach is described, e.g., in Section 1.1.3 of `Tucker, Differential equation-based wall distance computation for DES and RANS (2003) <https://doi.org/10.1016/S0021-9991(03)00272-9>`_. Args: mesh (fenics.Mesh): The dolfin mesh object, representing the computational domain boundaries (fenics.MeshFunction | None, optional): A meshfunction for the boundaries, which is needed in case specific boundaries are targeted for the distance computation (while others are ignored), default is `None` (all boundaries are used). Defaults to None. boundary_idcs (list[int | str] | None, optional): A list of indices which indicate, which parts of the boundaries should be used for the distance computation, default is `None` (all boundaries are used). Defaults to None. minimum_distance (float, optional): The distance of the mesh boundary to the (physical) wall. This should be set to 0.0 for most applications, which is also the default. One exception is for turbulence modeling and wall functions. Defaults to 0.0. Returns: A fenics function representing an approximation of the distance to the boundary. """ cg1_space = fenics.FunctionSpace(mesh, "CG", 1) dx = ufl.Measure("dx", domain=mesh) u = fenics.Function(cg1_space) v = fenics.TestFunction(cg1_space) poisson_form = ( ufl.dot(ufl.grad(u), ufl.grad(v)) * dx - fenics.Constant(1.0) * v * dx ) if (boundaries is not None) and (boundary_idcs is not None): if len(boundary_idcs) > 0: bcs = _utils.create_dirichlet_bcs( cg1_space, fenics.Constant(minimum_distance), boundaries, boundary_idcs ) else: bcs = fenics.DirichletBC( cg1_space, fenics.Constant(minimum_distance), fenics.CompiledSubDomain("on_boundary"), ) else: bcs = fenics.DirichletBC( cg1_space, fenics.Constant(minimum_distance), fenics.CompiledSubDomain("on_boundary"), ) ksp_options: _typing.KspOption = { "ksp_type": "cg", "pc_type": "hypre", "pc_hypre_type": "boomeramg", "pc_hypre_boomeramg_strong_threshold": 0.7, "ksp_rtol": 1e-12, "ksp_atol": 1e-50, "ksp_max_it": 1000, } nonlinear_solvers.linear_solve(poisson_form, u, bcs, ksp_options=ksp_options) distance = fenics.Function(cg1_space) form = ( distance * v * dx - ufl.sqrt(ufl.dot(ufl.grad(u), ufl.grad(u)) + fenics.Constant(2.0) * u) * v * dx + ufl.sqrt(ufl.dot(ufl.grad(u), ufl.grad(u))) * v * dx ) nonlinear_solvers.linear_solve(form, distance, bcs, ksp_options=ksp_options) return distance
[docs] def compute_boundary_distance_eikonal( mesh: fenics.Mesh, boundaries: fenics.MeshFunction | None = None, boundary_idcs: list[int | str] | None = None, tol: float = 1e-1, max_iter: int = 10, ) -> fenics.Function: """Computes the distance to the boundary by solving the Eikonal equation. The function iteratively solves the Eikonal equation to compute the distance to the boundary. The user can specify which boundaries are considered for the distance computation by specifying the parameters `boundaries` and `boundary_idcs`. Default is to consider all boundaries. Args: mesh: The dolfin mesh object, representing the computational domain boundaries: A meshfunction for the boundaries, which is needed in case specific boundaries are targeted for the distance computation (while others are ignored), default is `None` (all boundaries are used). boundary_idcs: A list of indices which indicate, which parts of the boundaries should be used for the distance computation, default is `None` (all boundaries are used). tol: A tolerance for the iterative solution of the eikonal equation. Default is 1e-1. max_iter: Number of iterations for the iterative solution of the eikonal equation. Default is 10. Returns: A fenics function representing an approximation of the distance to the boundary. """ function_space = fenics.FunctionSpace(mesh, "CG", 1) dx = measure.NamedMeasure("dx", mesh) ksp_options = copy.deepcopy(_utils.linalg.iterative_ksp_options) u = fenics.TrialFunction(function_space) v = fenics.TestFunction(function_space) u_curr = fenics.Function(function_space) u_prev = fenics.Function(function_space) norm_u_prev = ufl.sqrt(ufl.dot(ufl.grad(u_prev), ufl.grad(u_prev))) if (boundaries is not None) and (boundary_idcs is not None): if len(boundary_idcs) > 0: bcs = _utils.create_dirichlet_bcs( function_space, fenics.Constant(0.0), boundaries, boundary_idcs ) else: bcs = fenics.DirichletBC( function_space, fenics.Constant(0.0), fenics.CompiledSubDomain("on_boundary"), ) else: bcs = fenics.DirichletBC( function_space, fenics.Constant(0.0), fenics.CompiledSubDomain("on_boundary"), ) lhs = ufl.dot(ufl.grad(u), ufl.grad(v)) * dx rhs = fenics.Constant(1.0) * v * dx _utils.assemble_and_solve_linear(lhs, rhs, u_curr, bcs=bcs, ksp_options=ksp_options) rhs = ufl.dot(ufl.grad(u_prev) / norm_u_prev, ufl.grad(v)) * dx residual_form = ( pow( ufl.sqrt(ufl.dot(ufl.grad(u_curr), ufl.grad(u_curr))) - fenics.Constant(1.0), 2, ) * dx ) res_0 = np.sqrt(fenics.assemble(residual_form)) for _ in range(max_iter): u_prev.vector().vec().aypx(0.0, u_curr.vector().vec()) u_prev.vector().apply("") _utils.assemble_and_solve_linear( lhs, rhs, u_curr, bcs=bcs, ksp_options=ksp_options ) res = np.sqrt(fenics.assemble(residual_form)) if res <= res_0 * tol: break return u_curr