# -*- 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 warnings
from copy import deepcopy
import numpy as np
from ..math import det, inv
[docs]
class Region:
r"""
A numeric 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). If True, the partial
derivatives of the element shape functions w.r.t. undeformed coordinates
:math:`\frac{\partial \boldsymbol{h}(\boldsymbol{r})}{\partial \boldsymbol{X}}`
and the differential volumes :math:`dV` are evaluated.
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.
dV : ndarray
Numeric *differential volume element* as product of determinant of geometric
gradient ``dV_qc = det(dXdr)_qc w_q`` and quadrature weight ``w_q``, evaluated
at quadrature point ``q`` for every cell ``c``.
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.GaussLegendre(order=1, dim=2)
>>> region = fem.Region(mesh, element, quadrature)
>>> region
<felupe Region object>
Element formulation: Quad
Quadrature rule: GaussLegendre
Gradient evaluated: True
The numeric differential volumes are the products of the determinant of the
geometric gradient :math:`\frac{\partial X_I}{\partial r_J}` and the weights `w` of
the quadrature points. The differential volume array is of shape
``(nquadraturepoints, ncells)``.
>>> region.dV
array([[0.125, 0.125],
[0.125, 0.125],
[0.125, 0.125],
[0.125, 0.125]])
Cell-volumes may be obtained by cell-wise sums of the differential volumes located
at the quadrature points.
>>> region.dV.sum(axis=0)
array([0.5, 0.5])
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([-1.57735027, -0.21132487])
"""
def __init__(self, mesh, element, quadrature, grad=True):
self.evaluate_gradient = grad
self.reload(mesh=mesh, element=element, quadrature=quadrature)
[docs]
def copy(self, mesh=None, element=None, quadrature=None):
"""Return a copy of the region and reload it if necessary.
Parameters
----------
mesh : Mesh or None, optional
A mesh with points and cells (default is None).
element : Element or None, optional
The finite element formulation to be applied on the cells (default is None).
quadrature: Quadrature or None, optional
An element-compatible numeric integration scheme with points and weights
(default is None).
Returns
-------
Region
A copy of the reloaded region.
See Also
--------
felupe.Region.reload : Reload the numeric region inplace.
"""
region = deepcopy(self)
region.reload(mesh=mesh, element=element, quadrature=quadrature)
return region
[docs]
def reload(self, mesh=None, element=None, quadrature=None):
"""Reload the numeric region inplace.
Parameters
----------
mesh : Mesh or None, optional
A mesh with points and cells (default is None).
element : Element or None, optional
The finite element formulation to be applied on the cells (default is None).
quadrature: Quadrature or None, optional
An element-compatible numeric integration scheme with points and weights
(default is None).
Examples
--------
.. warning::
If the points of a mesh are modified and a region was already created with
the mesh, it is important to re-evaluate (reload) the
:class:`~felupe.Region` inplace.
>>> import felupe as fem
>>>
>>> mesh = fem.Cube(n=6)
>>> region = fem.RegionHexahedron(mesh)
>>> field = fem.FieldContainer([fem.Field(region, dim=3)])
>>>
>>> new_points = mesh.rotate(angle_deg=-90, axis=2).points
>>> mesh.update(points=new_points, callback=region.reload)
See Also
--------
felupe.Mesh.update : Update the mesh with given points and cells arrays inplace.
Optionally, a callback is evaluated.
"""
region = self
if mesh is not None:
region.mesh = mesh
if element is not None:
region.element = element
if quadrature is not None:
region.quadrature = quadrature
if mesh is not None or element is not None or quadrature is not None:
# element shape function
region.element.h = np.array(
[region.element.function(q) for q in region.quadrature.points]
).T
region.h = np.tile(np.expand_dims(region.element.h, -1), region.mesh.ncells)
# partial derivative of element shape function
region.element.dhdr = np.array(
[region.element.gradient(q) for q in region.quadrature.points]
).transpose(1, 2, 0)
region.dhdr = np.tile(
np.expand_dims(region.element.dhdr, -1), region.mesh.ncells
)
if region.evaluate_gradient:
# geometric gradient
region.dXdr = np.einsum(
"caI,aJqc->IJqc", region.mesh.points[region.mesh.cells], region.dhdr
)
# determinant and inverse of dXdr
J = det(region.dXdr)
region.drdX = inv(region.dXdr, determinant=J)
# numeric **differential volume element**
region.dV = np.multiply(
J, region.quadrature.weights.reshape(-1, 1), out=J
)
# check for negative **differential volume elements**
if np.any(region.dV < 0):
cells_negative_volume = np.where(np.any(region.dV < 0, axis=0))[0]
message_negative_volumes = "".join(
[
f"Negative volumes for cells \n {cells_negative_volume}\n",
"Try ``mesh.flip(np.any(region.dV < 0, axis=0))`` ",
"and re-create the region.",
]
)
warnings.warn(message_negative_volumes)
# Partial derivative of element shape function
# w.r.t. undeformed coordinates
region.dhdX = np.einsum("aIqc,IJqc->aJqc", region.dhdr, region.drdX)
def __repr__(self):
header = "<felupe Region object>"
element = f" Element formulation: {type(self.element).__name__}"
quadrature = f" Quadrature rule: {type(self.quadrature).__name__}"
grad = f" Gradient evaluated: {hasattr(self, 'dV')}"
return "\n".join([header, element, quadrature, grad])
[docs]
def plot(self, **kwargs):
"""Plot the element with point-ids and the quadrature points,
scaled by their weights."""
return self.quadrature.plot(plotter=self.element.plot(**kwargs))
[docs]
def screenshot(
self,
filename=None,
transparent_background=None,
scale=None,
**kwargs,
):
"""Take a screenshot of the element with the applied quadrature.
See Also
--------
pyvista.Plotter.screenshot: Take a screenshot of a PyVista plotter.
"""
if filename is None:
filename = f"region-{self.element.cell_type}.png"
return self.plot(off_screen=True, **kwargs).screenshot(
filename=filename,
transparent_background=transparent_background,
scale=scale,
)