Source code for felupe.region._boundary

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

import numpy as np

from ..math import cross
from ..mesh import Mesh
from ._region import Region


def boundary_cells_quad(mesh):
    "Convert the cells array of a quad mesh into a boundary cells array."

    # edges (boundary) of a quad
    i = [3, 1, 0, 2]
    j = [0, 2, 1, 3]

    cells_faces = np.dstack(
        (
            mesh.cells[:, i],
            mesh.cells[:, j],
        )
    )

    # complementary edges for the creation of "boundary" quads
    # (rotated quads with 1st edge as n-th edge of one original quad)
    t = [1, 0, 3, 2]
    k = np.array(i)[t]
    m = np.array(j)[t]

    cells = np.dstack(
        (
            cells_faces,
            mesh.cells[:, k],
            mesh.cells[:, m],
        )
    )

    return cells, cells_faces


def boundary_cells_quad8(mesh):
    "Convert the cells array of a quadratic quad mesh into a boundary cells array."

    cells_quad, cells_faces_quad = boundary_cells_quad(mesh)

    # midpoints on edges (boundary) of a quadratic quad
    m = [7, 5, 4, 6]

    cells_faces = np.dstack(
        (
            cells_faces_quad,
            mesh.cells[:, m],
        )
    )

    # complementary midpoints of edges for the creation of "boundary" quadratic quads
    # (rotated quads with 1st edge as n-th edge of one original quad)
    n = [5, 6, 7, 4]
    p = [4, 5, 6, 7]
    q = [6, 7, 4, 5]

    cells = np.dstack(
        (
            cells_quad,
            mesh.cells[:, m],
            mesh.cells[:, n],
            mesh.cells[:, p],
            mesh.cells[:, q],
        )
    )

    return cells, cells_faces


def boundary_cells_quad9(mesh):
    "Convert the cells array of a bi-quadratic quad mesh into a boundary cells array."

    cells_quad8, cells_faces_quad8 = boundary_cells_quad8(mesh)

    # midpoints on edges (boundary) of a quadratic quad
    r = [8, 8, 8, 8]

    cells = np.dstack(
        (
            cells_quad8,
            mesh.cells[:, r],
        )
    )

    return cells, cells_faces_quad8


def boundary_cells_hexahedron(mesh):
    "Convert the cells array of a hex mesh into a boundary cells array."

    # faces (boundary) of a hexahedron
    i = [0, 2, 1, 3, 0, 5]
    j = [3, 1, 0, 2, 1, 4]
    k = [7, 5, 4, 6, 2, 7]
    r = [4, 6, 5, 7, 3, 6]

    cells_faces = np.dstack(
        (
            mesh.cells[:, i],
            mesh.cells[:, j],
            mesh.cells[:, k],
            mesh.cells[:, r],
        )
    )

    # complementary faces for the creation of "boundary" hexahedrons
    # (6 rotated hexahedrons with 1st face as n-th face of
    #  one original hexahedron)
    m = [1, 3, 2, 0, 4, 1]
    n = [2, 0, 3, 1, 5, 0]
    p = [6, 4, 7, 5, 6, 3]
    q = [5, 7, 6, 4, 7, 2]

    cells = np.dstack(
        (
            cells_faces,
            mesh.cells[:, m],
            mesh.cells[:, n],
            mesh.cells[:, p],
            mesh.cells[:, q],
        )
    )

    return cells, cells_faces


def boundary_cells_hexahedron20(mesh):
    "Convert the cells array of a quadratic hex mesh into a boundary cells array."

    cells_hexahedron, cells_faces_hexahedron = boundary_cells_hexahedron(mesh)

    # midpoints on edges of faces (boundary) of a hexahedron
    i = [11, 9, 8, 10, 8, 12]
    j = [19, 17, 16, 18, 9, 15]
    k = [15, 13, 12, 14, 10, 14]
    v = [16, 18, 17, 19, 11, 13]

    cells_faces = np.dstack(
        (
            cells_faces_hexahedron,
            mesh.cells[:, i],
            mesh.cells[:, j],
            mesh.cells[:, k],
            mesh.cells[:, v],
        )
    )

    # complementary faces for the creation of "boundary" hexahedrons
    # (6 rotated hexahedrons with 1st face as n-th face of
    #  one original hexahedron)
    m = [9, 11, 10, 8, 12, 8]
    n = [18, 16, 19, 17, 13, 11]
    p = [13, 15, 14, 12, 14, 10]
    q = [17, 19, 18, 16, 15, 9]

    r = [8, 10, 9, 11, 16, 17]
    s = [10, 8, 11, 9, 17, 16]
    t = [14, 12, 15, 13, 18, 19]
    u = [12, 14, 13, 15, 19, 18]

    cells = np.dstack(
        (
            cells_hexahedron,
            mesh.cells[:, i],
            mesh.cells[:, j],
            mesh.cells[:, k],
            mesh.cells[:, v],
            mesh.cells[:, m],
            mesh.cells[:, n],
            mesh.cells[:, p],
            mesh.cells[:, q],
            mesh.cells[:, r],
            mesh.cells[:, s],
            mesh.cells[:, t],
            mesh.cells[:, u],
        )
    )

    return cells, cells_faces


