# -*- 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 = [4, 6, 5, 7]
p = [5, 7, 6, 4]
q = [6, 4, 7, 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.
tangents : list of ndarray
List of standard unit 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
Hessian evaluated: False
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, is obtained by:
>>> region.dhdX[0, :, 1, -1]
array([2. , 0.21132487])
The faces-cells may be used to create a mesh on the boundary.
>>> fem.Mesh(points=mesh.points, cells=region.mesh.cells_faces, cell_type="line")
<felupe Mesh object>
Number of points: 6
Number of cells:
line: 6
A second example shows the standard unit vectors and the area normals, located at
the quadrature points for all edges of a quadrilateral.
.. pyvista-plot::
:force_static:
>>> import felupe as fem
>>> import numpy as np
>>> import pyvista as pv
>>>
>>> m = fem.Rectangle(n=2)
>>> mesh = m.copy()
>>> mesh.points[-1, 0] += 0.5
>>> edges = fem.RegionQuadBoundary(mesh)
>>>
>>> start = fem.Field(edges, values=mesh.points).interpolate()
>>> (direction,) = edges.tangents
>>>
>>> # 3d-data required for plotting
>>> start = np.insert(start, len(start), 0, axis=0)
>>> direction = np.insert(direction, len(direction), 0, axis=0)
>>> normal = np.insert(edges.normals, len(edges.normals), 0, axis=0)
>>>
>>> plotter = pv.Plotter()
... actor = plotter.add_arrows(
... start.reshape(3, -1).T,
... direction.reshape(3, -1).T,
... show_scalar_bar=False,
... mag=1 / 7,
... color="red",
... label="tangents",
... )
>>> actor = plotter.add_arrows(
... start.reshape(3, -1).T,
... normal.reshape(3, -1).T,
... show_scalar_bar=False,
... mag=1 / 7,
... color="green",
... label="normals",
... )
>>> actor = plotter.add_legend()
>>> mesh.plot(plotter=plotter, style="wireframe").show()
A third example shows the standard unit vectors and the area normals, located at the
quadrature points for one face of a hexahedron.
.. pyvista-plot::
:force_static:
>>> import felupe as fem
>>> import numpy as np
>>> import pyvista as pv
>>>
>>> m = fem.Cube(n=2)
>>> mesh = m.copy()
>>> mesh.points[-1, 0] += 0.5
>>> faces = fem.RegionHexahedronBoundary(mesh, mask=m.x == m.x.max())
>>>
>>> start = fem.Field(faces, values=mesh.points).interpolate()
>>> direction, direction_2 = faces.tangents
>>>
>>> plotter = pv.Plotter()
>>> actor = plotter.add_arrows(
... start.reshape(3, -1).T,
... direction.reshape(3, -1).T,
... show_scalar_bar=False,
... mag=1 / 7,
... color="red",
... label="tangents (1)",
... )
>>> actor = plotter.add_arrows(
... start.reshape(3, -1).T,
... direction_2.reshape(3, -1).T,
... show_scalar_bar=False,
... mag=1 / 7,
... color="green",
... label="tangents (2)",
... )
>>> actor = plotter.add_arrows(
... start.reshape(3, -1).T,
... faces.normals.reshape(3, -1).T,
... show_scalar_bar=False,
... mag=1 / 7,
... color="blue",
... label="normals",
... )
>>> plotter.add_legend()
>>> mesh.plot(plotter=plotter, style="wireframe").show()
This examples shows how to evaluate the strain on the faces of the boundary region.
A field can't be used to evaluate the strain of a quad-cell in 3D space. However,
it is possible to evaluate the in-plane components of the right Cauchy-Green
deformation tensor on the boundary. With this tensor at hand, it is possible to
evaluate any in-plane strain tensor.
.. pyvista-plot::
:force_static:
>>> import felupe as fem
>>>
>>> mesh = fem.Cube(n=3)
>>> region = fem.RegionHexahedron(mesh)
>>> field = fem.FieldContainer([fem.Field(region, dim=3)])
>>>
>>> field[0].values[mesh.x == 1, 0] += 0.5
>>> field[0].values[mesh.x == 1, 2] += 0.5
>>> field[0].values += mesh.rotate(-30, axis=1).points - mesh.points
>>>
>>> face = fem.RegionHexahedronBoundary(mesh, mask=mesh.x == mesh.x.max())
>>> u = fem.FieldContainer([fem.Field(face, dim=3)])
>>> u.link(field)
>>>
>>> mesh_faces = face.mesh_faces()
>>> mesh_faces.points += field[0].values
>>>
>>> C = fem.math.inplane(u.evaluate.right_cauchy_green_deformation(), face.tangents)
>>> e = fem.math.strain(u, C=C, k=0)
>>>
>>> view = mesh_faces.view(
... cell_data={"ep": fem.math.eigvalsh(e).max(axis=0).mean(axis=-2).T}
... )
>>> view.plot(
... "ep", label="Strain (Max. Principal)", plotter=field.plot(style="wireframe")
... ).show()
"""
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.tangents = self._init_faces()
def _init_faces(self):
"Initialize (norm of) face normals of cells."
tangents = []
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]
dX_1 = -self.dXdr[:, 0]
dX_1[0] = -dX_1[0]
tangents.append(dX_1 / np.linalg.norm(dX_1, axis=0))
if self.ensure_3d:
tangents[0] = np.insert(tangents[0], len(tangents[0]), 0, axis=0)
other_tangent = np.zeros_like(tangents[0])
other_tangent[2] = 1.0
tangents.append(other_tangent)
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])
tangents.append(-self.dXdr[:, 0] / np.linalg.norm(self.dXdr[:, 0], axis=0))
tangents.append(self.dXdr[:, 1] / np.linalg.norm(self.dXdr[:, 1], axis=0))
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, tangents
[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)