"""
Loads module defining point, distributed, and varying loads for nodes and elements.
"""
import numpy as np
[docs]
class PointLoad:
"""
Represents a concentrated force/moment load acting on a node.
Attributes
----------
node : Node
The Node object to which the load is applied.
fx : float
Force along the global x-axis.
fy : float
Force along the global y-axis.
mz : float
Moment about the global z-axis.
"""
def __init__(self, node, fx=0.0, fy=0.0, mz=0.0):
"""
Initialize a PointLoad.
Parameters
----------
node : Node
The Node object to which the load is applied.
fx : float, optional
Force along the global x-axis. Defaults to 0.0.
fy : float, optional
Force along the global y-axis. Defaults to 0.0.
mz : float, optional
Moment about the global z-axis. Defaults to 0.0.
"""
self.node = node
self.fx, self.fy, self.mz = fx, fy, mz
[docs]
class ElementLoad:
"""
Base class for loads acting along the length of an element.
Attributes
----------
element : ElementBase
The element to which the load is applied.
"""
def __init__(self, element):
"""
Initialize an ElementLoad.
Parameters
----------
element : ElementBase
The element to which the load is applied.
"""
self.element = element
def _compute_equivalent_loads(self):
"""
Compute the equivalent local nodal forces and moments for this load type.
Raises
------
NotImplementedError
If not implemented by a subclass.
"""
raise NotImplementedError("Subclasses must implement this method")
def _transform_and_store_equivalent_loads(self, eq_local):
"""
Transform local equivalent nodal loads to global coordinates and
store them in the element.
Parameters
----------
eq_local : numpy.ndarray
6-component vector of equivalent loads in the element's local system.
"""
c = self.element.cos
s = self.element.sin
T = np.array(
[
[c, -s, 0, 0, 0, 0],
[s, c, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0],
[0, 0, 0, c, -s, 0],
[0, 0, 0, s, c, 0],
[0, 0, 0, 0, 0, 1],
]
)
eq_global = T @ eq_local
if not hasattr(self.element, "eq_load"):
self.element.eq_load = np.zeros(6)
self.element.eq_load += eq_global
[docs]
class DistributedLoad(ElementLoad):
"""
Represents a uniformly distributed load acting on an element.
The load is defined in the element's local coordinate system:
wx: force per unit length along local x (axial)
wy: force per unit length along local y (transverse)
"""
def __init__(self, element, wx=0.0, wy=0.0):
"""
Initialize a DistributedLoad.
Parameters
----------
element : ElementBase
The element to which the load is applied.
wx : float, optional
Axial force intensity per unit length. Defaults to 0.0.
wy : float, optional
Transverse force intensity per unit length. Defaults to 0.0.
"""
super().__init__(element)
self.wx = wx
self.wy = wy
self._compute_equivalent_loads()
def _compute_equivalent_loads(self):
"""Compute standard fixed-end forces for uniform load in local coordinates."""
L = self.element.length
#fixed end forces for uniform load in local coordinates
f0 = self.wx * L / 2
f1 = self.wy * L / 2
f2 = self.wy * L**2 / 12
f3 = self.wx * L / 2
f4 = self.wy * L / 2
f5 = -self.wy * L**2 / 12
eq_local = np.array(
[
f0, # Axial force at node i
f1, # Transverse force at node i
f2, # Moment at node i
f3, # Axial force at node j
f4, # Transverse force at node j
f5, # Moment at node j
]
)
hinge_i = getattr(self.element, "hinge_i", False)
hinge_j = getattr(self.element, "hinge_j", False)
if hinge_i or hinge_j:
if hinge_i and hinge_j:
eq_local[1] = f1 - (f2 + f5)/L # Adjust transverse force at node i
eq_local[2] = 0.0
eq_local[4] = f4 + (f2 + f5)/L # Adjust transverse force at node j
eq_local[5] = 0.0
elif hinge_i:
eq_local[1] = f1-f2*3/(2*L) # Adjust transverse force at node i
eq_local[2] = 0.0
eq_local[4] = f4 + f2*3/(2*L) # Adjust transverse force at node j
eq_local[5] = f5 - f2/2 # Adjust moment at node j
elif hinge_j:
eq_local[1] = f2-f5*3/(2*L) # Adjust transverse force at node i
eq_local[2] = f2-f5/2 # Adjust moment at node i
eq_local[4] = f4 + f5*3/(2*L)
eq_local[5] = 0.0
self._transform_and_store_equivalent_loads(eq_local)
[docs]
class ElementPointLoad(ElementLoad):
"""
Represents a point load acting on an element at a specific distance from its start node.
The load is defined in the element's local coordinate system:
px: force along local x (axial)
py: force along local y (transverse)
mz: moment around local z
x: distance from the start node of the element.
"""
def __init__(self, element, px=0.0, py=0.0, mz=0.0, x=0.0):
"""
Initialize an ElementPointLoad.
Parameters
----------
element : ElementBase
The element to which the load is applied.
px : float, optional
Local axial force intensity. Defaults to 0.0.
py : float, optional
Local transverse force intensity. Defaults to 0.0.
mz : float, optional
Local moment. Defaults to 0.0.
x : float, optional
Distance from the start node (node_i) along the element length. Defaults to 0.0.
Raises
------
ValueError
If the distance `x` is out of bounds (less than 0 or greater than element length).
"""
super().__init__(element)
self.px = px
self.py = py
self.mz = mz
if x > element.length or x < 0:
raise ValueError("Load position must be within the element length.")
self.x = x
self._compute_equivalent_loads()
def _compute_equivalent_loads(self):
"""Compute fixed-end forces for a point load in local coordinates."""
L = self.element.length
a = self.x
b = L - a
# Fixed-end forces for a point load in local coordinates
FAx = (self.px * b) / L
FAy = (self.py * b**2 * (3 * a + b)) / L**3
MAz = (self.py * a * b**2) / L**2
FBx = (self.px * a) / L
FBy = (self.py * a**2 * (a + 3 * b)) / L**3
MBz = -(self.py * a**2 * b) / L**2
# Add moment contributions at nodes
MAz += (self.mz * b) / L
MBz += (self.mz * a) / L
eq_local = np.array([FAx, FAy, MAz, FBx, FBy, MBz])
hinge_i = getattr(self.element, "hinge_i", False)
hinge_j = getattr(self.element, "hinge_j", False)
if hinge_i or hinge_j:
if hinge_i and hinge_j:
eq_local[1] = FAy - (MAz + MBz)/L # Adjust transverse force at node i
eq_local[2] = 0.0
eq_local[4] = FBy + (MAz + MBz)/L
eq_local[5] = 0.0
elif hinge_i:
eq_local[1] = FAy - MAz*3/(2*L) # Adjust transverse force at node i
eq_local[2] = 0.0
eq_local[4] = FBy + MAz*3/(2*L) # Adjust transverse force at node j
eq_local[5] = MBz - MAz/2 # Adjust moment at node j
elif hinge_j:
eq_local[1] = FBy - MBz*3/(2*L) # Adjust transverse force at node i
eq_local[2] = FBy - MBz/2 # Adjust moment at node i
eq_local[4] = FBy + MBz*3/(2*L)
eq_local[5] = 0.0
self._transform_and_store_equivalent_loads(eq_local)
[docs]
class TriangularLoad(ElementLoad):
"""
Represents a trapezoidally distributed load (UVL) on an element.
This can be used for triangular loads by setting one of w1 or w2 to zero.
The load is defined in the element's local coordinate system.
w1 and w2 are load intensities at the start and end of the element.
Can be axial (wx1, wx2) or transverse (wy1, wy2).
"""
def __init__(self, element, wx1=0.0, wx2=0.0, wy1=0.0, wy2=0.0):
"""
Initialize a TriangularLoad/TrapezoidalLoad.
Parameters
----------
element : ElementBase
The element to which the load is applied.
wx1 : float, optional
Axial force intensity at the start node. Defaults to 0.0.
wx2 : float, optional
Axial force intensity at the end node. Defaults to 0.0.
wy1 : float, optional
Transverse force intensity at the start node. Defaults to 0.0.
wy2 : float, optional
Transverse force intensity at the end node. Defaults to 0.0.
"""
super().__init__(element)
self.wx1 = wx1
self.wx2 = wx2
self.wy1 = wy1
self.wy2 = wy2
self._compute_equivalent_loads()
def _compute_equivalent_loads(self):
"""Compute equivalent local forces for trapezoidally distributed loads."""
L = self.element.length
eq_local = np.zeros(6)
# Axial forces (trapezoidal)
eq_local[0] += (L / 6) * (2 * self.wx1 + self.wx2)
eq_local[3] += (L / 6) * (self.wx1 + 2 * self.wx2)
# Transverse forces (trapezoidal), using superposition
# 1. Uniformly distributed load part (wy1)
eq_local[1] += self.wy1 * L / 2
eq_local[2] += self.wy1 * L**2 / 12
eq_local[4] += self.wy1 * L / 2
eq_local[5] -= self.wy1 * L**2 / 12
# 2. Uniformly varying load part (delta from wy1 to wy2)
wy_delta = self.wy2 - self.wy1
eq_local[1] += (3 * wy_delta * L) / 20
eq_local[2] += (wy_delta * L**2) / 30
eq_local[4] += (7 * wy_delta * L) / 20
eq_local[5] -= (wy_delta * L**2) / 20
self._transform_and_store_equivalent_loads(eq_local)