Skip to content

fastfem.mesh

The fastfem.mesh package contains utilities for generating and reading meshes.

RectangleMesh dataclass

Bases: Mesh

A class that is identical to the Mesh class, but with typing hints for a rectangle mesh.

Source code in fastfem/mesh/fundamentals.py
class RectangleMesh(Mesh):
    """A class that is identical to the `Mesh` class, but with typing hints for a
    rectangle mesh."""

    def __getitem__(  # type: ignore
        self,
        key: RectangleDomainName,
    ) -> Domain:
        return self[key]

Line dataclass

Create a line with the given points.

Parameters:

  • start_point (Point) –

    The start point of the line.

  • end_point (Point) –

    The end point of the line.

  • number_of_nodes (Optional[int], default: None ) –

    The number of nodes on the line. If provided, the line will be transfinite. Defaults to None.

  • domain_name (Optional[str], default: None ) –

    The name of the domain the line belongs to. Defaults to None.

Source code in fastfem/mesh/generator.py
@dataclasses.dataclass
class Line:
    """Create a line with the given points.

    Args:
        start_point (Point): The start point of the line.
        end_point (Point): The end point of the line.
        number_of_nodes (Optional[int], optional): The number of nodes on the line. If
            provided, the line will be transfinite. Defaults to None.
        domain_name (Optional[str], optional): The name of the domain the line belongs
            to. Defaults to None.
    """

    start_point: Point
    end_point: Point
    number_of_nodes: Optional[int] = None
    domain_name: Optional[str] = None

    def __post_init__(
        self,
    ):
        self.points = (self.start_point, self.end_point)

        line = Geometry()._add_line(self)
        if line:
            # This line already exists, send the old line instead of creating a new one
            self.__dict__.update(line.__dict__)
        else:
            self.tag: int = gmsh.model.occ.add_line(
                self.points[0].tag, self.points[1].tag
            )

            self.transfinite = False
            if self.number_of_nodes:
                self.transfinite = True
                gmsh.model.occ.synchronize()
                gmsh.model.mesh.set_transfinite_curve(self.tag, self.number_of_nodes)

    def __neg__(self) -> "Line":
        """Create a new line with the opposite orientation."""
        self.points = (self.points[1], self.points[0])
        self.tag = -self.tag

        return self

__neg__()

Create a new line with the opposite orientation.

Source code in fastfem/mesh/generator.py
def __neg__(self) -> "Line":
    """Create a new line with the opposite orientation."""
    self.points = (self.points[1], self.points[0])
    self.tag = -self.tag

    return self

Mesh dataclass

Source code in fastfem/mesh/generator.py
@dataclasses.dataclass(kw_only=True, frozen=True)
class Mesh:
    domains: list[Domain]

    @property
    def nodes(self) -> dict[int, np.ndarray]:
        """Return all the nodes."""
        nodes = {}
        for domain in self.domains:
            for submesh in domain.mesh:
                nodes.update(submesh.nodes)
        return nodes

    @property
    def number_of_nodes(self) -> int:
        """Return the number of nodes."""
        return len(self.nodes)

    def __getitem__(self, key: str) -> Domain:
        """Return the mesh of the domain with the given name.

        Args:
            key: The name of the domain.

        Returns:
            List of submeshes of the domain. Submeshes are required because each domain
            can have multiple element types.
        """
        for domain in self.domains:
            if domain.name == key:
                return domain

        message = f"Domain with the name {key} does not exist."
        raise KeyError(message)

    def __contains__(self, key: str) -> bool:
        """Check if the mesh has a domain with the given name.

        Args:
            key: The name of the domain.

        Returns:
            True if the domain exists, otherwise False.
        """
        return any(domain.name == key for domain in self.domains)

    def __iter__(self):
        """Return an iterator over the domains."""
        return iter(self.domains)

nodes: dict[int, np.ndarray] property

Return all the nodes.

number_of_nodes: int property

Return the number of nodes.

__getitem__(key)

Return the mesh of the domain with the given name.

Parameters:

  • key (str) –

    The name of the domain.

Returns:

  • Domain

    List of submeshes of the domain. Submeshes are required because each domain

  • Domain

    can have multiple element types.

