import numpy as np
from matplotlib.patches import Polygon
from feastruct.post.post import ScalarResult
[docs]class BoundaryCondition:
"""Parent class for supports and loads.
Provides an init method for the creation of boundary conditions.
:cvar node: The node object at which the boundary condition acts
:vartype node: :class:`~feastruct.fea.node.Node`
:cvar float val: The value of the boundary condition
:cvar int dof: The degree of freedom about which the boundary condition acts
"""
[docs] def __init__(self, node, val, dof):
"""Inits the BoundaryCondition class.
:param node: The node object at which the boundary condition acts
:type node: :class:`~feastruct.fea.node.Node`
:param float val: The value of the boundary condition
:param int dof: The degree of freedom about which the boundary condition acts
"""
# assign the node object, value and dof of the boundary condition
self.node = node
self.val = val
self.dof = dof
[docs] def get_gdof(self):
"""Returns the global degree of freedom number for the boundary condition.
:returns: Global degree of freedom number
:rtype: int
"""
return self.node.dofs[self.dof].global_dof_num
[docs]class NodalSupport(BoundaryCondition):
"""Class for a dirichlet boundary condition acting at a node.
Provides methods for the FEA solver and post-processing.
:cvar node: The node object at which the boundary condition acts
:vartype node: :class:`~feastruct.fea.node.Node`
:cvar float val: The value of the boundary condition
:cvar int dof: The degree of freedom about which the boundary condition acts
:cvar reactions: A list of reaction objects
:vartype reactions: list[:class:`~feastruct.post.post.ScalarResult`]
"""
[docs] def __init__(self, node, val, dof):
"""inits the NodalSupport class.
:param node: The node object at which the boundary condition acts
:type node: :class:`~feastruct.fea.node.Node`
:param float val: The value of the boundary condition
:param int dof: The degree of freedom about which the boundary condition acts
"""
# initialise the parent class
super().__init__(node, val, dof)
# initialise the nodal reaction results
self.reactions = []
[docs] def apply_support(self, K, f_ext):
"""Applies the nodal support.
The stiffness matrix and external force vector are modified to apply the dirichlet boundary
condition to enforce the displacement at the chosen degree of freedom to be equal to the
specified value.
:param K: Global stiffness matrix of size *(N x N)*
:type K: :class:`numpy.ndarray`
:param f_ext: Global external force vector of size *N*
:type f_ext: :class:`numpy.ndarray`
"""
# get gdof number for the support
gdof = self.node.dofs[self.dof].global_dof_num
# modify stiffness matrix and f_ext
K[gdof, :] = 0
K[gdof, gdof] = 1
f_ext[gdof] = self.val
[docs] def get_reaction(self, analysis_case):
"""Gets the reaction force result corresponding to analysis_case.
:param analysis_case: Analysis case
:type analysis_case: :class:`~feastruct.fea.cases.AnalysisCase`
:returns: Reaction force at the node
:rtype: float
"""
# loop through reactions
for reaction in self.reactions:
if reaction.analysis_case == analysis_case:
return reaction.result
[docs] def save_reaction(self, f, analysis_case):
"""Saves the reaction force corresponding to analysis_case.
:param float f: Reaction force at the node
:param analysis_case: Analysis case
:type analysis_case: :class:`~feastruct.fea.cases.AnalysisCase`
"""
# check to see if there is already a reaction for the current analysis_case
for reaction in self.reactions:
if reaction.analysis_case == analysis_case:
reaction.result = f
return
# if there isn't already a reaction for the current analysis_case
self.reactions.append(ScalarResult(result=f, analysis_case=analysis_case))
[docs] def plot_support(self, ax, small, get_support_angle, analysis_case, deformed, def_scale):
"""Plots a graphical representation of the nodal support.
Based on the type of support at the node, a graphical representation of the support type is
generated and plotted. Possible support types include rollers, hinges, rotation restraints,
fixed rollers and fully fixed supports. The angle of the connecting elements is considered
in order to produce the most visually appealing representation. The support location is
displaced if a deformed plot is desired. Note that some of the methods used to plot the
supports are taken from Frans van der Meer's code plotGeom.m.
:param ax: Axes object on which to plot
:type ax: :class:`matplotlib.axes.Axes`
:param float small: A dimension used to scale the support
:param get_support_angle: A function that returns the support angle and the number of
connected elements
:type get_support_angle: :func:`feastruct.post.post.PostProcessor.get_support_angle`
:param analysis_case: Analysis case
:type analysis_case: :class:`~feastruct.fea.cases.AnalysisCase`
:param bool deformed: Represents whether or not the node locations are deformed based on
the results of analysis_case
:param float def_scale: Value used to scale deformations
"""
fixity = analysis_case.freedom_case.get_nodal_fixities(node=self.node)
if fixity not in ([1, 1, 0], [0, 1, 0]):
(angle, num_el) = get_support_angle(self.node)
if fixity == [1, 0, 0]:
# ploy a y-roller
angle = round(angle / 180) * 180
self.plot_xysupport(ax, angle, True, num_el == 1, small, analysis_case, deformed,
def_scale)
elif fixity == [0, 1, 0]:
(angle, num_el) = get_support_angle(self.node, 1)
# plot an x-roller
if np.mod(angle + 1, 180) < 2: # prefer support below
angle = 90
else:
angle = round((angle + 90) / 180) * 180 - 90
self.plot_xysupport(ax, angle, True, num_el == 1, small, analysis_case, deformed,
def_scale)
elif fixity == [1, 1, 0]:
# plot a hinge
(angle, num_el) = get_support_angle(self.node, 1)
self.plot_xysupport(ax, angle, False, num_el == 1, small, analysis_case, deformed,
def_scale)
elif fixity == [0, 0, 1]:
ax.plot(self.node.x, self.node.y, 'kx', markersize=8)
else:
# plot a support with moment fixity
if fixity == [1, 1, 1]:
# plot a fixed support
s = np.sin(angle * np.pi / 180)
c = np.cos(angle * np.pi / 180)
rot_mat = np.array([[c, -s], [s, c]])
line = np.array([[0, 0], [-1, 1]]) * small
rect = np.array([[-0.6, -0.6, 0, 0], [-1, 1, 1, -1]]) * small
ec = 'none'
elif fixity == [1, 0, 1]:
# plot y-roller block
angle = round(angle / 180) * 180
s = np.sin(angle * np.pi / 180)
c = np.cos(angle * np.pi / 180)
rot_mat = np.array([[c, -s], [s, c]])
line = np.array([[-0.85, -0.85], [-1, 1]]) * small
rect = np.array([[-0.6, -0.6, 0, 0], [-1, 1, 1, -1]]) * small
ec = 'k'
elif fixity == [0, 1, 1]:
# plot x-roller block
angle = round((angle + 90) / 180) * 180 - 90
s = np.sin(angle * np.pi / 180)
c = np.cos(angle * np.pi / 180)
rot_mat = np.array([[c, -s], [s, c]])
line = np.array([[-0.85, -0.85], [-1, 1]]) * small
rect = np.array([[-0.6, -0.6, 0, 0], [-1, 1, 1, -1]]) * small
ec = 'k'
rot_line = np.matmul(rot_mat, line)
rot_rect = np.matmul(rot_mat, rect)
# add coordinates of node
if deformed:
# get displacement of node for current analysis case
u = [0, 0]
u[0] = self.node.dofs[0].get_displacement(analysis_case)
u[1] = self.node.dofs[1].get_displacement(analysis_case)
rot_line[0, :] += self.node.x + u[0] * def_scale
rot_line[1, :] += self.node.y + u[1] * def_scale
rot_rect[0, :] += self.node.x + u[0] * def_scale
rot_rect[1, :] += self.node.y + u[1] * def_scale
else:
rot_line[0, :] += self.node.x
rot_line[1, :] += self.node.y
rot_rect[0, :] += self.node.x
rot_rect[1, :] += self.node.y
ax.plot(rot_line[0, :], rot_line[1, :], 'k-', linewidth=1)
ax.add_patch(Polygon(np.transpose(rot_rect), facecolor=(0.7, 0.7, 0.7), edgecolor=ec))
[docs] def plot_imposed_disp(self, ax, max_disp, small, get_support_angle, analysis_case, deformed,
def_scale):
"""Plots a graphical representation of an imposed translation.
:param ax: Axes object on which to plot
:type ax: :class:`matplotlib.axes.Axes`
:param float max_disp: Maximum imposed displacement in the analysis case
:param float small: A dimension used to scale the support
:param get_support_angle: A function that returns the support angle and the number of
connected elements
:type get_support_angle: :func:`feastruct.post.post.PostProcessor.get_support_angle`
:param analysis_case: Analysis case
:type analysis_case: :class:`~feastruct.fea.cases.AnalysisCase`
:param bool deformed: Represents whether or not the node locations are deformed based on
the results of case id
:param float def_scale: Value used to scale deformations
"""
val = self.val / max_disp
offset = 0.5 * small
lf = abs(val) * 1.5 * small # arrow length
lh = 0.6 * small # arrow head length
wh = 0.6 * small # arrow head width
sp = 0.15 * small # half spacing between double line
lf = max(lf, lh * 1.5)
(angle, num_el) = get_support_angle(self.node)
s = np.sin(angle * np.pi / 180)
c = np.cos(angle * np.pi / 180)
n = np.array([c, s])
inward = (n[self.dof] == 0 or np.sign(n[self.dof]) == np.sign(val))
to_rotate = (self.dof) * 90 + (n[self.dof] >= 0) * 180
sr = np.sin(to_rotate * np.pi / 180)
cr = np.cos(to_rotate * np.pi / 180)
rot_mat = np.array([[cr, -sr], [sr, cr]])
x0 = offset + inward * lf
x2 = offset + (not inward) * lf
x1 = x2 + (inward) * lh - (not inward) * lh
pp = np.array([[x1, x1, x2], [-wh / 2, wh / 2, 0]])
ll = np.array([[x1, x0, x0, x1], [sp, sp, -sp, -sp]])
rl = np.matmul(rot_mat, ll)
rp = np.matmul(rot_mat, pp)
# add coordinates of node
if deformed:
# get displacement of node for current analysis case
u = [0, 0]
u[0] = self.node.dofs[0].get_displacement(analysis_case)
u[1] = self.node.dofs[1].get_displacement(analysis_case)
rp[0, :] += self.node.x + u[0] * def_scale
rp[1, :] += self.node.y + u[1] * def_scale
rl[0, :] += self.node.x + u[0] * def_scale
rl[1, :] += self.node.y + u[1] * def_scale
else:
rp[0, :] += self.node.x
rp[1, :] += self.node.y
rl[0, :] += self.node.x
rl[1, :] += self.node.y
ax.plot(rl[0, :], rl[1, :], 'k-')
ax.add_patch(Polygon(np.transpose(rp), facecolor='none', linewidth=1, edgecolor='k'))
[docs] def plot_imposed_rot(self, ax, small, get_support_angle, analysis_case, deformed, def_scale):
"""Plots a graphical representation of an imposed rotation.
:param ax: Axes object on which to plot
:type ax: :class:`matplotlib.axes.Axes`
:param float small: A dimension used to scale the support
:param get_support_angle: A function that returns the support angle and the number of
connected elements
:type get_support_angle: :func:`feastruct.post.post.PostProcessor.get_support_angle`
:param analysis_case: Analysis case
:type analysis_case: :class:`~feastruct.fea.cases.AnalysisCase`
:param bool deformed: Represents whether or not the node locations are deformed based on
the results of case id
:param float def_scale: Value used to scale deformations
"""
lh = 0.4 * small # arrow head length
wh = 0.4 * small # arrow head width
r1 = 1.0 * small
r2 = 1.2 * small
(angle, num_el) = get_support_angle(self.node)
ths = np.arange(100, 261)
s = np.sin(angle * np.pi / 180)
c = np.cos(angle * np.pi / 180)
rot_mat = np.array([[c, -s], [s, c]])
# make arrow tail around (0,0)
rr = (r1 + r2) / 2
ll = np.array([rr * np.cos(ths * np.pi / 180), rr * np.sin(ths * np.pi / 180)])
l1 = np.array([r1 * np.cos(ths * np.pi / 180), r1 * np.sin(ths * np.pi / 180)])
l2 = np.array([r2 * np.cos(ths * np.pi / 180), r2 * np.sin(ths * np.pi / 180)])
# make arrow head at (0,0)
pp = np.array([[-lh, -lh, 0], [-wh / 2, wh / 2, 0]])
# rotate arrow head around (0,0)
if self.val > 0:
thTip = 90 - ths[11]
xTip = ll[:, -1]
l1 = l1[:, 1:-21]
l2 = l2[:, 1:-21]
ibase = 0
else:
thTip = ths[11] - 90
xTip = ll[:, 0]
l1 = l1[:, 21:]
l2 = l2[:, 21:]
ibase = np.shape(l1)[1] - 1
cTip = np.cos(thTip * np.pi / 180)
sTip = np.sin(thTip * np.pi / 180)
rTip = np.array([[cTip, -sTip], [sTip, cTip]])
pp = np.matmul(rTip, pp)
# shift arrow head to tip
pp[0, :] += xTip[0]
pp[1, :] += xTip[1]
# rotate arrow to align it with the node
rl1 = np.matmul(rot_mat, l1)
rl2 = np.matmul(rot_mat, l2)
rp = np.matmul(rot_mat, pp)
# add coordinates of node
if deformed:
# get displacement of node for current analysis case
u = [0, 0]
u[0] = self.node.dofs[0].get_displacement(analysis_case)
u[1] = self.node.dofs[1].get_displacement(analysis_case)
rp[0, :] += self.node.x + u[0] * def_scale
rp[1, :] += self.node.y + u[1] * def_scale
rl1[0, :] += self.node.x + u[0] * def_scale
rl1[1, :] += self.node.y + u[1] * def_scale
rl2[0, :] += self.node.x + u[0] * def_scale
rl2[1, :] += self.node.y + u[1] * def_scale
else:
rp[0, :] += self.node.x
rp[1, :] += self.node.y
rl1[0, :] += self.node.x
rl1[1, :] += self.node.y
rl2[0, :] += self.node.x
rl2[1, :] += self.node.y
# shift arrow to node and plot
ax.plot(rl1[0, :], rl1[1, :], 'k-')
ax.plot(rl2[0, :], rl2[1, :], 'k-')
ax.plot(np.append(rl1[0, ibase], rl2[0, ibase]), np.append(rl1[1, ibase], rl2[1, ibase]),
'k-')
ax.add_patch(Polygon(np.transpose(rp), facecolor='none', linewidth=1, edgecolor='k'))
[docs] def plot_reaction(self, ax, max_reaction, small, get_support_angle, analysis_case):
"""Plots a graphical representation of a reaction force and displays the value of the
reaction force. A straight arrow is plotted for a translational reaction and a curved arrow
is plotted for a rotational reaction.
:param ax: Axes object on which to plot
:type ax: :class:`matplotlib.axes.Axes`
:param float max_reaction: Maximum reaction force in the analysis case
:param float small: A dimension used to scale the support
:param get_support_angle: A function that returns the support angle and the number of
connected elements
:type get_support_angle: :func:`feastruct.post.post.PostProcessor.get_support_angle`
:param analysis_case: Analysis case
:type analysis_case: :class:`~feastruct.fea.cases.AnalysisCase`
"""
# get reaction force
reaction = self.get_reaction(analysis_case)
# dont plot small reaction
if abs(reaction) < 1e-6:
return
if self.dof in [0, 1]:
val = reaction / max_reaction
lf = abs(val) * 1.5 * small # arrow length
lh = 0.4 * small # arrow head length
wh = 0.4 * small # arrow head width
lf = max(lf, lh * 1.5)
offset = 0.5 * small
xoff = 0
yoff = 0
if self.dof == 0:
rot_mat = np.array([[-1, 0], [0, -1]]) * np.sign(val)
va = 'center'
if val > 0:
ha = 'right'
xoff = -offset / 2
else:
ha = 'left'
xoff = offset / 2
elif self.dof == 1:
rot_mat = np.array([[0, 1], [-1, 0]]) * np.sign(val)
ha = 'center'
if val > 0:
va = 'top'
yoff = -offset / 2
else:
va = 'bottom'
yoff = offset / 2
inward = True
ll = np.array([[offset, offset + lf], [0, 0]])
p0 = offset
p1 = p0 + lh
pp = np.array([[p1, p1, p0], [-wh / 2, wh / 2, 0]])
# correct end of arrow line
if inward:
ll[0, 0] += lh
else:
ll[0, 1] -= lh
rl = np.matmul(rot_mat, ll)
rp = np.matmul(rot_mat, pp)
rl[0, :] += self.node.x
rl[1, :] += self.node.y
rp[0, :] += self.node.x
rp[1, :] += self.node.y
s = 0
e = None
tl = np.array([rl[0, 1] + xoff, rl[1, 1] + yoff])
else:
(angle, num_el) = get_support_angle(self.node)
s = np.sin(angle * np.pi / 180)
c = np.cos(angle * np.pi / 180)
lh = 0.3 * small # arrow head length
wh = 0.3 * small # arrow head width
rr = 1.5 * small
ths = np.arange(100, 261)
rot_mat = np.array([[c, -s], [s, c]])
# make arrow tail around (0,0)
ll = np.array([rr * np.cos(ths * np.pi / 180),
rr * np.sin(ths * np.pi / 180)])
# make arrow head at (0,0)
pp = np.array([[-lh, -lh, 0], [-wh / 2, wh / 2, 0]])
# rotate arrow head around (0,0)
if reaction > 0:
thTip = 90 - ths[11]
xTip = ll[:, -1]
s = 0
e = -1
else:
thTip = ths[11] - 90
xTip = ll[:, 0]
s = 1
e = None
cTip = np.cos(thTip * np.pi / 180)
sTip = np.sin(thTip * np.pi / 180)
rTip = np.array([[cTip, -sTip], [sTip, cTip]])
pp = np.matmul(rTip, pp)
# shift arrow head to tip
pp[0, :] += xTip[0]
pp[1, :] += xTip[1]
# rotate arrow to align it with the node
rl = np.matmul(rot_mat, ll)
rp = np.matmul(rot_mat, pp)
rl[0, :] += self.node.x
rl[1, :] += self.node.y
rp[0, :] += self.node.x
rp[1, :] += self.node.y
ha = 'center'
va = 'center'
tl = np.array([rl[0, 0], rl[1, 0]])
ax.plot(rl[0, s:e], rl[1, s:e], linewidth=1.5, color='r')
ax.add_patch(Polygon(np.transpose(rp), facecolor='r'))
ax.text(tl[0], tl[1], "{:5.3g}".format(reaction), size=8, horizontalalignment=ha,
verticalalignment=va)
[docs] def plot_xysupport(self, ax, angle, roller, hinge, small, analysis_case, deformed, def_scale):
"""Plots a hinged or roller support.
:param ax: Axes object on which to plot
:type ax: :class:`matplotlib.axes.Axes`
:param float angle: Angle at which to plot the support
:param bool roller: Whether or not the support is a roller
:param bool hinge: Whether or not there is a hinge at the support
:param float small: A dimension used to scale the support
:param analysis_case: Analysis case
:type analysis_case: :class:`~feastruct.fea.cases.AnalysisCase`
:param bool deformed: Represents whether or not the node locations are deformed based on
the results of case id
:param float def_scale: Value used to scale deformations
"""
# determine coordinates of node
if deformed:
# get displacement of node for current analysis case
u = [0, 0]
u[0] = self.node.dofs[0].get_displacement(analysis_case)
u[1] = self.node.dofs[1].get_displacement(analysis_case)
x = self.node.x + u[0] * def_scale
y = self.node.y + u[1] * def_scale
else:
x = self.node.x
y = self.node.y
# determine coordinates of triangle
dx = small
h = np.sqrt(3) / 2
triangle = np.array([[-h, -h, -h, 0, -h], [-1, 1, 0.5, 0, -0.5]]) * dx
s = np.sin(angle * np.pi / 180)
c = np.cos(angle * np.pi / 180)
rot_mat = np.array([[c, -s], [s, c]])
rot_triangle = np.matmul(rot_mat, triangle)
if roller:
line = np.array([[-1.1, -1.1], [-1, 1]]) * dx
rot_line = np.matmul(rot_mat, line)
ax.plot(rot_line[0, :] + x, rot_line[1, :] + y, 'k-', linewidth=1)
else:
rect = np.array([[-1.4, -1.4, -h, -h], [-1, 1, 1, -1]]) * dx
rot_rect = np.matmul(rot_mat, rect)
rot_rect[0, :] += x
rot_rect[1, :] += y
ax.add_patch(Polygon(np.transpose(rot_rect), facecolor=(0.7, 0.7, 0.7)))
ax.plot(rot_triangle[0, :] + x, rot_triangle[1, :] + y, 'k-', linewidth=1)
if hinge:
ax.plot(x, y, 'ko', markerfacecolor='w', linewidth=1, markersize=4)
[docs]class NodalLoad(BoundaryCondition):
"""Class for a neumann boundary condition acting at a node.
Provides methods for the FEA solver and post-processing.
:cvar node: The node object at which the nodal load acts
:vartype node: :class:`~feastruct.fea.node.Node`
:cvar float val: The value of the nodal load
:cvar int dof: The degree of freedom about which the nodal load acts
"""
[docs] def __init__(self, node, val, dof):
"""inits the NodalLoad class.
:param node: The node object at which the nodal load acts
:type node: :class:`~feastruct.fea.node.Node`
:param float val: The value of the nodal load
:param int dof: The degree of freedom about which the nodal load acts
"""
super().__init__(node, val, dof)
[docs] def apply_load(self, f_ext):
"""Applies the nodal load.
The external force vector is modified to apply the neumann boundary condition.
:param f_ext: Global external force vector of size *N*
:type f_ext: :class:`numpy.ndarray`
"""
# get gdof number for the load
gdof = self.node.dofs[self.dof].global_dof_num
# add load to f_ext, selecting the correct dof from dofs
f_ext[gdof] += self.val
[docs] def plot_load(self, ax, max_force, small, get_support_angle, analysis_case, deformed,
def_scale):
"""Plots a graphical representation of a nodal force. A straight arrow is plotted for a
translational load and a curved arrow is plotted for a moment.
:param ax: Axes object on which to plot
:type ax: :class:`matplotlib.axes.Axes`
:param float max_force: Maximum translational nodal load
:param float small: A dimension used to scale the support
:param get_support_angle: A function that returns the support angle and the number of
connected elements
:type get_support_angle: :func:`feastruct.post.post.PostProcessor.get_support_angle`
:param analysis_case: Analysis case
:type analysis_case: :class:`~feastruct.fea.cases.AnalysisCase`
:param bool deformed: Represents whether or not the node locations are deformed based on
the results of case id
:param float def_scale: Value used to scale deformations
"""
# determine coordinates of node
if deformed:
u = [0, 0]
u[0] = self.node.dofs[0].get_displacement(analysis_case)
u[1] = self.node.dofs[1].get_displacement(analysis_case)
x = self.node.x + u[0] * def_scale
y = self.node.y + u[1] * def_scale
else:
x = self.node.x
y = self.node.y
if max_force == 0:
val = self.val
else:
val = self.val / max_force
offset = 0.5 * small
(angle, num_el) = get_support_angle(self.node)
s = np.sin(angle * np.pi / 180)
c = np.cos(angle * np.pi / 180)
# plot nodal force
if self.dof in [0, 1]:
lf = abs(val) * 1.5 * small # arrow length
lh = 0.6 * small # arrow head length
wh = 0.6 * small # arrow head width
lf = max(lf, lh * 1.5)
n = np.array([c, s])
inward = (n[self.dof] == 0 or np.sign(n[self.dof]) == np.sign(val))
to_rotate = (self.dof) * 90 + (n[self.dof] > 0) * 180
sr = np.sin(to_rotate * np.pi / 180)
cr = np.cos(to_rotate * np.pi / 180)
rot_mat = np.array([[cr, -sr], [sr, cr]])
ll = np.array([[offset, offset + lf], [0, 0]])
p0 = offset + (not inward) * lf
p1 = p0 + (inward) * lh - (not inward) * lh
pp = np.array([[p1, p1, p0], [-wh / 2, wh / 2, 0]])
# correct end of arrow line
if inward:
ll[0, 0] += lh
else:
ll[0, 1] -= lh
rl = np.matmul(rot_mat, ll)
rp = np.matmul(rot_mat, pp)
rp[0, :] += x
rp[1, :] += y
s = 0
e = None
# plot nodal moment
else:
lh = 0.4 * small # arrow head length
wh = 0.4 * small # arrow head width
rr = 1.5 * small
ths = np.arange(100, 261)
rot_mat = np.array([[c, -s], [s, c]])
# make arrow tail around (0,0)
ll = np.array([rr * np.cos(ths * np.pi / 180), rr * np.sin(ths * np.pi / 180)])
# make arrow head at (0,0)
pp = np.array([[-lh, -lh, 0], [-wh / 2, wh / 2, 0]])
# rotate arrow head around (0,0)
if val > 0:
thTip = 90 - ths[11]
xTip = ll[:, -1]
s = 0
e = -1
else:
thTip = ths[11] - 90
xTip = ll[:, 0]
s = 1
e = None
cTip = np.cos(thTip * np.pi / 180)
sTip = np.sin(thTip * np.pi / 180)
rTip = np.array([[cTip, -sTip], [sTip, cTip]])
pp = np.matmul(rTip, pp)
# shift arrow head to tip
pp[0, :] += xTip[0]
pp[1, :] += xTip[1]
# rotate arrow to align it with the node
rl = np.matmul(rot_mat, ll)
rp = np.matmul(rot_mat, pp)
rp[0, :] += x
rp[1, :] += y
ax.plot(rl[0, s:e] + x, rl[1, s:e] + y, 'k-', linewidth=2)
ax.add_patch(Polygon(np.transpose(rp), facecolor='k'))
class ElementLoad:
"""Parent class for a load applied to a finite element.
:cvar element: Finite element to which the load is applied
:vartype element: :class:`~feastruct.fea.fea.FiniteElement`
"""
def __init__(self, element):
"""Inits the ElementLoad class.
:param element: Finite element to which the load is applied
:type element: :class:`~feastruct.fea.fea.FiniteElement`
"""
self.element = element
def nodal_equivalent_loads(self):
"""Placehodler for the nodal_equivalent_loads method."""
pass
def apply_load(self, f_eq):
"""Placeholder for the apply_load method.
:param f_eq: Global equivalent nodal loads vector of size *N*
:type f_eq: :class:`numpy.ndarray
"""
pass
def plot_load(self):
"""Placeholder for the plot_load method."""
pass