Source code for cashocs.geometry.mesh

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

"""Basic mesh generation."""

from __future__ import annotations

import collections
import functools
import time
from typing import Any, Callable, List, Optional

import fenics
import numpy as np
from typing_extensions import Literal
from typing_extensions import TYPE_CHECKING

from cashocs import _exceptions
from cashocs import _loggers
from cashocs.geometry import measure

if TYPE_CHECKING:
    from mpi4py import MPI

    from cashocs import _typing


def _get_mesh_stats(
    mode: Literal["import", "generate"] = "import"
) -> Callable[..., Callable[..., _typing.MeshTuple]]:
    """A decorator for mesh importing / generating function which prints stats.

    Args:
        mode: A string indicating whether the mesh is being generated or imported.

    Returns:
        The decorated function.

    """

    def decorator_stats(
        func: Callable[..., _typing.MeshTuple]
    ) -> Callable[..., _typing.MeshTuple]:
        """A decorator for a mesh generating function.

        Args:
            func: The function to be decorated.

        Returns:
            The decorated function

        """

        @functools.wraps(func)
        def wrapper_stats(*args: Any, **kwargs: Any) -> _typing.MeshTuple:
            """Wrapper function for mesh generating functions.

            Args:
                *args: The arguments for the function.
                **kwargs: The keyword arguments for the function.

            Returns:
                The wrapped function.

            """
            word = "importing" if mode.casefold() == "import" else "generating"
            worded = "imported" if mode.casefold() == "import" else "generated"
            mpi_size = fenics.MPI.size(fenics.MPI.comm_world)
            start_time = time.time()
            _loggers.info(f"{word.capitalize()} mesh.")

            value = func(*args, **kwargs)
            dim = value[0].geometry().dim()

            end_time = time.time()
            _loggers.info(
                f"Done {word} mesh. Elapsed time: {end_time - start_time:.2f} s."
            )
            _loggers.info(
                f"Successfully {worded} {dim}-dimensional mesh on {mpi_size} CPU(s)."
            )
            _loggers.info(
                f"Mesh contains {value[0].num_entities_global(0):,} vertices and "
                f"{value[0].num_entities_global(dim):,} cells of type "
                f"{value[0].ufl_cell().cellname()}.\n"
            )
            return value

        return wrapper_stats

    return decorator_stats