Source code in fastfem/mesh/generator.py
def __getitem__(self, key: str) -> Domain:
    """Return the mesh of the domain with the given name.

    Args:
        key: The name of the domain.

    Returns:
        List of submeshes of the domain. Submeshes are required because each domain
        can have multiple element types.
    """
    for domain in self.domains:
        if domain.name == key:
            return domain

    message = f"Domain with the name {key} does not exist."
    raise KeyError(message)

__contains__(key)

Check if the mesh has a domain with the given name.

Parameters:

  • key (str) –

    The name of the domain.

Returns:

  • bool

    True if the domain exists, otherwise False.

Source code in fastfem/mesh/generator.py
def __contains__(self, key: str) -> bool:
    """Check if the mesh has a domain with the given name.

    Args:
        key: The name of the domain.

    Returns:
        True if the domain exists, otherwise False.
    """
    return any(domain.name == key for domain in self.domains)

__iter__()

Return an iterator over the domains.

Source code in fastfem/mesh/generator.py
def __iter__(self):
    """Return an iterator over the domains."""
    return iter(self.domains)

Point dataclass

Create a point with the given coordinates.

Parameters:

  • x (float) –

    The x-coordinate of the point.

  • y (float) –

    The y-coordinate of the point.

  • z (float) –

    The z-coordinate of the point.

  • domain_name (Optional[str], default: None ) –

    The name of the domain the point belongs to. Defaults to None.

Source code in fastfem/mesh/generator.py
@dataclasses.dataclass
class Point:
    """Create a point with the given coordinates.

    Args:
        x: The x-coordinate of the point.
        y: The y-coordinate of the point.
        z: The z-coordinate of the point.
        domain_name: The name of the domain the point belongs to. Defaults to None.
    """

    x: float
    y: float
    z: float
    domain_name: Optional[str] = None

    def __post_init__(self):
        self.coordinates = (self.x, self.y, self.z)

        point = Geometry()._add_point(self)
        if point:
            # This point already exists, send the old point instead of creating a new
            # one
            self.__dict__.update(point.__dict__)
        else:
            self.tag: int = gmsh.model.occ.add_point(self.x, self.y, self.z)

Submesh dataclass

Source code in fastfem/mesh/generator.py
@dataclasses.dataclass(kw_only=True, frozen=True)
class Submesh:
    type: ZeroDElementType | OneDElementType | TwoDElementType
    nodes: dict[int, np.ndarray]
    elements: dict[int, np.ndarray]

    @property
    def number_of_elements(self) -> int:
        """Return the number of elements."""
        return len(self.elements)

    @property
    def number_of_nodes_per_element(self) -> int:
        """Return the number of nodes per element."""
        return number_of_nodes_per_element[self.type]

number_of_elements: int property

Return the number of elements.

number_of_nodes_per_element: int property

Return the number of nodes per element.

Surface dataclass

Create a surface with the given lines.