def boundary_cells_hexahedron27(mesh):
    "Convert the cells array of a bi-quadratic hex mesh into a boundary cells array."

    cells_hexahedron20, cells_faces_hexahedron20 = boundary_cells_hexahedron20(mesh)

    # midpoints of faces (boundary) of a hexahedron
    i = [20, 21, 22, 23, 24, 25]

    cells_faces = np.dstack(
        (
            cells_faces_hexahedron20,
            mesh.cells[:, i],
        )
    )

    # complementary faces for the creation of "boundary" hexahedrons
    # (6 rotated hexahedrons with 1st face as n-th face of
    #  one original hexahedron)
    j = [22, 23, 21, 20, 20, 21]
    k = [23, 22, 20, 21, 21, 20]
    r = [24, 25, 24, 25, 22, 23]
    m = [25, 24, 25, 24, 23, 22]
    n = [20, 21, 22, 23, 24, 25]
    p = [21, 20, 23, 22, 25, 24]
    q = [26, 26, 26, 26, 26, 26]

    cells = np.dstack(
        (
            cells_hexahedron20,
            mesh.cells[:, j],
            mesh.cells[:, k],
            mesh.cells[:, r],
            mesh.cells[:, m],
            mesh.cells[:, n],
            mesh.cells[:, p],
            mesh.cells[:, q],
        )
    )

    return cells, cells_faces


