# -*- 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
[docs]class Boundary:
"""A Boundary as a collection of prescribed degrees of freedom (numbered
coordinate components of a field at points of a mesh).
Parameters
----------
field : felupe.Field
Field on wich the boundary is created.
name : str, optional (default is "default")
Name of the boundary.
fx : float or callable, optional
Mask-function for x-component of mesh-points which returns `True` at points
on which the boundary will be applied (default is ``np.isnan``). If a float is
passed, this is transformed to ``lambda x: np.isclose(x, fx)``.
fy : float or callable, optional
Mask-function for y-component of mesh-points which returns `True` at points
on which the boundary will be applied (default is ``np.isnan``). If a float is
passed, this is transformed to ``lambda y: np.isclose(y, fy)``.
fz : float or callable, optional
Mask-function for z-component of mesh-points which returns `True` at points
on which the boundary will be applied (default is ``np.isnan``). If a float is
passed, this is transformed to ``lambda z: np.isclose(z, fz)``.
value : ndarray or float, optional
Value(s) of the selected (prescribed) degrees of freedom (default is 0.0).
skip : tuple of bool or int, optional
A tuple to define which axes of the selected points should be skipped, i.e.
not prescribed (default is ``(False, False, False)``).
mask : ndarray
Boolean mask for the prescribed degrees of freedom. If a mask is passed, ``fx``,
``fy`` and ``fz`` are ignored. However, ``skip`` is still applied on the mask.
mode : string, optional
A string which defines the logical operation for the selected points per
axis (default is `or`).
Attributes
----------
mask : ndarray
1d- or 2d-boolean mask array for the prescribed degrees of freedom.
dof : ndarray
1d-array of ints which contains the prescribed degrees of freedom.
points : ndarray
1d-array of ints which contains the point ids on which one or more degrees of
freedom are prescribed.
value : ndarray or float
Value of the selected (prescribed) degrees of freedom.
Examples
--------
A boundary condition prescribes values for chosen degrees of freedom of a given
field (**not** a field container). This is demonstrated for a plane-strain
vector field on a quad-mesh of a circle.
>>> import felupe as fem
>>> mesh = fem.Circle(radius=1, n=6)
>>> x, y = mesh.points.T
>>> region = fem.RegionQuad(mesh)
>>> displacement = fem.FieldPlaneStrain(region, dim=2)
>>> field = fem.FieldContainer([displacement])
A boundary on the displacement field which prescribes all components of the field
on the outermost left point of the circle is created. The easiest way is to pass the
desired value to ``fx``. The same result is obtained if a callable function is
passed to ``fx``.
>>> left = fem.Boundary(displacement, fx=x.min())
>>> left = fem.Boundary(displacement, fx=lambda x: np.isclose(x, x.min()))
>>> plotter = mesh.plot(off_screen=True)
>>> plotter.add_points(
>>> np.pad(mesh.points[left.points], ((0, 0), (0, 1))),
>>> point_size=20,
>>> color="red",
>>> )
>>> img = plotter.screenshot("boundary_left.png", transparent_background=True)
.. image:: images/boundary_left.png
If ``fx`` and ``fy`` are given, the masks are combined by *logical-or*.
>>> axes = fem.Boundary(displacement, fx=0, fy=0, mode="or")
.. image:: images/boundary_axes.png
This may be changed to *logical-and* if desired.
>>> center = fem.Boundary(displacement, fx=0, fy=0, mode="and")
.. image:: images/boundary_center.png
For the most-general case, a user-defined boolean mask for the selection of the
mesh-points is provided. While the two upper methods are useful to select
points separated per point-coordinates, providing a mask is more flexible as
it may involve all three coordinates (or any other quantities of interest).
>>> mask = np.logical_and(np.isclose(x**2 + y**2, 1), x <= 0)
>>> surface = fem.Boundary(displacement, mask=mask)
.. image:: images/boundary_surface.png
A boundary condition may be skipped on given axes, i.e. if only the x-components
of a field should be prescribed on the selected points, then the y-axis must
be skipped.
>>> axes_x = fem.Boundary(displacement, fx=0, fy=0, skip=(False, True))
Values for the prescribed degress of freedom are either applied during creation
or by the update-method.
>>> left = fem.Boundary(displacement, fx=x.min(), value=-0.2)
>>> left.update(-0.3)
Sometimes it is useful to create a boundary with all axes skipped. This
boundary has no prescribed degrees of freedom and hence, is without effect.
However, it may still be used in a characteristic job for the boundary to be
tracked.
See Also
--------
felupe.CharacteristicCurve : A job with a boundary to be tracked.
felupe.dof.partition : Partition degrees of freedom into prescribed and active dof.
felupe.dof.apply : Apply prescribed values for a list of boundaries.
"""
def __init__(
self,
field,
name="default",
fx=np.isnan,
fy=np.isnan,
fz=np.isnan,
value=0.0,
skip=(False, False, False),
mask=None,
mode="or",
):
mesh = field.region.mesh
dof = field.indices.dof
self.field = field
self.dim = field.dim # mesh.dim
self.name = name
self.value = value
self.skip = np.array(skip).astype(int)[: mesh.dim] # self.dim
self.mode = mode
# check if callable
_fx = fx if callable(fx) else lambda x: np.isclose(x, fx)
_fy = fy if callable(fy) else lambda y: np.isclose(y, fy)
_fz = fz if callable(fz) else lambda z: np.isclose(z, fz)
self.fun = [_fx, _fy, _fz][: mesh.dim]
if mask is None:
# apply functions on the points per coordinate
# fx(x), fy(y), fz(z) and create a mask for each coordinate
mask = [f(x) for f, x in zip(self.fun, mesh.points.T)]
# select the logical combination function "or" or "and"
combine = {"or": np.logical_or, "and": np.logical_and}[self.mode]
# combine the masks with "logical_or" if dim > 1
if mesh.dim == 1:
mask = mask[0]
elif mesh.dim == 2:
mask = combine(mask[0], mask[1])
elif mesh.dim == 3: # and mesh.points.shape[1] == 3:
tmp = np.logical_or(mask[0], mask[1])
mask = combine(tmp, mask[2])
# tile the mask
self.mask = np.tile(mask.reshape(-1, 1), self.dim)
# check if some axes should be skipped
if True not in skip:
pass
else:
# exclude mask from axes which should be skipped
self.mask[:, np.where(self.skip)[0]] = False
self.dof = dof[self.mask]
self.points = np.arange(mesh.npoints)[mask]
[docs] def update(self, value):
"Update the value of the boundary in-place."
self.value = value