Parameters:

  • outer_lines (list[Line]) –

    The lines that form the surface. The lines must be connected (each line's end point is the start point of the next line).

  • inner_lines (Optional[list[Line]], default: None ) –

    The lines that form the holes in the surface. Defaults to None.

  • transfinite (bool, default: False ) –

    If True, the surface will be transfinite. If transfinite, all the lines' number_of_nodes argument must be provided, and the surface must have 3 or 4 lines, and there should be no inner lines. Defaults to False.

  • element_type (TwoDElementType, default: 'triangle' ) –

    The type of element to uFse. Defaults to "triangle". domain_name: The name of the domain the surface belongs to. Defaults to None.

Source code in fastfem/mesh/generator.py
@dataclasses.dataclass
class Surface:
    """Create a surface with the given lines.

    Args:
        outer_lines: The lines that form the surface. The lines must be connected (each
            line's end point is the start point of the next line).
        inner_lines: The lines that form the holes in the surface. Defaults to None.
        transfinite: If True, the surface will be transfinite. If transfinite, all the
            lines' number_of_nodes argument must be provided, and the surface must have
            3 or 4 lines, and there should be no inner lines. Defaults to False.
        element_type: The type of element to uFse. Defaults to "triangle".
            domain_name: The name of the domain the surface belongs to. Defaults to
                None.
    """

    outer_lines: list[Line]
    inner_lines: Optional[list[Line]] = None
    transfinite: bool = False
    element_type: TwoDElementType = "triangle"
    domain_name: Optional[str] = None

    def __post_init__(self):
        # Make sure all the lines are transfinite if the surface is transfinite
        if self.transfinite:
            lines_are_all_transfinite = all(
                line.transfinite for line in self.outer_lines
            )
            if not lines_are_all_transfinite:
                message = (
                    "If you would like to create a transfinite surface, all the lines'"
                    " number_of_nodes argument must be provided."
                )
                raise ValueError(message)

            if len(self.outer_lines) not in [3, 4]:
                message = "All the transfinite surfaces must have 3 or 4 lines."
                raise ValueError(message)

        # Make sure the points of lines are connected (each line's end point is the
        # start point of the next line) and lines are in clockwise order:
        def order_lines(lines: list[Line]) -> list[Line]:
            ordered_lines: list[Line] = []
            current_point_tag = lines[0].points[0].tag
            for line in lines:
                if line.points[0].tag == current_point_tag:
                    ordered_lines.append(line)
                    current_point_tag = line.points[1].tag
                elif line.points[1].tag == current_point_tag:
                    # If the line is in the opposite direction, use negative tag
                    ordered_lines.append(-line)
                    current_point_tag = line.points[1].tag
                else:
                    message = (
                        "Lines are not properly connected. Make sure the lines are"
                        " ordered."
                    )
                    raise ValueError(message)

            return ordered_lines

        ordered_outer_lines = order_lines(self.outer_lines)
        # Check if the loop is closed:
        if ordered_outer_lines[0].points[0] != ordered_outer_lines[-1].points[1]:
            message = "Lines do not form a closed loop."
            raise ValueError(message)

        self.outer_lines = ordered_outer_lines

        if self.inner_lines:
            ordered_inner_lines = order_lines(self.inner_lines)
            # Check if the loop is closed:
            if ordered_inner_lines[0].points[0] != ordered_inner_lines[-1].points[1]:
                message = "Inner lines do not form a closed loop."
                raise ValueError(message)

            self.inner_lines = ordered_inner_lines

        surface = Geometry()._add_surface(self)
        if surface:
            # This surface already exists, send the old surface instead of creating a
            # new one
            if self.inner_lines != surface.inner_lines:
                message = (
                    "There already exists a surface with the same outer lines but"
                    " different inner lines. We cannot create both of them as it will"
                    " cause duplication. Also, we cannot remove the old one since they"
                    " are not the same."
                )
                raise ValueError(message)
            self.__dict__.update(surface.__dict__)
        else:
            curve_loop_tags = []
            # Create a line loop and a plane surface
            outer_lines_tags = [line.tag for line in self.outer_lines]
            curve_loop_tags.append(gmsh.model.occ.add_curve_loop(outer_lines_tags))

            if self.inner_lines:
                inner_lines_tags = [line.tag for line in self.inner_lines]
                curve_loop_tags.append(gmsh.model.occ.add_curve_loop(inner_lines_tags))

            self.tag = gmsh.model.occ.add_plane_surface(curve_loop_tags)

            if self.transfinite or self.element_type == "quadrangle":
                gmsh.model.occ.synchronize()

            if self.transfinite:
                gmsh.model.mesh.set_transfinite_surface(self.tag)

            if self.element_type == "quadrangle":
                gmsh.model.mesh.set_recombine(2, self.tag)

create_a_rectangle_mesh(horizontal_length, vertical_length, nodes_in_horizontal_direction=None, nodes_in_vertical_direction=None, element_type='quadrangle', file_name=None)

Create a 2D mesh of a rectangle.

Parameters:

  • horizontal_length (float) –

    The horizontal length of the rectangle.

  • vertical_length (float) –

    The vertical length of the rectangle.

  • nodes_in_horizontal_direction (Optional[int], default: None ) –

    The number of nodes in the horizontal direction. Defaults to None.

  • nodes_in_vertical_direction (Optional[int], default: None ) –

    The number of nodes in the vertical direction. Defaults to None.

  • element_type (TwoDElementType, default: 'quadrangle' ) –

    The type of element to use. Defaults to "quadrangle".

  • file_name (Optional[Path], default: None ) –

    The file name to save the mesh to. Defaults to None.

Returns:

Source code in fastfem/mesh/fundamentals.py
def create_a_rectangle_mesh(
    horizontal_length: float,
    vertical_length: float,
    nodes_in_horizontal_direction: Optional[int] = None,
    nodes_in_vertical_direction: Optional[int] = None,
    element_type: TwoDElementType = "quadrangle",
    file_name: Optional[pathlib.Path] = None,
) -> RectangleMesh:
    """Create a 2D mesh of a rectangle.

    Args:
        horizontal_length: The horizontal length of the rectangle.
        vertical_length: The vertical length of the rectangle.
        nodes_in_horizontal_direction: The number of nodes in the horizontal direction.
            Defaults to None.
        nodes_in_vertical_direction: The number of nodes in the vertical direction.
            Defaults to None.
        element_type: The type of element to use. Defaults to "quadrangle".
        file_name: The file name to save the mesh to. Defaults to None.

    Returns:
        The mesh of the rectangle.
    """

    both_nx_and_ny_are_provided = all(
        [nodes_in_horizontal_direction, nodes_in_vertical_direction]
    )
    transfinite = False
    if both_nx_and_ny_are_provided:
        transfinite = True

    Surface(
        outer_lines=[
            Line(
                Point(0, 0, 0),
                Point(horizontal_length, 0, 0),
                nodes_in_horizontal_direction,
                domain_name="bottom_boundary",
            ),
            Line(
                Point(horizontal_length, 0, 0),
                Point(horizontal_length, vertical_length, 0),
                nodes_in_vertical_direction,
                domain_name="right_boundary",
            ),
            Line(
                Point(horizontal_length, vertical_length, 0),
                Point(0, vertical_length, 0),
                nodes_in_horizontal_direction,
                domain_name="top_boundary",
            ),
            Line(
                Point(0, vertical_length, 0),
                Point(0, 0, 0),
                nodes_in_vertical_direction,
                domain_name="left_boundary",
            ),
        ],
        transfinite=transfinite,
        element_type=element_type,
        domain_name="surface",
    )
    return mesh(file_name=file_name)  # type: ignore

create_a_square_mesh(side_length, nodes_in_horizontal_direction=None, nodes_in_vertical_direction=None, element_type='quadrangle', file_name=None)

Create a 2D mesh of a square.

Parameters:

  • side_length (float) –

    The side length of the square.

  • nodes_in_horizontal_direction (Optional[int], default: None ) –

    The number of nodes in the horizontal direction. Defaults to None.

  • nodes_in_vertical_direction (Optional[int], default: None ) –

    The number of nodes in the vertical direction. Defaults to None.

  • element_type (TwoDElementType, default: 'quadrangle' ) –

    The type of element to use. Defaults to "quadrangle".

Source code in fastfem/mesh/fundamentals.py
def create_a_square_mesh(
    side_length: float,
    nodes_in_horizontal_direction: Optional[int] = None,
    nodes_in_vertical_direction: Optional[int] = None,
    element_type: TwoDElementType = "quadrangle",
    file_name: Optional[pathlib.Path] = None,
) -> SquareMesh:
    """Create a 2D mesh of a square.

    Args:
        side_length: The side length of the square.
        nodes_in_horizontal_direction: The number of nodes in the horizontal direction.
            Defaults to None.
        nodes_in_vertical_direction: The number of nodes in the vertical direction.
            Defaults to None.
        element_type: The type of element to use. Defaults to "quadrangle".
    """
    horizontal_length = side_length
    vertical_length = side_length
    return create_a_rectangle_mesh(
        horizontal_length,
        vertical_length,
        nodes_in_horizontal_direction,
        nodes_in_vertical_direction,
        element_type,
        file_name,
    )  # type: ignore

mesh(file_name=None)

Create a mesh from all the created geometric entities so far and return it as a Mesh object. If a file name is provided, write the mesh to the file in the Gmsh format.

Parameters:

  • file_name (Optional[Path], default: None ) –

    The name of the file to write the mesh to. For example, "mesh.msh".

Returns:

  • Mesh

    The mesh as a Mesh object.

Source code in fastfem/mesh/generator.py
def mesh(file_name: Optional[pathlib.Path] = None) -> Mesh:
    """Create a mesh from all the created geometric entities so far and return it as a
    `Mesh` object. If a file name is provided, write the mesh to the file in the Gmsh
    format.

    Args:
        file_name: The name of the file to write the mesh to. For example, "mesh.msh".

    Returns:
        The mesh as a `Mesh` object.
    """
    mesh = Geometry().mesh()

    if file_name:
        gmsh.write(file_name)

    Geometry().clear()

    return mesh