Source code for fem2d.loads

"""
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)