[docs] @_get_mesh_stats(mode="generate") def interval_mesh( n: int = 10, start: float = 0.0, end: float = 1.0, partitions: Optional[List[float]] = None, comm: Optional[MPI.Comm] = None, ) -> _typing.MeshTuple: r"""Creates an 1D interval mesh starting at x=0 to x=length. This function creates a uniform mesh of a 1D interval, starting at the ``start`` and ending at ``end``. The resulting mesh uses ``n`` sub-intervals to discretize the geometry. The boundary markers are as follows: - 1 corresponds to :math:`x=start` - 2 corresponds to :math:`x=end` Args: n: Number of elements for discretizing the interval, default is 10 start: The start of the interval, default is 0.0 end: The end of the interval, default is 1.0 partitions: Points in the interval at which a partition in subdomains should be made. The resulting volume measure is sorted ascendingly according to the sub-intervals defined in partitions (starting at 1). Defaults to ``None``. comm: MPI communicator that is to be used for creating the mesh. Returns: A tuple (mesh, subdomains, boundaries, dx, ds, dS), where mesh is the imported FEM mesh, subdomains is a mesh function for the subdomains, boundaries is a mesh function for the boundaries, dx is a volume measure, ds is a surface measure, and dS is a measure for the interior facets. """ if end <= start: raise _exceptions.InputError( "cashocs.geometry.interval_mesh", "end", "end needs to be larger than start" ) if partitions is not None: if not all(x < y for x, y in zip(partitions, partitions[1:])): raise _exceptions.InputError( "cashocs.geometry.interval_mesh", "partitions", "partitions must be strictly increasing", ) n = int(n) dim = 1 if comm is None: comm = fenics.MPI.comm_world mesh = fenics.IntervalMesh(comm, n, start, end) subdomains = fenics.MeshFunction("size_t", mesh, dim=dim) boundaries = fenics.MeshFunction("size_t", mesh, dim=dim - 1) x_min = fenics.CompiledSubDomain( "on_boundary && near(x[0], start, tol)", tol=fenics.DOLFIN_EPS, start=start ) x_max = fenics.CompiledSubDomain( "on_boundary && near(x[0], end, tol)", tol=fenics.DOLFIN_EPS, end=end ) x_min.mark(boundaries, 1) x_max.mark(boundaries, 2) if partitions is not None: padded_partitions = collections.deque(partitions) padded_partitions.appendleft(start) padded_partitions.append(end) for i in range(len(padded_partitions) - 1): start_point = padded_partitions[i] end_point = padded_partitions[i + 1] part = fenics.CompiledSubDomain( "x[0] >= start_point && x[0] <= end_point", start_point=start_point, end_point=end_point, ) part.mark(subdomains, i + 1) dx = measure.NamedMeasure("dx", mesh, subdomain_data=subdomains) ds = measure.NamedMeasure("ds", mesh, subdomain_data=boundaries) d_interior_facet = measure.NamedMeasure("dS", mesh) return mesh, subdomains, boundaries, dx, ds, d_interior_facet
[docs] @_get_mesh_stats(mode="generate") def regular_mesh( n: int = 10, length_x: float = 1.0, length_y: float = 1.0, length_z: Optional[float] = None, diagonal: Literal["left", "right", "left/right", "right/left", "crossed"] = "right", comm: Optional[MPI.Comm] = None, ) -> _typing.MeshTuple: r"""Creates a mesh corresponding to a rectangle or cube. This function creates a uniform mesh of either a rectangle or a cube, starting at the origin and having length specified in ``length_x``, ``length_y``, and ``length_z``. The resulting mesh uses ``n`` elements along the shortest direction and accordingly many along the longer ones. The resulting domain is .. math:: \begin{alignedat}{2} &[0, length_x] \times [0, length_y] \quad &&\text{ in } 2D, \\ &[0, length_x] \times [0, length_y] \times [0, length_z] \quad &&\text{ in } 3D. \end{alignedat} The boundary markers are ordered as follows: - 1 corresponds to :math:`x=0`. - 2 corresponds to :math:`x=length_x`. - 3 corresponds to :math:`y=0`. - 4 corresponds to :math:`y=length_y`. - 5 corresponds to :math:`z=0` (only in 3D). - 6 corresponds to :math:`z=length_z` (only in 3D). Args: n: Number of elements in the shortest coordinate direction. length_x: Length in x-direction. length_y: Length in y-direction. length_z: Length in z-direction, if this is ``None``, then the geometry will be two-dimensional (default is ``None``). diagonal: This defines the type of diagonal used to create the box mesh in 2D. This can be one of ``"right"``, ``"left"``, ``"left/right"``, ``"right/left"`` or ``"crossed"``. comm: MPI communicator that is to be used for creating the mesh. Returns: A tuple (mesh, subdomains, boundaries, dx, ds, dS), where mesh is the imported FEM mesh, subdomains is a mesh function for the subdomains, boundaries is a mesh function for the boundaries, dx is a volume measure, ds is a surface measure, and dS is a measure for the interior facets. """ if length_x <= 0.0: raise _exceptions.InputError( "cashocs.geometry.regular_mesh", "length_x", "length_x needs to be positive" ) if length_y <= 0.0: raise _exceptions.InputError( "cashocs.geometry.regular_mesh", "length_y", "length_y needs to be positive" ) if not (length_z is None or length_z > 0.0): raise _exceptions.InputError( "cashocs.geometry.regular_mesh", "length_z", "length_z needs to be positive or None (for 2D mesh)", ) n = int(n) if comm is None: comm = fenics.MPI.comm_world if length_z is None: sizes = [length_x, length_y] dim = 2 else: sizes = [length_x, length_y, length_z] dim = 3 size_min = np.min(sizes) num_points = [int(np.round(length / size_min * n)) for length in sizes] if length_z is None: mesh = fenics.RectangleMesh( comm, fenics.Point(0, 0), fenics.Point(sizes), num_points[0], num_points[1], diagonal=diagonal, ) else: mesh = fenics.BoxMesh( comm, fenics.Point(0, 0, 0), fenics.Point(sizes), num_points[0], num_points[1], num_points[2], ) subdomains = fenics.MeshFunction("size_t", mesh, dim=dim) boundaries = fenics.MeshFunction("size_t", mesh, dim=dim - 1) x_min = fenics.CompiledSubDomain( "on_boundary && near(x[0], 0, tol)", tol=fenics.DOLFIN_EPS ) x_max = fenics.CompiledSubDomain( "on_boundary && near(x[0], length, tol)", tol=fenics.DOLFIN_EPS, length=sizes[0] ) x_min.mark(boundaries, 1) x_max.mark(boundaries, 2) y_min = fenics.CompiledSubDomain( "on_boundary && near(x[1], 0, tol)", tol=fenics.DOLFIN_EPS ) y_max = fenics.CompiledSubDomain( "on_boundary && near(x[1], length, tol)", tol=fenics.DOLFIN_EPS, length=sizes[1] ) y_min.mark(boundaries, 3) y_max.mark(boundaries, 4) if length_z is not None: z_min = fenics.CompiledSubDomain( "on_boundary && near(x[2], 0, tol)", tol=fenics.DOLFIN_EPS ) z_max = fenics.CompiledSubDomain( "on_boundary && near(x[2], length, tol)", tol=fenics.DOLFIN_EPS, length=sizes[2], ) z_min.mark(boundaries, 5) z_max.mark(boundaries, 6) dx = measure.NamedMeasure("dx", mesh, subdomain_data=subdomains) ds = measure.NamedMeasure("ds", mesh, subdomain_data=boundaries) d_interior_facet = measure.NamedMeasure("dS", mesh) return mesh, subdomains, boundaries, dx, ds, d_interior_facet
[docs] @_get_mesh_stats(mode="generate") def regular_box_mesh( n: int = 10, start_x: float = 0.0, start_y: float = 0.0, start_z: Optional[float] = None, end_x: float = 1.0, end_y: float = 1.0, end_z: Optional[float] = None, diagonal: Literal["right", "left", "left/right", "right/left", "crossed"] = "right", comm: Optional[MPI.Comm] = None, ) -> _typing.MeshTuple: r"""Creates a mesh corresponding to a rectangle or cube. This function creates a uniform mesh of either a rectangle or a cube, with specified start (``S_``) and end points (``E_``). The resulting mesh uses ``n`` elements along the shortest direction and accordingly many along the longer ones. The resulting domain is .. math:: \begin{alignedat}{2} &[start_x, end_x] \times [start_y, end_y] \quad &&\text{ in } 2D, \\ &[start_x, end_x] \times [start_y, end_y] \times [start_z, end_z] \quad &&\text{ in } 3D. \end{alignedat} The boundary markers are ordered as follows: - 1 corresponds to :math:`x=start_x`. - 2 corresponds to :math:`x=end_x`. - 3 corresponds to :math:`y=start_y`. - 4 corresponds to :math:`y=end_y`. - 5 corresponds to :math:`z=start_z` (only in 3D). - 6 corresponds to :math:`z=end_z` (only in 3D). Args: n: Number of elements in the shortest coordinate direction. start_x: Start of the x-interval. start_y: Start of the y-interval. start_z: Start of the z-interval, mesh is 2D if this is ``None`` (default is ``None``). end_x: End of the x-interval. end_y: End of the y-interval. end_z: End of the z-interval, mesh is 2D if this is ``None`` (default is ``None``). diagonal: This defines the type of diagonal used to create the box mesh in 2D. This can be one of ``"right"``, ``"left"``, ``"left/right"``, ``"right/left"`` or ``"crossed"``. comm: MPI communicator that is to be used for creating the mesh. Returns: A tuple (mesh, subdomains, boundaries, dx, ds, dS), where mesh is the imported FEM mesh, subdomains is a mesh function for the subdomains, boundaries is a mesh function for the boundaries, dx is a volume measure, ds is a surface measure, and dS is a measure for the interior facets. """ n = int(n) dim = 2 sizes = [1.0, 1.0] if comm is None: comm = fenics.MPI.comm_world if start_z is None and end_z is None: lx = end_x - start_x ly = end_y - start_y sizes = [lx, ly] dim = 2 else: if start_z is not None and end_z is not None: if start_z < end_z: lx = end_x - start_x ly = end_y - start_y lz = end_z - start_z sizes = [lx, ly, lz] dim = 3 else: raise _exceptions.InputError( "cashocs.geometry.regular_box_mesh", "start_z", "Incorrect input for the z-coordinate. " "start_z has to be smaller than end_z, " "or only one of them is specified.", ) _check_sizes(sizes) size_min = np.min(sizes) num_points = [int(np.round(length / size_min * n)) for length in sizes] if start_z is None: mesh = fenics.RectangleMesh( comm, fenics.Point(start_x, start_y), fenics.Point(end_x, end_y), num_points[0], num_points[1], diagonal=diagonal, ) else: mesh = fenics.BoxMesh( comm, fenics.Point(start_x, start_y, start_z), fenics.Point(end_x, end_y, end_z), num_points[0], num_points[1], num_points[2], ) subdomains = fenics.MeshFunction("size_t", mesh, dim=dim) boundaries = fenics.MeshFunction("size_t", mesh, dim=dim - 1) x_min = fenics.CompiledSubDomain( "on_boundary && near(x[0], sx, tol)", tol=fenics.DOLFIN_EPS, sx=start_x ) x_max = fenics.CompiledSubDomain( "on_boundary && near(x[0], ex, tol)", tol=fenics.DOLFIN_EPS, ex=end_x ) x_min.mark(boundaries, 1) x_max.mark(boundaries, 2) y_min = fenics.CompiledSubDomain( "on_boundary && near(x[1], sy, tol)", tol=fenics.DOLFIN_EPS, sy=start_y ) y_max = fenics.CompiledSubDomain( "on_boundary && near(x[1], ey, tol)", tol=fenics.DOLFIN_EPS, ey=end_y ) y_min.mark(boundaries, 3) y_max.mark(boundaries, 4) if start_z is not None: z_min = fenics.CompiledSubDomain( "on_boundary && near(x[2], sz, tol)", tol=fenics.DOLFIN_EPS, sz=start_z ) z_max = fenics.CompiledSubDomain( "on_boundary && near(x[2], ez, tol)", tol=fenics.DOLFIN_EPS, ez=end_z ) z_min.mark(boundaries, 5) z_max.mark(boundaries, 6) dx = measure.NamedMeasure("dx", mesh, subdomain_data=subdomains) ds = measure.NamedMeasure("ds", mesh, subdomain_data=boundaries) d_interior_facet = measure.NamedMeasure("dS", mesh) return mesh, subdomains, boundaries, dx, ds, d_interior_facet
def _check_sizes(sizes: List[float]) -> None: for size in sizes: if size <= 0: raise _exceptions.InputError( "cashocs.geometry.regular_box_mesh", "start_", "The start values have to be smaller than the end values.", )