[docs] class RegionBoundary(Region): r""" A numeric boundary-region as a combination of a mesh, an element and a numeric integration scheme (quadrature rule). Parameters ---------- mesh : Mesh A mesh with points and cells. element : Element The finite element formulation to be applied on the cells. quadrature: Quadrature An element-compatible numeric integration scheme with points and weights. grad : bool, optional A flag to invoke gradient evaluation (default is True). only_surface: bool, optional A flag to use only the enclosing outline of the region (default is True). mask: ndarray or None, optional A boolean array to select a specific set of points (default is None). ensure_3d : bool, optional A flag to enforce 3d area normal vectors. Attributes ---------- mesh : Mesh A mesh with points and cells. element : Finite element The finite element formulation to be applied on the cells. quadrature: Quadrature scheme An element-compatible numeric integration scheme with points and weights. h : ndarray Element shape function array ``h_aq`` of shape function ``a`` evaluated at quadrature point ``q``. dhdr : ndarray Partial derivative of element shape function array ``dhdr_aJq`` with shape function ``a`` w.r.t. natural element coordinate ``J`` evaluated at quadrature point ``q`` for every cell ``c`` (geometric gradient or **Jacobian** transformation between ``X`` and ``r``). dXdr : ndarray Geometric gradient ``dXdr_IJqc`` as partial derivative of undeformed coordinate ``I`` w.r.t. natural element coordinate ``J`` evaluated at quadrature point ``q`` for every cell ``c`` (geometric gradient or **Jacobian** transformation between ``X`` and ``r``). drdX : ndarray Inverse of dXdr. dA : ndarray Numeric *Differential area vectors*. normals : ndarray Area unit normal vectors. dV : ndarray Numeric *Differential volume element* as norm of *Differential area vectors*. dhdX : ndarray Partial derivative of element shape functions ``dhdX_aJqc`` of shape function ``a`` w.r.t. undeformed coordinate ``J`` evaluated at quadrature point ``q`` for every cell ``c``. Notes ----- The gradients of the element shape functions w.r.t the undeformed coordinates are evaluated at all integration points of each cell in the region if the optional gradient argument is ``True``. .. math:: \frac{\partial X_I}{\partial r_J} &= \hat{X}_{aI} \frac{ \partial h_a}{\partial r_J } \frac{\partial h_a}{\partial X_J} &= \frac{\partial h_a}{\partial r_I} \frac{\partial r_I}{\partial X_J} dV &= \det\left(\frac{\partial X_I}{\partial r_J}\right) w Examples -------- >>> import felupe as fem >>> mesh = fem.Rectangle(n=(3, 2)) >>> element = fem.Quad() >>> quadrature = fem.GaussLegendreBoundary(order=1, dim=2) >>> region = fem.RegionBoundary(mesh, element, quadrature) >>> region <felupe Region object> Element formulation: Quad Quadrature rule: GaussLegendreBoundary Gradient evaluated: True The numeric differential area vectors are the products of the cofactors of the geometric gradient :math:`\partial X_I / \partial r_J` and the weights `w` of the quadrature points. The differential area vectors array is of shape ``(nquadraturepoints, ndim, nboundarycells)``. >>> region.dA array([[[ 0. , -0.5 , 0. , 0.5 , 0. , 0. ], [ 0. , -0.5 , 0. , 0.5 , 0. , 0. ]], <BLANKLINE> [[-0.25, -0. , -0.25, -0. , 0.25, 0.25], [-0.25, -0. , -0.25, -0. , 0.25, 0.25]]]) In a boundary region, the numeric differential volumes are the magnitudes of the differential area vectors. For a quad mesh, the boundary cell volumes are the edge lengths. >>> region.dV.sum(axis=0) array([0.5, 1. , 0.5, 1. , 0.5, 0.5]) Unit normal vectors are obtained by the ratio of the differential area vectors and the differential volumes. >>> region.dA / region.dV ## this is equal to ``region.normals`` array([[[ 0., -1., 0., 1., 0., 0.], [ 0., -1., 0., 1., 0., 0.]], <BLANKLINE> [[-1., -0., -1., -0., 1., 1.], [-1., -0., -1., -0., 1., 1.]]]) The partial derivative of the first element shape function w.r.t. the undeformed coordinates evaluated at the second integration point of the last element of the region: >>> region.dhdX[0, :, 1, -1] array([2. , 0.21132487]) """ def __init__( self, mesh, element, quadrature, grad=True, only_surface=True, mask=None, ensure_3d=False, ): self.only_surface = only_surface self.mask = mask self.ensure_3d = ensure_3d if mesh.cell_type == "quad": cells, cells_faces = boundary_cells_quad(mesh) elif mesh.cell_type == "quad8": cells, cells_faces = boundary_cells_quad8(mesh) elif mesh.cell_type == "quad9": cells, cells_faces = boundary_cells_quad9(mesh) elif mesh.cell_type == "hexahedron": cells, cells_faces = boundary_cells_hexahedron(mesh) elif mesh.cell_type == "hexahedron20": cells, cells_faces = boundary_cells_hexahedron20(mesh) elif mesh.cell_type == "hexahedron27": cells, cells_faces = boundary_cells_hexahedron27(mesh) else: raise NotImplementedError("Cell type not supported.") cells_faces = cells_faces.reshape(-1, cells_faces.shape[-1]) cells = cells.reshape(-1, cells.shape[-1]) if self.only_surface: # sort faces, get indices of unique faces and counts cells_faces_sorted = np.sort(cells_faces, axis=1) cells_faces_unique, index, counts = np.unique( cells_faces_sorted, True, False, True, 0 ) self._index = index self._mask = counts == 1 else: self._index = np.arange(len(cells_faces)) self._mask = np.ones_like(self._index, dtype=bool) self._selection = self._index[self._mask] # merge with point-mask if mask is not None: point_selection = np.arange(len(mesh.points))[mask] self._selection = self._selection[ np.all(np.isin(cells_faces[self._selection], point_selection), axis=1) ] # get cell-faces and cells on boundary (unique cell-faces with one count) cells_on_boundary = cells[self._selection] # create mesh on boundary mesh_boundary_cells = mesh.copy() mesh_boundary_cells.update(cells=cells_on_boundary) self.mesh = mesh_boundary_cells self.mesh.cells_faces = cells_faces[self._selection] # init region and faces super().__init__(mesh_boundary_cells, element, quadrature, grad=grad) if grad: self.dA, self.dV, self.normals = self._init_faces() def _init_faces(self): "Initialize (norm of) face normals of cells." if ( self.mesh.cell_type == "quad" or self.mesh.cell_type == "quad8" or self.mesh.cell_type == "quad9" ): dA_1 = self.dXdr[:, 0][::-1] dA_1[0] = -dA_1[0] elif ( self.mesh.cell_type == "hexahedron" or self.mesh.cell_type == "hexahedron20" or self.mesh.cell_type == "hexahedron27" ): dA_1 = cross(self.dXdr[:, 0], self.dXdr[:, 1]) dA = -dA_1 * self.quadrature.weights.reshape(-1, 1) # norm and unit normal vector dV = np.linalg.norm(dA, axis=0) normals = dA / dV if self.ensure_3d: if dA.shape[0] == 2: dA = np.pad(dA, ((0, 1), (0, 0), (0, 0))) normals = np.pad(normals, ((0, 1), (0, 0), (0, 0))) return dA, dV, normals
[docs] def mesh_faces(self): "Return a Mesh with face-cells on the selected boundary." face_type = { "quad": "line", "hexahedron": "quad", "quad8": "line3", "quad9": "line3", }[self.mesh.cell_type] return Mesh(self.mesh.points, self.mesh.cells_faces, face_type)