Source code for felupe.mesh._mesh

# -*- coding: utf-8 -*-
"""
This file is part of FElupe.

FElupe 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.

FElupe 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 FElupe.  If not, see <http://www.gnu.org/licenses/>.
"""

from functools import wraps

import numpy as np

from ..view import ViewMesh
from ._convert import (
    add_midpoints_edges,
    add_midpoints_faces,
    add_midpoints_volumes,
    cell_types,
    collect_edges,
    collect_faces,
    collect_volumes,
    convert,
    subdivide,
)
from ._discrete_geometry import DiscreteGeometry
from ._dual import dual
from ._tools import (
    expand,
    fill_between,
    flip,
    merge_duplicate_cells,
    merge_duplicate_points,
    mirror,
    revolve,
    rotate,
    runouts,
    translate,
    triangulate,
)


def as_mesh(obj):
    "Convert a ``DiscreteGeometry`` object to a ``Mesh`` object."
    return Mesh(points=obj.points, cells=obj.cells, cell_type=obj.cell_type)


[docs] class Mesh(DiscreteGeometry): r"""A mesh with points, cells and optional a specified cell type. Parameters ---------- points : ndarray Array with point coordinates of shape ``(npoints, dim)``. cells : ndarray Array with cell connectivities of shape ``(ncells, points_per_cell)``. cell_type : str or None, optional An optional string in VTK-convention that specifies the cell type (default is None). Necessary when a mesh is saved to a file. Notes ----- The ``points``-array is of shape ``(npoints, dim)``, as denoted in Eq. :eq:`points`. .. math:: :label: points \hat{\boldsymbol{X}} = \begin{bmatrix} \hat{\boldsymbol{X}_1}^T \\ \hat{\boldsymbol{X}_2}^T \\ \dots \\ \hat{\boldsymbol{X}_n}^T \\ \end{bmatrix} Exemplaric point coordinates of a single point at the origin in :math:`\mathbb{R}^3` are shown in Eq. :eq:`point`. .. math:: :label: point \hat{\boldsymbol{X}_1}^T = \begin{bmatrix} 0.0 & 0.0 & 0.0 \end{bmatrix} Topologic cell connectivies are stored inside the ``cells``-array, see Eq. :eq:`cells`, .. math:: :label: cells \boldsymbol{t} = \begin{bmatrix} \boldsymbol{t}_1^T \\ \boldsymbol{t}_2^T \\ \vdots \\ \boldsymbol{t}_n^T \\ \end{bmatrix} with an exemplaric topologic cell-connectivity of a :class:`~felupe.Quad`, as shown in Eq. :eq:`cell`. .. math:: :label: cell \boldsymbol{t}_1^T = \begin{bmatrix} 0 & 1 & 2 & 3 \end{bmatrix} Examples -------- .. pyvista-plot:: :context: :force_static: >>> import numpy as np >>> import felupe as fem >>> >>> points = np.array( ... [[0.0, 0.0], [0.5, 0.1], [1.0, 0.2], [0.0, 1.0], [0.5, 0.9], [1.0, 0.8]] ... ) >>> cells = np.array([[0, 1, 4, 3], [1, 2, 5, 4]]) >>> mesh = fem.Mesh(points, cells, cell_type="quad") >>> mesh.plot().show() A mesh provides several useful attributes, like the dimension, the number of points, number of cells, number of deegrees of freedom, points connected to cells and points not connected to cells. .. pyvista-plot:: :context: :force_static: >>> mesh.dim 2 >>> mesh.npoints 6 >>> mesh.ncells 2 >>> mesh.ndof 12 >>> mesh.points_with_cells array([0, 1, 2, 3, 4, 5]) See Also -------- felupe.MeshContainer : A container which operates on a list of meshes with identical dimensions. felupe.Rectangle : A rectangular 2d-mesh with quads between ``a`` and ``b`` with ``n`` points per axis. felupe.Cube : A cube shaped 3d-mesh with hexahedrons between ``a`` and ``b`` with ``n`` points per axis. felupe.Grid : A grid shaped 3d-mesh with hexahedrons. Basically a wrapper for :func:`numpy.meshgrid` with default ``indexing="ij"``. felupe.Circle : A circular shaped 2d-mesh with quads and ``n`` points on the circumferential edge of a 45-degree section. 90-degree ``sections`` are placed at given angles in degree. felupe.mesh.Triangle : A triangular shaped 2d-mesh with quads and ``n`` points at the edges of the three sub-quadrilaterals. """ def __init__(self, points, cells, cell_type=None): self.points = np.array(points) self.cells = np.array(cells) self.cell_type = cell_type super().__init__(points=points, cells=cells, cell_type=cell_type) self.__mesh__ = Mesh # alias self.sweep = self.merge_duplicate_points self.save = self.write self.as_pyvista = self.as_unstructured_grid def __repr__(self): header = "<felupe Mesh object>" points = f" Number of points: {len(self.points)}" cells_header = " Number of cells:" cells = [f" {self.cell_type}: {self.ncells}"] return "\n".join([header, points, cells_header, *cells]) def __str__(self): return self.__repr__()
[docs] def disconnect(self, points_per_cell=None, calc_points=True): """Return a new instance of a Mesh with disconnected cells. Optionally, the points-per-cell may be specified (must be lower or equal the number of points- per-cell of the original Mesh). If the Mesh is to be used as a *dual* Mesh, then the point-coordinates do not have to be re-created because they are not used. See Also -------- felupe.Mesh.dual : Create a new dual mesh with given points per cell. """ return self.dual( points_per_cell=points_per_cell, disconnect=True, calc_points=calc_points, offset=0, npoints=None, )
[docs] def as_meshio(self, **kwargs): """Export the mesh as :class:`meshio.Mesh`. Parameters ---------- **kwargs : dict, optional Additional keyword-arguments for ``meshio.Mesh(points, cells, **kwargs)``. Returns ------- meshio.Mesh The mesh as :class:`meshio.Mesh`. """ import meshio points = np.pad(self.points, ((0, 0), (0, 3 - self.points.shape[1]))) return meshio.Mesh(points=points, cells={self.cell_type: self.cells}, **kwargs)
[docs] def as_unstructured_grid(self, cell_type=None, **kwargs): """Export the mesh as :class:`pyvista.UnstructuredGrid`. Parameters ---------- cell_type : pyvista.CellType or None, optional Cell-type of PyVista (default is None). **kwargs : dict, optional Additional keyword-arguments for :class:`pyvista.UnstructuredGrid`. Returns ------- pyvista.UnstructuredGrid The mesh as :class:`pyvista.UnstructuredGrid`. """ import pyvista as pv if cell_type is None: cell_type = dict(cell_types())[self.cell_type] points = np.pad(self.points, ((0, 0), (0, 3 - self.points.shape[1]))) cells = np.pad( self.cells, ((0, 0), (1, 0)), constant_values=self.cells.shape[1] ) cell_types_int = cell_type * np.ones(self.ncells, dtype=int) return pv.UnstructuredGrid(cells, cell_types_int, points)
[docs] def write(self, filename="mesh.vtk", **kwargs): """Write the mesh to a file. Parameters ---------- filename : str, optional The filename of the mesh (default is ``mesh.vtk``). **kwargs : dict, optional Additional keyword arguments for :meth:`meshio.Mesh.write`. Notes ----- .. note:: For XDMF-export please ensure to have ``h5py`` (as an optional dependency of ``meshio``) installed. Examples -------- >>> import felupe as fem >>> >>> mesh = fem.Rectangle(n=3) >>> mesh.write(filename="mesh.vtk") See Also -------- felupe.mesh.read : Read a mesh from a file using :func:`meshio.read`. felupe.Mesh.write : Write the mesh to a file. """ self.as_meshio(**kwargs).write(filename)
[docs] def view(self, point_data=None, cell_data=None, cell_type=None): """View the mesh with optional given dicts of point- and cell-data items. Parameters ---------- point_data : dict or None, optional Additional point-data dict (default is None). cell_data : dict or None, optional Additional cell-data dict (default is None). cell_type : pyvista.CellType or None, optional Cell-type of PyVista (default is None). Returns ------- felupe.ViewMesh A object which provides visualization methods for :class:`felupe.Mesh`. See Also -------- felupe.ViewMesh : Visualization methods for :class:`~felupe.Mesh`. """ return ViewMesh( self, point_data=point_data, cell_data=cell_data, cell_type=cell_type, )
[docs] def plot(self, *args, **kwargs): """Plot the mesh. See Also -------- felupe.Scene.plot : Plot method of a scene. """ return self.view().plot(*args, show_undeformed=False, **kwargs)
[docs] def screenshot( self, *args, filename="mesh.png", transparent_background=None, scale=None, **kwargs, ): """Take a screenshot of the mesh. See Also -------- pyvista.Plotter.screenshot : Take a screenshot of a PyVista plotter. """ return self.plot(*args, off_screen=True, **kwargs).screenshot( filename=filename, transparent_background=transparent_background, scale=scale, )
[docs] def imshow(self, *args, ax=None, **kwargs): """Take a screenshot of the mesh, show the image data in a figure and return the ax. """ if ax is None: import matplotlib.pyplot as plt fig, ax = plt.subplots() ax.imshow(self.screenshot(*args, filename=None, **kwargs)) ax.set_axis_off() return ax
[docs] def add_points(self, points): "Add additional points to the mesh in-place." self.update(points=np.vstack([self.points, points]))
[docs] def clear_points_without_cells(self): "Clear the list of points without cells." self.points_without_cells = self.points_without_cells[:0]
[docs] def get_point_ids(self, value, fun=np.isclose, mode=np.all, **kwargs): """Return point ids for points which are close to a given value. Parameters ---------- value : float, list or ndarray Scalar value or point coordinates. fun : callable, optional The function used to compare the points to the given value, i.e. with a function signature ``fun(mesh.points, value, **kwargs)``. Default is :func:`numpy.isclose`. mode : callable, optional A callable used to combine the search results, either :func:`numpy.any` or :func:`numpy.all`. **kwargs : dict, optional Additional keyword arguments for ``fun(mesh.points, value, **kwargs)``. Returns ------- ndarray Array with point ids. Examples -------- Get point ids at given coordinates for a mesh with duplicate points. >>> import numpy as np >>> import felupe as fem >>> >>> mesh = fem.Cube(n=11) >>> mesh.update(points=np.vstack([mesh.points, mesh.points])) >>> point_ids = mesh.get_point_ids([0, 1, 1]) >>> point_ids array([1320, 2651]) >>> mesh.points[point_ids] array([[0., 1., 1.], [0., 1., 1.]]) """ return np.argwhere(mode(fun(self.points, value, **kwargs), axis=1))[:, 0]
[docs] def get_cell_ids(self, point_ids): """Return cell ids which have the given point ids in their connectivity. Parameters ---------- point_ids : list or ndarray Array with point ids which are used to search for cells. Returns ------- ndarray Array with cell ids which have the given point ids in their connectivity. Examples -------- >>> import numpy as np >>> import felupe as fem >>> >>> mesh = fem.Cube(n=11) >>> point_ids = mesh.get_point_ids([0, 1, 1]) >>> point_ids array([1320]) >>> cell_ids = mesh.get_cell_ids(point_ids) >>> cell_ids array([990]) """ return np.argwhere(np.isin(self.cells, point_ids).any(axis=1))[:, 0]
[docs] def get_cell_ids_neighbours(self, cell_ids): """Return cell ids which share points with given cell ids. Parameters ---------- cell_ids : list or ndarray Array with cell ids which are used to search for neighbour cells. Returns ------- ndarray Array with cell ids which are next to the given cells. Examples -------- >>> import numpy as np >>> import felupe as fem >>> >>> mesh = fem.Cube(n=11) >>> point_ids = mesh.get_point_ids([0, 1, 1]) >>> point_ids array([1320]) >>> cell_ids = mesh.get_cell_ids(point_ids) >>> cell_ids array([990]) Find the cell ids which share at least one point with the given cell id(s). >>> cell_ids_neighbours = mesh.get_cell_ids_neighbours(cell_ids) >>> cell_ids_neighbours array([880, 881, 890, 891, 980, 981, 990, 991]) """ return self.get_cell_ids(self.cells[cell_ids])
[docs] def get_point_ids_shared(self, cell_ids_neighbours): """Return shared point ids for given cell ids. Parameters ---------- cells_neighbours : list or ndarray Array with cell ids. Returns ------- ndarray Array with point ids which are connected to all given cell neighbours. Examples -------- >>> import numpy as np >>> import felupe as fem >>> >>> mesh = fem.Cube(n=11) >>> point_ids = mesh.get_point_ids([0, 1, 1]) >>> point_ids array([1320]) >>> cell_ids = mesh.get_cell_ids(point_ids) >>> cell_ids array([990]) Find the cell ids which share at least one point with the given cell id(s). >>> cell_ids_neighbours = mesh.get_cell_ids_neighbours(cell_ids) >>> cell_ids_neighbours array([880, 881, 890, 891, 980, 981, 990, 991]) Find the shared point ids for the list of cell ids. >>> point_ids_shared = mesh.get_point_ids_shared(cell_ids_neighbours) >>> point_ids_shared array([1189]) """ neighbours = self.cells[cell_ids_neighbours] cell = neighbours[0] return cell[[np.isin(neighbours, point).any(axis=1).all() for point in cell]]
[docs] def get_point_ids_corners(self): """Return point ids which are located at (xmin, ymin), (xmax, ymin), etc. Returns ------- ndarray Array with point ids which are located at the corners. Examples -------- >>> import numpy as np >>> import felupe as fem >>> >>> mesh = fem.Cube(n=11) >>> point_ids_corners = mesh.get_point_ids_corners() >>> point_ids_corners array([ 0, 1210, 10, 1220, 110, 1320, 120, 1330]) """ xmin = np.min(self.points, axis=0) xmax = np.max(self.points, axis=0) points = np.vstack( [x.ravel() for x in np.meshgrid(*np.vstack([xmin, xmax]).T)] ).T return np.concatenate([self.get_point_ids(point) for point in points])
[docs] def modify_corners(self, point_ids=None): """Modify the corners of a regular rectangle (quad) or cube (hexahedron) inplace. Only the cells array is modified, the points array remains unchanged. Parameters ---------- point_ids : ndarray or None, optional Array with point ids located at the corners which are modified (default is None). If None, all corners are modified. Notes ----- Description of the algorithm: 1. Get corner point ids. For each corner point: 2. Get attached cell and find cell neighbours. 3. Get the shared point of the cell and its neighbours. 4. Get pair-wise shared points which are located on an edge. 5. Replace the shared points with the corner point. 6. Delete the cell attached to the corner point. Examples -------- .. pyvista-plot:: :context: :force_static: >>> import numpy as np >>> import felupe as fem >>> >>> mesh = fem.Rectangle(b=(3, 1), n=(16, 6)) >>> mesh.plot().show() .. pyvista-plot:: :context: :force_static: >>> mesh = mesh.modify_corners() # inplace >>> mesh.plot().show() """ if self.cell_type not in ["quad", "hexahedron"]: message = [ "Cell type not supported.", "Must be either 'quad' or 'hexahedron'", f"but given cell type is '{self.cell_type}'.", ] raise TypeError(" ".join(message)) if point_ids is None: point_ids = self.get_point_ids_corners() for point_id in point_ids: cell_id = self.get_cell_ids(point_id)[0] cell_id_with_neighbours = self.get_cell_ids_neighbours(cell_id) cell_ids_neighbours = cell_id_with_neighbours[ cell_id_with_neighbours != cell_id ] point_id_shared = self.get_point_ids_shared(cell_id_with_neighbours)[0] point_ids_shared_individual = [ self.get_point_ids_shared([cell_id, neighbour]) for neighbour in cell_ids_neighbours ] if self.cell_type == "hexahedron": edges = np.argwhere( np.isclose(self.points, self.points[point_id]).sum(axis=1) >= 2 )[:, 0] point_ids_shared_individual = [ p[np.isin(p, edges)] for p in point_ids_shared_individual ] point_ids_shared_individual = [ shared[shared != point_id_shared] for shared in point_ids_shared_individual ] for shared, neighbour in zip( point_ids_shared_individual, cell_ids_neighbours ): point_id_replace = np.argwhere(np.isin(self.cells[neighbour], shared)) if len(point_id_replace) > 0: self.cells[neighbour, point_id_replace[0][0]] = point_id self.cells = np.delete(self.cells, cell_id, axis=0) self.update(cells=self.cells) return self
[docs] def dual( self, points_per_cell=None, disconnect=True, calc_points=False, offset=0, npoints=None, ): """Create a new dual mesh with given points per cell. Parameters ---------- points_per_cell : int or None, optional Number of points per cell, must be equal or lower than ``cells.shape[1]`` ( default is None). If None, all points per cell are considered for the dual mesh. disconnect : bool, optional A flag to disconnect the mesh (each cell has its own points). Default is True. calc_points : bool, optional A flag to calculate the point coordinates for the dual mesh (default is False). If False, the points array is filled with zeros. offset : int, optional An offset to be added to the cells array (default is 0). npoints : int or None, optional Number of points for the dual mesh. If the given number of points is greater than ``npoints * points_per_cell``, then the missing points are added to the points array (filled with zeros). Default is None. Returns ------- Mesh The dual mesh. Notes ----- .. note:: The points array of the dual mesh always has a shape of ``(npoints * points_per_cell, dim)``. Examples -------- >>> import felupe as fem >>> >>> mesh = fem.Rectangle(n=5).add_midpoints_edges() >>> region = fem.RegionQuadraticQuad(mesh=mesh) >>> >>> mesh_dual = mesh.dual(points_per_cell=1, disconnect=False) >>> region_dual = fem.RegionConstantQuad( ... mesh_dual, quadrature=region.quadrature, grad=False ... ) >>> >>> displacement = fem.FieldPlaneStrain(region, dim=2) >>> pressure = fem.Field(region_dual) >>> field = fem.FieldContainer([displacement, pressure]) See Also -------- felupe.mesh.dual : Create a new dual mesh with given points per cell. """ return as_mesh( dual( self, points_per_cell=points_per_cell, disconnect=disconnect, calc_points=calc_points, offset=offset, npoints=npoints, ) )
[docs] def expand(self, n=11, z=1, axis=-1, expand_dim=True): """Expand a 0d-Point to a 1d-Line, a 1d-Line to a 2d-Quad or a 2d-Quad to a 3d-Hexahedron Mesh. Parameters ---------- n : int, optional Number of n-point repetitions or (n-1)-cell repetitions, default is 11. Must be greater than 0. z : float or ndarray, optional Total expand dimension as float (edge length in expand direction is z / n), default is 1. Optionally, if an array is passed these entries are taken as expansion and ``n`` is ignored. axis : int, optional Axis of expansion (default is -1). mask : ndarray or None, optional A boolean mask to select points which are rotated (default is None). expand_dim : bool, optional Expand the dimension of the point coordinates (default is True). Returns ------- Mesh The expanded mesh. Examples -------- Expand a rectangle to a cube. .. pyvista-plot:: :force_static: >>> import felupe as fem >>> >>> rect = fem.Rectangle(n=4) >>> cube = rect.expand(n=7, z=2) >>> >>> cube.plot().show() >>> cube <felupe Mesh object> Number of points: 112 Number of cells: hexahedron: 54 See Also -------- felupe.mesh.expand : Expand a 0d-Point to a 1d-Line, a 1d-Line to a 2d-Quad or a 2d-Quad to a 3d-Hexahedron Mesh. """ return as_mesh(expand(self, n=n, z=z, axis=axis, expand_dim=expand_dim))
[docs] def rotate(self, angle_deg, axis, center=None, mask=None): """Rotate a Mesh. Parameters ---------- angle_deg : int Rotation angle in degree. axis : int Rotation axis. center : list or ndarray or None, optional Center point coordinates (default is None). mask : ndarray or None, optional A boolean mask to select points which are rotated (default is None). Returns ------- Mesh The rotated mesh. Examples -------- Rotate a rectangle in the xy-plane by 35 degree. .. pyvista-plot:: :force_static: >>> import felupe as fem >>> >>> rect = fem.Rectangle(b=(3, 1), n=(10, 4)) >>> mesh = rect.rotate(angle_deg=35, axis=2, center=[1.5, 0.5]) >>> mesh.plot().show() >>> mesh <felupe Mesh object> Number of points: 40 Number of cells: quad: 27 See Also -------- felupe.mesh.rotate : Rotate a Mesh. """ return as_mesh( rotate(self, angle_deg=angle_deg, axis=axis, center=center, mask=mask) )
[docs] def revolve(self, n=11, phi=180, axis=0, expand_dim=True): """Revolve a 2d-Quad to a 3d-Hexahedron Mesh. Parameters ---------- n : int, optional Number of n-point revolutions (or (n-1) cell revolutions), default is 11. phi : float or ndarray, optional Revolution angle in degree (default is 180). axis : int, optional Revolution axis (default is 0). expand_dim : bool, optional Expand the dimension of the point coordinates (default is True). Returns ------- Mesh The revolved mesh. Examples -------- Revolve a cylinder from a rectangle. .. pyvista-plot:: :force_static: >>> import felupe as fem >>> >>> rect = fem.Rectangle(a=(0, 4), b=(3, 5), n=(10, 4)) >>> mesh = rect.revolve(n=11, phi=180, axis=0) >>> mesh.plot().show() >>> mesh <felupe Mesh object> Number of points: 440 Number of cells: hexahedron: 270 See Also -------- felupe.mesh.revolve : Revolve a 2d-Quad to a 3d-Hexahedron Mesh. """ return as_mesh(revolve(self, n=n, phi=phi, axis=axis, expand_dim=expand_dim))
[docs] def merge_duplicate_points(self, decimals=None): """Merge duplicate points and update cells of a Mesh. Parameters ---------- decimals : int or None, optional Number of decimals for point coordinate comparison (default is None). Returns ------- Mesh The mesh with merged duplicate points and updated cells. Notes ----- .. warning:: This function re-sorts points. .. note:: This function does not merge duplicate cells. Examples -------- Two quad meshes to be merged overlap some points. Merge these duplicated points and update the cells. .. pyvista-plot:: :context: >>> import felupe as fem >>> >>> rect1 = fem.Rectangle(n=11) >>> rect2 = fem.Rectangle(a=(0.9, 0), b=(1.9, 1), n=11) >>> rect2 <felupe Mesh object> Number of points: 121 Number of cells: quad: 100 Each mesh contains 121 points and 100 cells. These two meshes are now stored in a :class:`~felupe.MeshContainer`. .. pyvista-plot:: :context: >>> container = fem.MeshContainer([rect1, rect2]) >>> container <felupe mesh container object> Number of points: 242 Number of cells: quad: 100 quad: 100 The meshes of the mesh container are :func:`stacked <felupe.mesh.stack>`. .. pyvista-plot:: :context: >>> stack = fem.mesh.stack(container.meshes) >>> stack <felupe Mesh object> Number of points: 242 Number of cells: quad: 200 After merging the duplicated points and cells, the number of points is reduced but the number of cells is unchanged. .. pyvista-plot:: :context: :force_static: >>> mesh = stack.merge_duplicate_points() >>> mesh <felupe Mesh object> Number of points: 220 Number of cells: quad: 200 >>> mesh.plot(opacity=0.6).show() .. note:: The :class:`~felupe.MeshContainer` may be directly created with ``merge=True``. This enforces :func:`~felupe.mesh.merge_duplicate_points` for the shared points array of the container. See Also -------- felupe.mesh.merge_duplicate_points : Merge duplicated points and update cells of a Mesh. felupe.MeshContainer : A container which operates on a list of meshes with identical dimensions. """ return as_mesh(merge_duplicate_points(self, decimals=decimals))
@wraps(merge_duplicate_cells) def merge_duplicate_cells(self): """Merge duplicate cells of a Mesh. Returns ------- Mesh The mesh with merged duplicate cells. Notes ----- .. warning:: This function re-sorts cells. .. note:: This function does not merge duplicate points. Examples -------- Two quad meshes to be merged overlap some cells. Merge these duplicated points and update the cells. .. pyvista-plot:: :context: >>> import felupe as fem >>> >>> rect1 = fem.Rectangle(n=11) >>> rect2 = fem.Rectangle(a=(0.9, 0), b=(1.9, 1), n=11) >>> rect2 <felupe Mesh object> Number of points: 121 Number of cells: quad: 100 Each mesh contains 121 points and 100 cells. These two meshes are now stored in a :class:`~felupe.MeshContainer`. .. pyvista-plot:: :context: >>> container = fem.MeshContainer([rect1, rect2]) >>> container <felupe mesh container object> Number of points: 242 Number of cells: quad: 100 quad: 100 The meshes of the mesh container are :func:`stacked <felupe.mesh.stack>`. .. pyvista-plot:: :context: >>> stack = fem.mesh.stack(container.meshes) >>> stack <felupe Mesh object> Number of points: 242 Number of cells: quad: 200 After merging the duplicated points and cells, the number of points is reduced but the number of cells is unchanged. .. pyvista-plot:: :context: :force_static: >>> mesh = stack.merge_duplicate_points() >>> mesh <felupe Mesh object> Number of points: 220 Number of cells: quad: 200 mesh.plot(opacity=0.6).show() .. note:: The :class:`~felupe.MeshContainer` may be directly created with ``merge=True``. This enforces :func:`~felupe.mesh.merge_duplicate_points` for the shared points array of the container. The duplicate cells are merged in a second step. .. pyvista-plot:: :context: >>> merged = mesh.merge_duplicate_cells() >>> merged <felupe Mesh object> Number of points: 220 Number of cells: quad: 190 See Also -------- felupe.mesh.merge_duplicate_points : Merge duplicate points of a Mesh. felupe.mesh.merge_duplicate_cells : Merge duplicate cells of a Mesh. felupe.MeshContainer : A container which operates on a list of meshes with identical dimensions. """ return as_mesh(merge_duplicate_cells(self))
[docs] def fill_between(self, other_mesh, n=11): """Fill a 2d-Quad Mesh between two 1d-Line Meshes, embedded in 2d-space, or a 3d-Hexahedron Mesh between two 2d-Quad Meshes, embedded in 3d-space, by expansion. Both meshes must have equal number of points and cells. The cells-array is taken from the first mesh. Parameters ---------- other_mesh : felupe.Mesh The other line- or quad-mesh. n : int or ndarray Number of n-point repetitions or (n-1)-cell repetitions, (default is 11). If an array is given, then its values are used for the relative positions in a reference configuration (-1, 1) between the two meshes. Returns ------- felupe.Mesh The expanded mesh. Examples -------- .. pyvista-plot:: :force_static: >>> import felupe as fem >>> >>> inner = fem.mesh.revolve(fem.Point(1)).expand(z=0.4).translate(0.2, axis=2) >>> outer = fem.mesh.revolve(fem.Point(2), phi=160).rotate( ... axis=2, angle_deg=20 ... ).expand(z=1.2) >>> mesh = inner.fill_between(outer, n=6) >>> >>> mesh.plot().show() See Also -------- felupe.mesh.fill_between : Fill a 2d-Quad Mesh between two 1d-Line Meshes, embedded in 2d-space, or a 3d-Hexahedron Mesh between two 2d-Quad Meshes, embedded in 3d-space, by expansion. """ return as_mesh(fill_between(self, other_mesh=other_mesh, n=n))
[docs] def flip(self, mask=None): r"""Ensure positive cell volumes for `tria`, `tetra`, `quad` and `hexahedron` cell types. Parameters ---------- mask: list, ndarray or None, optional Boolean mask for selected cells to flip (default is None). If None, all cells are selected to be flipped. Returns ------- Mesh The mesh with a rearranged cells array to ensure positive cell volumes. Examples -------- A quad mesh with negative cell volumes occurs if one coordinate axis is multiplied by -1. The error pops up if a region is created with this mesh. >>> import numpy as np >>> import felupe as fem >>> >>> mesh = fem.Rectangle(n=3) >>> mesh.update(points=mesh.points * np.array([[-1, 1]])) >>> region = fem.RegionQuad(mesh) The sum of the differential volumes :math:`V = \sum_c \sum_q dV_{qc}` is evaluated to -1.0. >>> region.dV.sum() -1.0 Let's try to fix the mesh. >>> mesh.cells array([[0, 1, 4, 3], [1, 2, 5, 4], [3, 4, 7, 6], [4, 5, 8, 7]]) The cells array is rearranged to ensure positive cell volumes. >>> mesh_fixed = mesh.flip() >>> mesh_fixed.cells array([[3, 4, 1, 0], [4, 5, 2, 1], [6, 7, 4, 3], [7, 8, 5, 4]]) A region now correctly evaluates the total volume of the mesh to 1.0. >>> region_fixed = fem.RegionQuad(mesh_fixed) >>> region_fixed.dV.sum() 1.0 See Also -------- felupe.mesh.flip : Ensure positive cell volumes for `tria`, `tetra`, `quad` and `hexahedron` cell types. """ return as_mesh(flip(self, mask=mask))
[docs] def mirror(self, normal=[1, 0, 0], centerpoint=[0, 0, 0], axis=None): """Mirror points by plane normal and ensure positive cell volumes for `tria`, `tetra`, `quad` and `hexahedron` cell types. Parameters ---------- normal: list or ndarray, optional Mirror-plane normal vector (default is [1, 0, 0]). centerpoint: list or ndarray, optional Center-point coordinates on the mirror plane (default is [0, 0, 0]). axis: int or None, optional Mirror axis (default is None). Returns ------- Mesh The mirrored mesh. Examples -------- .. pyvista-plot:: :force_static: >>> import felupe as fem >>> >>> mesh = fem.Circle(sections=[0, 90, 180], n=5) >>> mesh.plot().show() .. pyvista-plot:: :force_static: >>> import felupe as fem >>> >>> mesh = fem.Circle(sections=[0, 90, 180], n=5) >>> mesh.mirror(normal=[0, 1, 0]).plot().show() See Also -------- felupe.Mesh.mirror : Mirror points by plane normal and ensure positive cell volumes for `tria`, `tetra`, `quad` and `hexahedron` cell types. """ return as_mesh(mirror(self, normal=normal, centerpoint=centerpoint, axis=axis))
[docs] def translate(self, move, axis): """Translate (move) a Mesh along a given axis. Parameters ---------- move : float Translation along given axis. axis : int Translation axis. Returns ------- Mesh The translated mesh. Examples -------- >>> import felupe as fem >>> >>> mesh = fem.Circle(n=6) >>> mesh.points.min(axis=0), mesh.points.max(axis=0) (array([-1., -1.]), array([1., 1.])) >>> translated = mesh.translate(0.3, axis=1) >>> translated.points.min(axis=0), translated.points.max(axis=0) (array([-1. , -0.7]), array([1. , 1.3])) See Also -------- felupe.mesh.translate : Translate (move) a Mesh along a given axis. """ return as_mesh(translate(self, move=move, axis=axis))
[docs] def triangulate(self, mode=3): """Triangulate a quad or a hex mesh. Parameters ---------- mode: int, optional Choose a mode how to convert hexahedrons to tets [1]_ (default is 3). Returns ------- Mesh The triangulated mesh. Examples -------- Use ``mode=0`` to convert a mesh of hexahedrons into tetrahedrons [1]_. .. pyvista-plot:: :force_static: >>> import felupe as fem >>> >>> mesh = fem.Cube(n=6) >>> mesh.triangulate(mode=0).plot().show() Use ``mode=3`` to convert a mesh of hexahedrons into tetrahedrons [1]_. .. pyvista-plot:: :force_static: >>> import felupe as fem >>> >>> mesh = fem.Cube(n=6) >>> mesh.triangulate(mode=3).plot().show() References ---------- .. [1] Dompierre, J., Labbé, P., Vallet, M. G., & Camarero, R. (1999). How to Subdivide Pyramids, Prisms, and Hexahedra into Tetrahedra. IMR, 99, 195. See Also -------- felupe.mesh.triangulate : Triangulate a quad or a hex mesh. """ return as_mesh(triangulate(self, mode=mode))
[docs] def add_runouts( self, values=[0.1, 0.1], centerpoint=[0, 0, 0], axis=0, exponent=5, mask=slice(None), normalize=False, ): """Add simple rubber-runouts for realistic rubber-metal structures. Parameters ---------- values : list or ndarray, optional Relative amount of runouts (per coordinate) perpendicular to the axis (default is 10% per coordinate, i.e. [0.1, 0.1]). centerpoint : list or ndarray, optional Center-point coordinates (default is [0, 0, 0]). axis : int or None, optional Axis (default is 0). exponent : int, optional Positive exponent to control the shape of the runout. The higher the exponent, the steeper the transition (default is 5). mask : list or None, optional List of points to be considered (default is None). normalize : bool, optional Normalize the runouts to create indents, i.e. maintain the original shape at the ends (default is False). Returns ------- Mesh The mesh with the modified point coordinates. Examples -------- .. pyvista-plot:: :force_static: >>> import felupe as fem >>> >>> rect = fem.Rectangle(a=(-3, -1), b=(3, 1), n=(31, 11)) >>> mesh = rect.add_runouts(axis=1, values=[0.2], normalize=True) >>> >>> mesh.plot().show() .. pyvista-plot:: :force_static: >>> import felupe as fem >>> >>> cube = fem.Cube(a=(-3, -2, -1), b=(3, 2, 1), n=(31, 21, 11)) >>> mesh = cube.add_runouts(axis=2, values=[0.1, 0.3], normalize=True) >>> >>> mesh.plot().show() See Also -------- felupe.mesh.runouts : Add simple rubber-runouts for realistic rubber-metal structures. """ return as_mesh( runouts( self, values=values, centerpoint=centerpoint, axis=axis, exponent=exponent, mask=mask, normalize=normalize, ) )
[docs] def convert( self, order=0, calc_points=False, calc_midfaces=False, calc_midvolumes=False, ): """Convert a mesh to a given order. Only conversions to ``order=0`` and ``order=2`` are supported. This function supports meshes with cell types ``"triangle"``, ``"tetra"``, ``"quad"`` and ``"hexahedron"``. Parameters ---------- order : int, optional The order of the converted mesh (default is 0). If 0, the points-array will be of shape ``(ncells, dim)``. If 0 and ``calc_points`` is True, the mean of all points per cell is evaluated. If 0 and ``calc_points`` is False, the points array is filled with zeros. If 2, at least midpoints on cell edges are added to the mesh. If 2 and ``calc_midfaces`` is True, midpoints on cell faces are also added. If 2 and ``calc_midvolumes`` is True, midpoints on cell volumes are also added. Raises an error if not 0 or 2. calc_points : bool, optional Flag to return the mean of all points per cell if ``order=0`` (default is False). If False, the points-array is filled with zeros. calc_midfaces : bool, optional Flag to add midpoints on cell faces if ``order=2`` (default is False). calc_midvolumes : bool, optional Flag to add midpoints on cell volumes if ``order=2`` (default is False). Returns ------- Mesh The converted mesh. Examples -------- Convert a mesh of hexahedrons to quadratic hexahedrons by inserting midpoints on the cell edges. .. pyvista-plot:: :force_static: >>> import felupe as fem >>> >>> mesh = fem.Rectangle(n=6) >>> mesh2 = mesh.convert(order=2) >>> >>> mesh2.plot(plotter=mesh.plot(), style="points", color="black").show() >>> mesh2 <felupe Mesh object> Number of points: 96 Number of cells: quad8: 25 See Also -------- felupe.mesh.add_midpoints_edges : Add midpoints on edges for given points and cells and update cell_type accordingly. felupe.mesh.add_midpoints_faces : Add midpoints on faces for given points and cells and update cell_type accordingly. felupe.mesh.add_midpoints_volumes : Add midpoints on volumes for given points and cells and update cell_type accordingly. felupe.Mesh.add_midpoints_edges : Add midpoints on edges for given points and cells and update cell_type accordingly. felupe.Mesh.add_midpoints_faces : Add midpoints on faces for given points and cells and update cell_type accordingly. felupe.Mesh.add_midpoints_volumes : Add midpoints on volumes for given points and cells and update cell_type accordingly. """ return as_mesh( convert( self, order=order, calc_points=calc_points, calc_midfaces=calc_midfaces, calc_midvolumes=calc_midvolumes, ) )
@wraps(collect_edges) def collect_edges(self): return collect_edges(self) @wraps(collect_faces) def collect_faces(self): return collect_faces(self) @wraps(collect_volumes) def collect_volumes(self): return collect_volumes(self)
[docs] def add_midpoints_edges(self, cell_type=None): """Add midpoints on edges for given points and cells and update cell_type accordingly. Parameters ---------- cell_type: str or None, optional A string in VTK-convention that specifies the new cell type (default is None). If None, the cell type is chosen automatically. Returns ------- Mesh A new mesh with inserted midpoints on cell edges. Examples -------- Convert a mesh of hexahedrons to quadratic hexahedrons by inserting midpoints on the cell edges. .. pyvista-plot:: :force_static: >>> import felupe as fem >>> >>> mesh = fem.Rectangle(n=6) >>> mesh_with_midpoints_edges = mesh.add_midpoints_edges() >>> >>> mesh_with_midpoints_edges.plot( ... plotter=mesh.plot(), style="points", color="black" ... ).show() >>> mesh_with_midpoints_edges <felupe Mesh object> Number of points: 96 Number of cells: quad8: 25 See Also -------- felupe.mesh.add_midpoints_edges : Add midpoints on edges for given points and cells and update cell_type accordingly. """ return add_midpoints_edges(self, cell_type_new=cell_type)
[docs] def add_midpoints_faces(self, cell_type=None): """Add midpoints on faces for given points and cells and update cell_type accordingly. Parameters ---------- cell_type: str or None, optional A string in VTK-convention that specifies the new cell type (default is None). If None, the cell type is chosen automatically. Returns ------- Mesh A new mesh with inserted midpoints on cell faces. Examples -------- .. pyvista-plot:: :force_static: >>> import felupe as fem >>> >>> mesh = fem.Rectangle(n=6) >>> mesh_with_midpoints_faces = mesh.add_midpoints_faces(cell_type="quad") >>> >>> mesh_with_midpoints_faces.plot( ... plotter=mesh.plot(), style="points", color="black" ... ).show() >>> mesh_with_midpoints_faces <felupe Mesh object> Number of points: 61 Number of cells: quad: 25 See Also -------- felupe.mesh.add_midpoints_faces : Add midpoints on faces for given points and cells and update cell_type accordingly. """ return add_midpoints_faces(self, cell_type_new=cell_type)
[docs] def add_midpoints_volumes(self, cell_type=None): """Add midpoints on volumes for given points and cells and update cell_type accordingly. Parameters ---------- cell_type: str or None, optional A string in VTK-convention that specifies the new cell type (default is None). If None, the cell type is chosen automatically. Returns ------- Mesh A new mesh with inserted midpoints on cell volumes. Examples -------- .. pyvista-plot:: :force_static: >>> import felupe as fem >>> import pyvista as pv >>> >>> mesh = fem.Cube(n=6) >>> mesh_with_midpoints_volumes = fem.mesh.add_midpoints_volumes( ... mesh, cell_type_new="hexahedron9" ... ) >>> plotter = pv.Plotter() >>> actor = plotter.add_points(mesh_with_midpoints_volumes.points, color="black") >>> mesh.plot(opacity=0.5, plotter=plotter).show() >>> mesh_with_midpoints_volumes <felupe Mesh object> Number of points: 341 Number of cells: hexahedron9: 125 See Also -------- felupe.mesh.add_midpoints_volumes : Add midpoints on volumes for given points and cells and update cell_type accordingly. """ return add_midpoints_volumes(self, cell_type_new=cell_type)
[docs] def subdivide(self): """Subdivide cells by adding midpoints on edges, faces and volumes. Returns ------- Mesh A new subdivided mesh. Examples -------- Take a rectangle mesh and subdivide it two times. .. pyvista-plot:: :context: :force_static: >>> import felupe as fem >>> >>> rect = fem.Rectangle(n=4).modify_corners() >>> rect.plot().show() .. pyvista-plot:: :context: :force_static: >>> mesh = rect.subdivide().subdivide() >>> mesh.plot().show() See Also -------- felupe.Mesh.subdivide : Subdivide cells by adding midpoints on edges, faces and volumes. """ return subdivide(self)
[docs] def cells_in(self, other_mesh, decimals=8): """Return a boolean cells-mask for cells that are also present in another mesh. Parameters ---------- other_mesh : fem.Mesh The mesh to compare with. decimals : int, optional The number of decimals to consider for point comparison. Default is 8. Returns ------- np.ndarray A boolean array where True indicates that cells are also in the other mesh. Examples -------- .. pyvista-plot:: :context: :force_static: >>> import felupe as fem >>> >>> rect_top = fem.Rectangle(n=(4, 2)).translate(1.0, axis=1) >>> rect_bottom = fem.Rectangle(n=(4, 2)).translate(1e-6, axis=0) >>> >>> meshes = [rect_top, rect_bottom] >>> mesh = fem.MeshContainer(meshes, merge=True, decimals=6).stack() >>> >>> cells_mask = mesh.cells_in(rect_top, decimals=6) >>> cells_mask array([ True, True, True, False, False, False]) """ def view_points(points, decimals): "Convert point coordinates to integers for comparison." points = np.round(points * 10**decimals).astype(np.int64) return points.view([("x", np.int64), ("y", np.int64)]).ravel() # generate views on points view = view_points(self.points, decimals) view_other = view_points( other_mesh.points[other_mesh.cells].reshape(-1, 2), decimals ) # shared label mapping inverse = np.unique(np.concatenate([view, view_other]), return_inverse=True)[1] labels, local_labels = np.split(inverse, [len(view)]) # point and cell masks point_mask = np.isin(labels, local_labels) cell_mask = point_mask[self.cells].all(axis=1) if cell_mask.sum() < len(other_mesh.cells): raise ValueError("Cells not found, try to use lower values for decimals.") if cell_mask.sum() > len(other_mesh.cells): raise ValueError("More cells found, try to use higher values for decimals.") return cell_mask