from .Nodes import Nodes
from .Node import Node
from .Submember import SubMember
import numpy as np
from math import sqrt
from typing import Any
from dataclasses import dataclass, field, asdict
[docs]
@dataclass(slots=True)
class Member():
"""
Represents a 3D space frame member connecting two nodes.
A member is a structural element that connects two nodes and can be discretized into
submembers for analysis. It supports various boundary conditions, material properties,
and loading conditions.
:ivar nodes: Reference to the Nodes collection object.
:vartype nodes: Nodes
:ivar node_i: Start node of the member.
:vartype node_i: Node
:ivar node_j: End node of the member.
:vartype node_j: Node
:ivar i_release: Boundary condition at node i (False = fixed, True = pinned).
:vartype i_release: bool
:ivar j_release: Boundary condition at node j (False = fixed, True = pinned).
:vartype j_release: bool
:ivar E: Young's modulus in ksi.
:vartype E: float
:ivar Ixx: Strong axis moment of inertia in in^4.
:vartype Ixx: float
:ivar Iyy: Weak axis moment of inertia in in^4.
:vartype Iyy: float
:ivar A: Cross-sectional area in in^2.
:vartype A: float
:ivar G: Shear modulus in ksi.
:vartype G: float
:ivar J: Polar moment of inertia in in^4.
:vartype J: float
:ivar mesh: Number of discretizations (submembers) along the member span.
:vartype mesh: int
:ivar bracing: Lateral bracing configuration. Can be 'continuous', 'quarter', 'third',
'midspan', or a list of bracing locations along the span.
:vartype bracing: str | list[float]
:ivar shape: Cross-sectional shape identifier.
:vartype shape: str
:ivar length: Calculated length of the member (in).
:vartype length: float
:ivar Cb: Lateral-torsional buckling coefficient.
:vartype Cb: float
:ivar count: Counter for submember creation.
:vartype count: int
:ivar submembers: Dictionary of submembers indexed by creation order.
:vartype submembers: dict[int, SubMember]
"""
nodes: Nodes
node_i: Node
node_j: Node
i_release: bool
j_release: bool
E: float
Ixx: float
Iyy: float
A: float
G: float
J: float
mesh: int
bracing: str | list[float]
shape: str
length: float = field(init=False)
Cb: float = field(init=False)
count: int = 0
submembers: dict[int, SubMember] = field(
default_factory=dict[int, SubMember])
def __post_init__(self) -> None:
"""Initialize the member after dataclass instantiation.
Calculates the member length and discretizes the member into submembers
based on the specified mesh parameter. Creates connectivity between
submembers using intermediate mesh nodes.
:returns: None
:rtype: None
"""
# Calculate the member length based on the node coordinates
self.length = self.calculate_length(self.node_i, self.node_j)
j: Node = self.node_i
for i, node in enumerate(
self.add_mesh(self.nodes, self.node_i,
self.node_j, self.mesh, self.length)
):
if i+1 == 1:
i = self.node_i
j = node
self.add_submember(
i,
j,
self.i_release,
False,
self.E,
self.Ixx,
self.Iyy,
self.A,
self.G,
self.J)
elif i+1 < self.mesh:
i = j
j = node
self.add_submember(
i,
j,
False,
False,
self.E,
self.Ixx,
self.Iyy,
self.A,
self.G,
self.J
)
else:
i = j
j = self.node_j
self.add_submember(
i,
j,
False,
self.j_release,
self.E,
self.Ixx,
self.Iyy,
self.A,
self.G,
self.J
)
[docs]
def calculate_length(self, node_i: Node, node_j: Node) -> float:
"""Calculate the length of the member using the Euclidean distance formula.
Computes the 3D distance between two nodes based on their coordinate
positions in the global reference frame.
:param node_i: Start node of the member
:type node_i: Node
:param node_j: End node of the member
:type node_j: Node
:returns: The length of the member in inches
:rtype: float
"""
# Calculate the x, y and z vector components of the member
dx = node_j.coordinates.x - node_i.coordinates.x
dy = node_j.coordinates.y - node_i.coordinates.y
dz = node_j.coordinates.z - node_i.coordinates.z
# Calculate and return the member length
return (sqrt(dx**2 + dy**2 + dz**2))
[docs]
def properties(self) -> dict[str, Any]:
"""Return all member properties as a dictionary.
Converts the dataclass instance into a dictionary representation containing
all field names and their current values.
:returns: Dictionary containing all member attributes and their values
:rtype: dict[str, Any]
"""
return asdict(self)
[docs]
def add_mesh(self, nodes: Nodes, node_i: Node, node_j: Node, mesh: int, l: float) -> list[Node]:
"""Create intermediate mesh nodes along the member span.
Generates evenly spaced nodes along the member length based on the mesh
parameter. These nodes are used as intermediary connection points for
submembers in the discretization process.
:param nodes: Collection of nodes in the model
:type nodes: Nodes
:param node_i: Start node of the member
:type node_i: Node
:param node_j: End node of the member
:type node_j: Node
:param mesh: Number of equal segments to divide the member into
:type mesh: int
:param l: Total length of the member in inches
:type l: float
:returns: List of intermediate mesh nodes along the member
:rtype: list[Node]
"""
# Instantiate an array to hold the mesh nodes
mesh_nodes: list[Node] = []
# Calculate the x, y and z vector components of the member
dx = node_j.coordinates.x - node_i.coordinates.x
dy = node_j.coordinates.y - node_i.coordinates.y
dz = node_j.coordinates.z - node_i.coordinates.z
# Calculate the member unit vectors
x_unit = dx/l
y_unit = dy/l
z_unit = dz/l
# Add the mesh to the model
for i in range(mesh):
# Calculate the scalar
scalar = l/mesh*(i+1)
# Calculate nodal coordinates of mesh point
x = node_i.coordinates.x + scalar*x_unit
y = node_i.coordinates.y + scalar*y_unit
z = node_i.coordinates.z + scalar*z_unit
# Add the mesh coordinates as a node to the model
mesh_nodes.append(nodes.add_node(x, y, z, mesh_node=True))
# Return a list of the mesh nodes
return (mesh_nodes)
[docs]
def add_submember(
self,
node_i: Node,
node_j: Node,
i_release: bool,
j_release: bool,
E: float,
Ixx: float,
Iyy: float,
A: float,
G: float,
J: float
) -> None:
"""Add a submember to the member collection.
Creates a new submember element connecting two nodes with specified
boundary conditions and material properties. The submember is stored
in the submembers dictionary using an auto-incremented counter.
:param node_i: Start node of the submember
:type node_i: Node
:param node_j: End node of the submember
:type node_j: Node
:param i_release: Release condition at start node (False = fixed, True = pinned)
:type i_release: bool
:param j_release: Release condition at end node (False = fixed, True = pinned)
:type j_release: bool
:param E: Young's modulus in ksi
:type E: float
:param Ixx: Strong axis moment of inertia in in^4
:type Ixx: float
:param Iyy: Weak axis moment of inertia in in^4
:type Iyy: float
:param A: Cross-sectional area in in^2
:type A: float
:param G: Shear modulus in ksi
:type G: float
:param J: Polar moment of inertia in in^4
:type J: float
:returns: None
:rtype: None
"""
self.count += 1
submbr = SubMember(
node_i,
node_j,
i_release,
j_release,
E,
Ixx,
Iyy,
A,
G,
J
)
self.submembers[self.count] = submbr
[docs]
def calculate_Cb(self) -> float:
"""
Calculate the lateral-torsional buckling coefficient (Cb) for the member.
Computes Cb based on moment variation along unbraced spans using the
standard AISC formula: Cb = 12.5*Mmax / (2.5*Mmax + 3*Ma + 4*Mb + 3*Mc).
The calculation accounts for lateral bracing locations and interpolates
moments at quarter-span points. The result is limited to a maximum of 3.0.
:returns: The lateral-torsional buckling coefficient, limited to 3.0 maximum.
:rtype: float
"""
# Instantiate an array to hold Cb for each unbraced span
Cb: list[float] = []
def discretize(length: float, n: int, l1: float = 0.0) -> list[float]:
"""Discretize span into n segments and return division points.
:param length: Length of span.
:type length: float
:param n: Number of segments.
:type n: int
:param l1: Offset location.
:type l1: float
:returns: List of discretized locations.
:rtype: list[float]
"""
# Returns an n-sized array of locations along the member span
locations: list[float] = []
for i in range(n-1):
i += 1
locations.append(l1 + i * length / n)
return (locations)
def braceCoordinates(bracePoint: float) -> tuple[float, float, float]:
"""Calculate the global coordinates of a brace point along the member.
:param bracePoint: Distance along member span.
:type bracePoint: float
:returns: Tuple of (x, y, z) coordinates.
:rtype: tuple[float, float, float]
"""
# Calculate the x, y and z vector components of the member
dx = self.node_j.coordinates.x - self.node_i.coordinates.x
dy = self.node_j.coordinates.y - self.node_i.coordinates.y
dz = self.node_j.coordinates.z - self.node_i.coordinates.z
# Calculate the member unit vectors
x_unit = dx/self.length
y_unit = dy/self.length
z_unit = dz/self.length
# Calculate nodal coordinates of brace point
x = self.node_i.coordinates.x + bracePoint*x_unit
y = self.node_i.coordinates.y + bracePoint*y_unit
z = self.node_i.coordinates.z + bracePoint*z_unit
# Return the brace point coordinates
return (x, y, z)
if self.bracing == 'quarter':
bracePoints = discretize(self.length, 4)
elif self.bracing == 'third':
bracePoints = discretize(self.length, 3)
elif self.bracing == 'midspan':
bracePoints = discretize(self.length, 2)
else:
if type(self.bracing) == list:
pass
else:
raise ValueError(
"Must be 'quarter','third','midspan' or an array of locations"
)
bracePoints = self.bracing
unbracedSpans = len(bracePoints) + 1
for i in range(unbracedSpans):
# Determine unbraced span length
if i == 0:
l1 = 0
unbracedLength = bracePoints[i]
elif i == len(bracePoints):
l1 = bracePoints[i-1]
unbracedLength = self.length - l1
else:
l1 = bracePoints[i-1]
unbracedLength = bracePoints[i] - l1
# Determine the 1/4 point locations along the unbraced span
quarterPoints = discretize(unbracedLength, 4, l1)
# Instantiate an empty array to hold Mmax
moments: list[float] = []
# Instantiate an empty array to hold Ma, Mb, and Mc
M: list[float] = []
# Determine the nodal coordinates of the quarter points
for quarterPoint in quarterPoints:
coordinates = braceCoordinates(quarterPoint)
for submbr in self.submembers.values():
# 1/4 point coordinates
x = coordinates[0]
y = coordinates[1]
z = coordinates[2]
# Node i coordinates
xi = submbr.node_i.coordinates.x
yi = submbr.node_i.coordinates.y
zi = submbr.node_i.coordinates.z
# Node j coordinates
xj = submbr.node_j.coordinates.x
yj = submbr.node_j.coordinates.y
zj = submbr.node_j.coordinates.z
# Determine which submembers the 1/4 point falls between
if (xi, yi, zi) <= coordinates <= (xj, yj, zj):
# Location values for linear interpolation
Lp: list[float] = []
Lp.append(0.0)
Lp.append(submbr.length)
# Moment values for linear interpolation
Mp: list[float] = []
Mp.append(submbr.results['major axis moments'][0])
Mp.append(-1*submbr.results['major axis moments'][1])
# Location for moment interpolation
dx = x - xi
dy = y - yi
dz = z - zi
L = sqrt(dx**2 + dy**2 + dz**2)
# Interpolate the quarter point moment value
M.append(abs(np.interp(L, Lp, Mp)))
moments.append(max(
abs(submbr.results['major axis moments'][0]),
abs(submbr.results['major axis moments'][1])
))
else:
moments.append(max(
abs(submbr.results['major axis moments'][0]),
abs(submbr.results['major axis moments'][1])
))
# Calculate Cb
Mmax = max(moments)
Ma = M[0]
Mb = M[1]
Mc = M[2]
Cb.append(12.5*Mmax/(2.5*Mmax + 3*Ma + 4*Mb + 3*Mc))
self.Cb = min(min(Cb), 3)
return self.Cb
[docs]
def add_point_load(self, mag: float, direction: str, location: float) -> None:
"""
Apply a concentrated point load to the member.
Applies a point load at a specified location along the member span.
The load can be specified in either global (X, Y, Z) or local (x, y, z)
coordinates. The method calculates equivalent nodal actions and distributes
loads to the appropriate nodes and submembers.
:param mag: Magnitude of the load in kips.
:type mag: float
:param direction: Load direction - global ('X', 'Y', 'Z') or local ('x', 'y', 'z').
:type direction: str
:param location: Load location as percentage of member span (0-100%).
:type location: float
:returns: None
:rtype: None
:raises ValueError: If direction is not one of 'X', 'Y', 'Z', 'x', 'y', 'z'.
"""
# Convert location from percentage to absolute distance
location = self.length*(location/100)
# Instantiate a variable measuring distance along the member
l1 = 0
# Acceptable floating-point error tolerance to consider load at a node
pointError = 1*10**-10
# Iterate through the submembers
for _, submbr in self.submembers.items():
l2 = l1 + submbr.length
# Check if the load lands on the current submember
if l1 <= location <= l2:
# Extract rotation matrix for the current submember
transformation_matrix = submbr.rotation_matrix[0:3, 0:3]
# Initialize a global force vector
if direction == 'X':
fg = np.array([mag, 0, 0])
elif direction == 'x':
fg = np.array(
np.matmul(transformation_matrix, np.array([mag, 0, 0])))
elif direction == 'Y':
fg = np.array([0, mag, 0])
elif direction == 'y':
fg = np.array(
np.matmul(transformation_matrix, np.array([0, mag, 0])))
elif direction == 'Z':
fg = np.array([0, 0, mag])
elif direction == 'z':
fg = np.array(
np.matmul(transformation_matrix, np.array([0, 0, mag])))
else:
raise ValueError(
"Load direction must be global ('X', 'Y', 'Z') or local ('x', 'y', 'z')."
)
# Check if the load lands on node i of the submember
if l1-pointError < location < l1+pointError:
# Add the X component of the load
submbr.node_i.add_load(mag=fg[0], lType='v', direction='X')
# Add the Y component of the load
submbr.node_i.add_load(mag=fg[1], lType='v', direction='Y')
# Add the Z component of the load
submbr.node_i.add_load(mag=fg[2], lType='v', direction='Z')
# Check if the load lands on node j of the submember
elif l2-pointError < location < l2+pointError:
# Add the X component of the load
submbr.node_j.add_load(mag=fg[0], lType='v', direction='X')
# Add the Y component of the load
submbr.node_j.add_load(mag=fg[1], lType='v', direction='Y')
# Add the Z component of the load
submbr.node_j.add_load(mag=fg[2], lType='v', direction='Z')
# Load lands somewhere between the nodes of the submember
else:
# P
# o-------|-----o
# |<- a ->|<-b->|
b = l2 - location
a = submbr.length - b
# Transform the global force vector to local coordinates
FL = np.matmul(transformation_matrix, fg)
# Extract local axial force
axial = FL[0]
# Extract local shearing force
v = FL[1]
# Extract local transverse force
t = FL[2]
# Calculate equivalent nodal actions
if submbr.i_release == True and submbr.j_release == False:
# instantiate an array to hold equivalent nodal actions
f_local = np.zeros([10, 1])
f_local[0, 0] = axial*b/(submbr.length)
f_local[1, 0] = v*b**2 * \
(a+2*submbr.length)/(2*submbr.length**3)
f_local[2, 0] = t*b**2 * \
(a+2*submbr.length)/(2*submbr.length**3)
f_local[4, 0] = axial*a/(submbr.length)
f_local[5, 0] = v*a * \
(3*submbr.length**2-a**2)/(2*submbr.length**3)
f_local[6, 0] = t*a * \
(3*submbr.length**2-a**2)/(2*submbr.length**3)
f_local[8, 0] = t*a*b * \
(a+submbr.length)/(2*submbr.length**2)
f_local[9, 0] = v*a*b * \
(a+submbr.length)/(2*submbr.length**2)
elif submbr.i_release == False and submbr.j_release == True:
# Instantiate an array to hold equivalent nodal actions
f_local = np.zeros([10, 1])
f_local[0, 0] = axial*b/(submbr.length)
f_local[1, 0] = v*b * \
(3*submbr.length**2-b**2)/(2*submbr.length**3)
f_local[2, 0] = t*b * \
(3*submbr.length**2-b**2)/(2*submbr.length**3)
f_local[4, 0] = -t*b*a * \
(b+submbr.length)/(2*submbr.length**2)
f_local[5, 0] = -v*b*a * \
(b+submbr.length)/(2*submbr.length**2)
f_local[6, 0] = axial*a/(submbr.length)
f_local[7, 0] = v*a**2 * \
(b+2*submbr.length)/(2*submbr.length**3)
f_local[8, 0] = t*a**2 * \
(b+2*submbr.length)/(2*submbr.length**3)
else:
# Instantiate an array to hold equivalent nodal actions
f_local = np.zeros([12, 1])
# Forces at node i
f_local[0, 0] = axial*b/(submbr.length)
f_local[1, 0] = v*b**2*(3*a+b)/submbr.length**3
f_local[2, 0] = t*b**2*(3*a+b)/submbr.length**3
f_local[4, 0] = -t*a*b**2/submbr.length**2
f_local[5, 0] = -v*a*b**2/submbr.length**2
# Forces at node j
f_local[6, 0] = axial*a/(submbr.length)
f_local[7, 0] = v*a**2*(a+3*b)/submbr.length**3
f_local[8, 0] = t*a**2*(a+3*b)/submbr.length**3
f_local[10, 0] = t*a**2*b/submbr.length**2
f_local[11, 0] = v*a**2*b/submbr.length**2
# Transform the local force vector to the global reference plane
transformation_matrix = np.matrix(submbr.rotation_matrix)
f_global = transformation_matrix.I*f_local
# Add the equivalent nodal forces and moments to each node
if submbr.i_release == True and submbr.j_release == False:
submbr.node_i.Fx += f_global[0, 0]
submbr.node_i.Fy += f_global[1, 0]
submbr.node_i.Fz += f_global[2, 0]
submbr.node_i.Mx += f_global[3, 0]
submbr.node_j.Fx += f_global[4, 0]
submbr.node_j.Fy += f_global[5, 0]
submbr.node_j.Fz += f_global[6, 0]
submbr.node_j.Mx += f_global[7, 0]
submbr.node_j.My += f_global[8, 0]
submbr.node_j.Mz += f_global[9, 0]
submbr.node_i.eFx += f_global[0, 0]
submbr.node_i.eFy += f_global[1, 0]
submbr.node_i.eFz += f_global[2, 0]
submbr.node_i.eMx += f_global[3, 0]
submbr.node_j.eFx += f_global[4, 0]
submbr.node_j.eFy += f_global[5, 0]
submbr.node_j.eFz += f_global[6, 0]
submbr.node_j.eMx += f_global[7, 0]
submbr.node_j.eMy += f_global[8, 0]
submbr.node_j.eMz += f_global[9, 0]
submbr.ENAs['axial'][0] += f_local[0, 0]
submbr.ENAs['axial'][1] += f_local[4, 0]
submbr.ENAs['shear'][0] += f_local[1, 0]
submbr.ENAs['shear'][1] += f_local[5, 0]
submbr.ENAs['transverse shear'][0] += f_local[2, 0]
submbr.ENAs['transverse shear'][1] += f_local[6, 0]
submbr.ENAs['minor axis moments'][0] += f_local[8, 0]
submbr.ENAs['major axis moments'][1] += f_local[9, 0]
elif submbr.i_release == False and submbr.j_release == True:
submbr.node_i.Fx += f_global[0, 0]
submbr.node_i.Fy += f_global[1, 0]
submbr.node_i.Fz += f_global[2, 0]
submbr.node_i.Mx += f_global[3, 0]
submbr.node_i.My += f_global[4, 0]
submbr.node_i.Mz += f_global[5, 0]
submbr.node_j.Fx += f_global[6, 0]
submbr.node_j.Fy += f_global[7, 0]
submbr.node_j.Fz += f_global[8, 0]
submbr.node_j.Mx += f_global[9, 0]
submbr.node_i.eFx += f_global[0, 0]
submbr.node_i.eFy += f_global[1, 0]
submbr.node_i.eFz += f_global[2, 0]
submbr.node_i.eMx += f_global[3, 0]
submbr.node_i.eMy += f_global[4, 0]
submbr.node_i.eMz += f_global[5, 0]
submbr.node_j.eFx += f_global[6, 0]
submbr.node_j.eFy += f_global[7, 0]
submbr.node_j.eFz += f_global[8, 0]
submbr.node_j.eMx += f_global[9, 0]
submbr.ENAs['axial'][0] += f_local[0, 0]
submbr.ENAs['axial'][1] += f_local[6, 0]
submbr.ENAs['shear'][0] += f_local[1, 0]
submbr.ENAs['shear'][1] += f_local[7, 0]
submbr.ENAs['transverse shear'][0] += f_local[2, 0]
submbr.ENAs['transverse shear'][1] += f_local[8, 0]
submbr.ENAs['minor axis moments'][0] += f_local[4, 0]
submbr.ENAs['major axis moments'][1] += f_local[5, 0]
else:
submbr.node_i.Fx += f_global[0, 0]
submbr.node_i.Fy += f_global[1, 0]
submbr.node_i.Fz += f_global[2, 0]
submbr.node_i.Mx += f_global[3, 0]
submbr.node_i.My += f_global[4, 0]
submbr.node_i.Mz += f_global[5, 0]
submbr.node_j.Fx += f_global[6, 0]
submbr.node_j.Fy += f_global[7, 0]
submbr.node_j.Fz += f_global[8, 0]
submbr.node_j.Mx += f_global[9, 0]
submbr.node_j.My += f_global[10, 0]
submbr.node_j.Mz += f_global[11, 0]
submbr.node_i.eFx += f_global[0, 0]
submbr.node_i.eFy += f_global[1, 0]
submbr.node_i.eFz += f_global[2, 0]
submbr.node_i.eMx += f_global[3, 0]
submbr.node_i.eMy += f_global[4, 0]
submbr.node_i.eMz += f_global[5, 0]
submbr.node_j.eFx += f_global[6, 0]
submbr.node_j.eFy += f_global[7, 0]
submbr.node_j.eFz += f_global[8, 0]
submbr.node_j.eMx += f_global[9, 0]
submbr.node_j.eMy += f_global[10, 0]
submbr.node_j.eMz += f_global[11, 0]
submbr.ENAs['axial'][0] += f_local[0, 0]
submbr.ENAs['axial'][1] += f_local[6, 0]
submbr.ENAs['shear'][0] += f_local[1, 0]
submbr.ENAs['shear'][1] += f_local[7, 0]
submbr.ENAs['transverse shear'][0] += f_local[2, 0]
submbr.ENAs['transverse shear'][1] += f_local[8, 0]
submbr.ENAs['minor axis moments'][0] += f_local[4, 0]
submbr.ENAs['minor axis moments'][1] += f_local[10, 0]
submbr.ENAs['major axis moments'][0] += f_local[5, 0]
submbr.ENAs['major axis moments'][1] += f_local[11, 0]
break
l1 = l2
[docs]
def add_distributed_load(self, Mag1: float, Mag2: float, direction: str, loc1: float, loc2: float):
"""
Apply a trapezoidal distributed load along the member.
Applies a distributed load with linearly varying magnitude over a specified
portion of the member span. Handles loading that spans across multiple
submembers by calculating equivalent nodal actions for each affected segment.
:param Mag1: Starting magnitude of the distributed load in kips.
:type Mag1: float
:param Mag2: Ending magnitude of the distributed load in kips.
:type Mag2: float
:param direction: Load direction - global ('X', 'Y', 'Z') or local ('x', 'y', 'z').
:type direction: str
:param loc1: Starting location of load along member span as percentage (0-100%).
:type loc1: float
:param loc2: Ending location of load along member span as percentage (0-100%).
:type loc2: float
:returns: None
:rtype: None
:raises ValueError: If direction is not one of 'X', 'Y', 'Z', 'x', 'y', 'z'.
"""
# Convert the start and end locations to absolute distance
loc1 = self.length*(loc1/100)
loc2 = self.length*(loc2/100)
# Calculate the slope of the trapezoidal load
m = (Mag2-Mag1)/(loc2-loc1)
# Instantiate a variable to measure distance along the member
l1 = 0
# Iterate through the member's submembers
for _, submbr in self.submembers.items():
# Calculate the end location of the current submember
l2 = l1+submbr.length
# Extract rotation matrix for the current submember
transformation_matrix = submbr.rotation_matrix[0:3, 0:3]
if l2 < loc1 or l1 > loc2:
# The load does not land on the current submember
# Continue to the next iteration
l1 = l2
continue
elif l1 <= loc1 and l2 >= loc2:
# The load is located entirely on the current submember
w1 = Mag1
w2 = Mag2
l = submbr.length
a = loc1 - l1
lw = loc2 - loc1
b = l2 - loc2
elif l1 <= loc1 and l2 <= loc2:
# The load begins on the current submember
w1 = Mag1
w2 = m*(l2-loc1)+Mag1
l = submbr.length
a = loc1 - l1
lw = l2 - loc1
b = 0
elif l1 >= loc1 and l2 >= loc2:
# The load ends on the current submember
w1 = m*(l1-loc1)+Mag1
w2 = Mag2
l = submbr.length
a = 0
lw = loc2 - l1
b = l2 - loc2
else:
# l1 >= loc1 and l2 <= loc2:
# Load continues over the current submember
w1 = m*(l1-loc1)+Mag1
w2 = m*(l2-loc1)+Mag1
l = submbr.length
a = 0
lw = l2 - l1
b = 0
# Initialize a global force vector
if direction == 'X':
fg1 = np.array([w1, 0, 0])
fg2 = np.array([w2, 0, 0])
elif direction == 'x':
fg1 = np.matmul(transformation_matrix, np.array([w1, 0, 0]))
fg2 = np.matmul(transformation_matrix, np.array([w2, 0, 0]))
elif direction == 'Y':
fg1 = np.array([0, w1, 0])
fg2 = np.array([0, w2, 0])
elif direction == 'y':
fg1 = np.matmul(transformation_matrix, np.array([0, w1, 0]))
fg2 = np.matmul(transformation_matrix, np.array([0, w2, 0]))
elif direction == 'Z':
fg1 = np.array([0, 0, w1])
fg2 = np.array([0, 0, w2])
elif direction == 'z':
fg1 = np.matmul(transformation_matrix, np.array([0, 0, w1]))
fg2 = np.matmul(transformation_matrix, np.array([0, 0, w2]))
else:
raise ValueError(
"Force vector must be bound to a global or local axis."
)
# Transform the global force vector to local coordinates
FL1 = np.matmul(transformation_matrix, fg1)
FL2 = np.matmul(transformation_matrix, fg2)
# Extract local axial force
a1 = FL1[0]
a2 = FL2[0]
# Extract local shearing force
v1 = FL1[1]
v2 = FL2[1]
vd = v2 - v1
vm = (v1+v2)/2
# Extract local transverse force
t1 = FL1[2]
t2 = FL2[2]
td = t2 - t1
tm = (t1+t2)/2
if submbr.i_release == False and submbr.j_release == False:
# Instantiate a local force vector
f_local = np.zeros([12, 1])
# Calculate the geometric constants
s1 = 10*((l**2+a**2)*(l+a)-(a**2+b**2)*(a-b)-l*b*(l+b)-a**3)
s2 = lw*(l*(2*l+a+b)-3*(a-b)**2-2*a*b)
s3 = 120*a*b*(a+lw)+10*lw*(6*a**2+4*l*lw-3*lw**2)
s4 = 10*l*lw**2-10*lw*a*(l-3*b)-9*lw**3
# forces at node j
f_local[6, 0] = (a1+a2)*submbr.length/2 # axial
f_local[7, 0] = (lw*(s1*vm+s2*vd))/(20*l**3) # normal shear
f_local[8, 0] = (lw*(s1*tm+s2*td)) / \
(20*l**3) # transverse shear
f_local[10, 0] = -(lw*(s3*tm+s4*td)) / \
(120*l**2) # minor axis moment
f_local[11, 0] = -(lw*(s3*vm+s4*vd)) / \
(120*l**2) # major axis moment
# Forces at node i
vj = f_local[7, 0] # Normal shear force at node j
tj = f_local[8, 0] # Transverse shear force at node j
mj = f_local[10, 0] # Minor axis moment at node j
Mj = f_local[11, 0] # Major axis moment at node j
f_local[0, 0] = (a1+a2)*submbr.length/2 # axial
f_local[1, 0] = lw*vm-vj # normal shear
f_local[2, 0] = lw*tm-tj # transverse shear
f_local[4, 0] = mj+tj*l-a*lw*tm - \
(lw**2*(2*t2+t1))/6 # minor axis moment
f_local[5, 0] = Mj+vj*l-a*lw*vm - \
(lw**2*(2*v2+v1))/6 # major axis moment
elif submbr.i_release == True and submbr.j_release == False:
# Instantiate a local force vector
f_local = np.zeros([12, 1])
s1 = 40*l*(2*l**2-lw**2)+10*lw * \
(lw**2-2*b**2)-40*b*(l-a)*(2*l+a)
s2 = lw*(3*lw**2-10*l*(lw+2*b)+10*b*(b+lw))
# Forces at node i
f_local[0, 0] = (a1+a2)*submbr.length/2 # axial
f_local[1, 0] = (lw*(s1*vm+s2*vd))/(80*l**3) # normal shear
f_local[2, 0] = (lw*(s1*tm+s2*td)) / \
(80*l**3) # transverse shear
f_local[4, 0] = 0 # minor axis moment
f_local[5, 0] = 0 # major axis moment
# forces at node j
f_local[6, 0] = (a1+a2)*submbr.length/2 # axial
f_local[7, 0] = lw*vm-f_local[1, 0] # normal shear
f_local[8, 0] = lw*vm-f_local[2, 0] # transverse shear
f_local[10, 0] = f_local[8, 0]*l-b*lw*tm - \
(lw**2*(2*t2+t1))/6 # minor axis moment
f_local[11, 0] = f_local[7, 0]*l-b*lw*vm - \
(lw**2*(2*v2+v1))/6 # major axis moment
else:
# Instantiate a local force vector
# (submbr.i_release == False and submbr.j_release == True)
f_local = np.zeros([12, 1])
s1 = 40*l*(2*l**2-lw**2)+10*lw * \
(lw**2-2*a**2)-40*a*(l-b)*(2*l+b)
s2 = lw*(3*lw**2-10*l*(lw+2*a)+10*a*(a+lw))
# Forces at node i
f_local[0, 0] = (a1+a2)*submbr.length/2 # axial
f_local[1, 0] = (lw*(s1*vm+s2*vd))/(80*l**3) # normal shear
f_local[2, 0] = (lw*(s1*tm+s2*td)) / \
(80*l**3) # transverse shear
f_local[4, 0] = f_local[2, 0]*l-a*lw*tm - \
(lw**2*(2*t2+t1))/6 # minor axis moment
f_local[5, 0] = f_local[1, 0]*l-a*lw*vm - \
(lw**2*(2*v2+v1))/6 # major axis moment
# Forces at node j
f_local[6, 0] = (a1+a2)*submbr.length/2 # Axial
f_local[7, 0] = lw*vm-f_local[1, 0] # Normal shear
f_local[8, 0] = lw*vm-f_local[2, 0] # Transverse shear
f_local[10, 0] = 0 # Minor axis moment
f_local[11, 0] = 0 # Major axis moment
# Transform the local force vector to the global reference plane
transformation_matrix = np.matrix(submbr.rotation_matrix)
f_global = transformation_matrix.I*f_local
# Add the equivalent nodal forces and moments to each node
if submbr.i_release == True and submbr.j_release == False:
submbr.node_i.Fx += f_global[0, 0]
submbr.node_i.Fy += f_global[1, 0]
submbr.node_i.Fz += f_global[2, 0]
submbr.node_i.Mx += f_global[3, 0]
submbr.node_j.Fx += f_global[4, 0]
submbr.node_j.Fy += f_global[5, 0]
submbr.node_j.Fz += f_global[6, 0]
submbr.node_j.Mx += f_global[7, 0]
submbr.node_j.My += f_global[8, 0]
submbr.node_j.Mz += f_global[9, 0]
submbr.node_i.eFx += f_global[0, 0]
submbr.node_i.eFy += f_global[1, 0]
submbr.node_i.eFz += f_global[2, 0]
submbr.node_i.eMx += f_global[3, 0]
submbr.node_j.eFx += f_global[4, 0]
submbr.node_j.eFy += f_global[5, 0]
submbr.node_j.eFz += f_global[6, 0]
submbr.node_j.eMx += f_global[7, 0]
submbr.node_j.eMy += f_global[8, 0]
submbr.node_j.eMz += f_global[9, 0]
submbr.ENAs['axial'][0] += f_local[0, 0]
submbr.ENAs['axial'][1] += f_local[4, 0]
submbr.ENAs['shear'][0] += f_local[1, 0]
submbr.ENAs['shear'][1] += f_local[5, 0]
submbr.ENAs['transverse shear'][0] += f_local[2, 0]
submbr.ENAs['transverse shear'][1] += f_local[6, 0]
submbr.ENAs['minor axis moments'][0] += f_local[8, 0]
submbr.ENAs['major axis moments'][1] += f_local[9, 0]
elif submbr.i_release == False and submbr.j_release == True:
submbr.node_i.Fx += f_global[0, 0]
submbr.node_i.Fy += f_global[1, 0]
submbr.node_i.Fz += f_global[2, 0]
submbr.node_i.Mx += f_global[3, 0]
submbr.node_i.My += f_global[4, 0]
submbr.node_i.Mz += f_global[5, 0]
submbr.node_j.Fx += f_global[6, 0]
submbr.node_j.Fy += f_global[7, 0]
submbr.node_j.Fz += f_global[8, 0]
submbr.node_j.Mx += f_global[9, 0]
submbr.node_i.eFx += f_global[0, 0]
submbr.node_i.eFy += f_global[1, 0]
submbr.node_i.eFz += f_global[2, 0]
submbr.node_i.eMx += f_global[3, 0]
submbr.node_i.eMy += f_global[4, 0]
submbr.node_i.eMz += f_global[5, 0]
submbr.node_j.eFx += f_global[6, 0]
submbr.node_j.eFy += f_global[7, 0]
submbr.node_j.eFz += f_global[8, 0]
submbr.node_j.eMx += f_global[9, 0]
submbr.ENAs['axial'][0] += f_local[0, 0]
submbr.ENAs['axial'][1] += f_local[6, 0]
submbr.ENAs['shear'][0] += f_local[1, 0]
submbr.ENAs['shear'][1] += f_local[7, 0]
submbr.ENAs['transverse shear'][0] += f_local[2, 0]
submbr.ENAs['transverse shear'][1] += f_local[8, 0]
submbr.ENAs['minor axis moments'][0] += f_local[4, 0]
submbr.ENAs['major axis moments'][1] += f_local[5, 0]
else:
submbr.node_i.Fx += f_global[0, 0]
submbr.node_i.Fy += f_global[1, 0]
submbr.node_i.Fz += f_global[2, 0]
submbr.node_i.Mx += f_global[3, 0]
submbr.node_i.My += f_global[4, 0]
submbr.node_i.Mz += f_global[5, 0]
submbr.node_j.Fx += f_global[6, 0]
submbr.node_j.Fy += f_global[7, 0]
submbr.node_j.Fz += f_global[8, 0]
submbr.node_j.Mx += f_global[9, 0]
submbr.node_j.My += f_global[10, 0]
submbr.node_j.Mz += f_global[11, 0]
submbr.node_i.eFx += f_global[0, 0]
submbr.node_i.eFy += f_global[1, 0]
submbr.node_i.eFz += f_global[2, 0]
submbr.node_i.eMx += f_global[3, 0]
submbr.node_i.eMy += f_global[4, 0]
submbr.node_i.eMz += f_global[5, 0]
submbr.node_j.eFx += f_global[6, 0]
submbr.node_j.eFy += f_global[7, 0]
submbr.node_j.eFz += f_global[8, 0]
submbr.node_j.eMx += f_global[9, 0]
submbr.node_j.eMy += f_global[10, 0]
submbr.node_j.eMz += f_global[11, 0]
submbr.ENAs['axial'][0] += f_local[0, 0]
submbr.ENAs['axial'][1] += f_local[6, 0]
submbr.ENAs['shear'][0] += f_local[1, 0]
submbr.ENAs['shear'][1] += f_local[7, 0]
submbr.ENAs['transverse shear'][0] += f_local[2, 0]
submbr.ENAs['transverse shear'][1] += f_local[8, 0]
submbr.ENAs['minor axis moments'][0] += f_local[4, 0]
submbr.ENAs['minor axis moments'][1] += f_local[10, 0]
submbr.ENAs['major axis moments'][0] += f_local[5, 0]
submbr.ENAs['major axis moments'][1] += f_local[11, 0]
l1 = l2
[docs]
def second_order(self) -> None:
"""
Apply second-order (P-delta) effects to account for geometric nonlinearity.
Constructs and adds geometric stiffness matrices to all submembers to account
for second-order effects including P-delta moments. This modifies the local
stiffness matrix of each submember by including the effects of axial forces
on bending behavior.
:returns: None
:rtype: None
"""
for submbr in self.submembers.values():
# Calculate the submember local geometric stiffness matrix
KG = submbr.build_geometric_stiffness_matrix()
# Redefine the submember local stiffness matrix
submbr.Kl += KG
# Calculate the submember global geometric stiffness matrix
submbr.KG = submbr.transformation_matrix.T.dot(
submbr.Kl).dot(submbr.transformation_matrix)