use std::f64::consts::PI;
pub const TRUSS2D_DOF_PER_NODE: usize = 2;
pub const TRUSS3D_DOF_PER_NODE: usize = 3;
pub const FRAME2D_DOF_PER_NODE: usize = 3;
pub const FRAME3D_DOF_PER_NODE: usize = 6;
pub const TRUSS2D_NDOF: usize = 4;
pub const TRUSS3D_NDOF: usize = 6;
pub const FRAME2D_NDOF: usize = 6;
pub const FRAME3D_NDOF: usize = 12;
pub const MIN_LENGTH: f64 = 1.0e-14;
#[derive(Debug, Clone, Copy)]
pub struct TrussElement2D {
pub elastic_modulus: f64,
pub area: f64,
pub node_i: [f64; 2],
pub node_j: [f64; 2],
}
impl TrussElement2D {
pub fn length(&self) -> f64 {
let dx = self.node_j[0] - self.node_i[0];
let dy = self.node_j[1] - self.node_i[1];
(dx * dx + dy * dy).sqrt()
}
pub fn direction_cosines(&self) -> [f64; 2] {
let l = self.length().max(MIN_LENGTH);
let dx = self.node_j[0] - self.node_i[0];
let dy = self.node_j[1] - self.node_i[1];
[dx / l, dy / l]
}
pub fn local_stiffness(&self) -> [[f64; 2]; 2] {
let k = self.elastic_modulus * self.area / self.length().max(MIN_LENGTH);
[[k, -k], [-k, k]]
}
pub fn global_stiffness(&self) -> [[f64; 4]; 4] {
let [l, m] = self.direction_cosines();
let k = self.elastic_modulus * self.area / self.length().max(MIN_LENGTH);
let l2 = l * l;
let m2 = m * m;
let lm = l * m;
[
[k * l2, k * lm, -k * l2, -k * lm],
[k * lm, k * m2, -k * lm, -k * m2],
[-k * l2, -k * lm, k * l2, k * lm],
[-k * lm, -k * m2, k * lm, k * m2],
]
}
pub fn axial_force(&self, disp: [f64; 4]) -> f64 {
let [l, m] = self.direction_cosines();
let ea_over_l = self.elastic_modulus * self.area / self.length().max(MIN_LENGTH);
let du = (disp[2] - disp[0]) * l + (disp[3] - disp[1]) * m;
ea_over_l * du
}
}
#[derive(Debug, Clone, Copy)]
pub struct TrussElement3D {
pub elastic_modulus: f64,
pub area: f64,
pub node_i: [f64; 3],
pub node_j: [f64; 3],
}
impl TrussElement3D {
pub fn length(&self) -> f64 {
let dx = self.node_j[0] - self.node_i[0];
let dy = self.node_j[1] - self.node_i[1];
let dz = self.node_j[2] - self.node_i[2];
(dx * dx + dy * dy + dz * dz).sqrt()
}
pub fn direction_cosines(&self) -> [f64; 3] {
let len = self.length().max(MIN_LENGTH);
[
(self.node_j[0] - self.node_i[0]) / len,
(self.node_j[1] - self.node_i[1]) / len,
(self.node_j[2] - self.node_i[2]) / len,
]
}
pub fn global_stiffness(&self) -> [[f64; 6]; 6] {
let [l, m, n] = self.direction_cosines();
let k = self.elastic_modulus * self.area / self.length().max(MIN_LENGTH);
let mut kg = [[0.0_f64; 6]; 6];
let dc = [l, m, n];
for a in 0..3 {
for b in 0..3 {
let val = k * dc[a] * dc[b];
kg[a][b] = val;
kg[a][b + 3] = -val;
kg[a + 3][b] = -val;
kg[a + 3][b + 3] = val;
}
}
kg
}
pub fn axial_force(&self, disp: [f64; 6]) -> f64 {
let [l, m, n] = self.direction_cosines();
let ea_over_len = self.elastic_modulus * self.area / self.length().max(MIN_LENGTH);
let du = (disp[3] - disp[0]) * l + (disp[4] - disp[1]) * m + (disp[5] - disp[2]) * n;
ea_over_len * du
}
}
#[derive(Debug, Clone, Copy)]
pub struct FrameElement2D {
pub elastic_modulus: f64,
pub area: f64,
pub inertia: f64,
pub node_i: [f64; 2],
pub node_j: [f64; 2],
}
impl FrameElement2D {
pub fn length(&self) -> f64 {
let dx = self.node_j[0] - self.node_i[0];
let dy = self.node_j[1] - self.node_i[1];
(dx * dx + dy * dy).sqrt()
}
pub fn angle(&self) -> f64 {
let dx = self.node_j[0] - self.node_i[0];
let dy = self.node_j[1] - self.node_i[1];
dy.atan2(dx)
}
pub fn local_stiffness(&self) -> [[f64; 6]; 6] {
let l = self.length().max(MIN_LENGTH);
let ea = self.elastic_modulus * self.area;
let ei = self.elastic_modulus * self.inertia;
let a = ea / l;
let b1 = 12.0 * ei / (l * l * l);
let b2 = 6.0 * ei / (l * l);
let b3 = 4.0 * ei / l;
let b4 = 2.0 * ei / l;
[
[a, 0.0, 0.0, -a, 0.0, 0.0],
[0.0, b1, b2, 0.0, -b1, b2],
[0.0, b2, b3, 0.0, -b2, b4],
[-a, 0.0, 0.0, a, 0.0, 0.0],
[0.0, -b1, -b2, 0.0, b1, -b2],
[0.0, b2, b4, 0.0, -b2, b3],
]
}
pub fn rotation_matrix(&self) -> [[f64; 6]; 6] {
let alpha = self.angle();
let c = alpha.cos();
let s = alpha.sin();
let mut t = [[0.0_f64; 6]; 6];
t[0][0] = c;
t[0][1] = s;
t[1][0] = -s;
t[1][1] = c;
t[2][2] = 1.0;
t[3][3] = c;
t[3][4] = s;
t[4][3] = -s;
t[4][4] = c;
t[5][5] = 1.0;
t
}
pub fn global_stiffness(&self) -> [[f64; 6]; 6] {
let kl = self.local_stiffness();
let t = self.rotation_matrix();
let kt = mat6_mul(&transpose6(&t), &kl);
mat6_mul(&kt, &t)
}
pub fn internal_forces(&self, disp_global: [f64; 6]) -> [f64; 6] {
let t = self.rotation_matrix();
let d_loc = mat6_vec_mul(&t, &disp_global);
let kl = self.local_stiffness();
mat6_vec_mul(&kl, &d_loc)
}
}
#[derive(Debug, Clone, Copy)]
pub struct SpaceFrameElement {
pub elastic_modulus: f64,
pub shear_modulus: f64,
pub area: f64,
pub iy: f64,
pub iz: f64,
pub j_torsion: f64,
pub node_i: [f64; 3],
pub node_j: [f64; 3],
}
impl SpaceFrameElement {
pub fn length(&self) -> f64 {
let dx = self.node_j[0] - self.node_i[0];
let dy = self.node_j[1] - self.node_i[1];
let dz = self.node_j[2] - self.node_i[2];
(dx * dx + dy * dy + dz * dz).sqrt()
}
pub fn axial_stiffness(&self) -> f64 {
self.elastic_modulus * self.area / self.length().max(MIN_LENGTH)
}
pub fn bending_z_coeff(&self) -> f64 {
self.elastic_modulus * self.iz / self.length().max(MIN_LENGTH).powi(3)
}
pub fn bending_y_coeff(&self) -> f64 {
self.elastic_modulus * self.iy / self.length().max(MIN_LENGTH).powi(3)
}
pub fn torsional_stiffness(&self) -> f64 {
self.shear_modulus * self.j_torsion / self.length().max(MIN_LENGTH)
}
pub fn axial_force(&self, u_i: f64, u_j: f64) -> f64 {
self.axial_stiffness() * (u_j - u_i)
}
pub fn torsional_moment(&self, phi_i: f64, phi_j: f64) -> f64 {
self.torsional_stiffness() * (phi_j - phi_i)
}
}
pub fn direction_cosine_matrix(
node_i: [f64; 3],
node_j: [f64; 3],
ref_up: [f64; 3],
) -> [[f64; 3]; 3] {
let ex = vec3_sub(node_j, node_i);
let ex_len = vec3_norm(ex).max(MIN_LENGTH);
let ex = vec3_scale(ex, 1.0 / ex_len);
let ez = vec3_cross(ex, ref_up);
let ez_len = vec3_norm(ez);
let (ez, ey) = if ez_len < 1e-10 {
let helper = [1.0_f64, 0.0, 0.0];
let ez2 = vec3_cross(ex, helper);
let ez2_len = vec3_norm(ez2).max(MIN_LENGTH);
let ez2 = vec3_scale(ez2, 1.0 / ez2_len);
let ey2 = vec3_cross(ez2, ex);
(ez2, ey2)
} else {
let ez = vec3_scale(ez, 1.0 / ez_len);
let ey = vec3_cross(ez, ex);
(ez, ey)
};
[ex, ey, ez]
}
pub fn expand_to_12x12(gamma: &[[f64; 3]; 3]) -> [[f64; 12]; 12] {
let mut t = [[0.0_f64; 12]; 12];
for block in 0..4 {
let r0 = 3 * block;
for i in 0..3 {
for j in 0..3 {
t[r0 + i][r0 + j] = gamma[i][j];
}
}
}
t
}
pub struct FrameAssembler2D {
pub n_dof: usize,
pub k_global: Vec<Vec<f64>>,
pub f_global: Vec<f64>,
}
impl FrameAssembler2D {
pub fn new(n_nodes: usize) -> Self {
let n_dof = n_nodes * FRAME2D_DOF_PER_NODE;
Self {
n_dof,
k_global: vec![vec![0.0_f64; n_dof]; n_dof],
f_global: vec![0.0_f64; n_dof],
}
}
pub fn add_element(&mut self, ke: &[[f64; 6]; 6], node_i: usize, node_j: usize) {
let dofs = [
node_i * 3,
node_i * 3 + 1,
node_i * 3 + 2,
node_j * 3,
node_j * 3 + 1,
node_j * 3 + 2,
];
for r in 0..6 {
for c in 0..6 {
self.k_global[dofs[r]][dofs[c]] += ke[r][c];
}
}
}
pub fn apply_dirichlet(&mut self, dof: usize, value: f64, penalty: f64) {
self.k_global[dof][dof] += penalty;
self.f_global[dof] += penalty * value;
}
pub fn solve(&self) -> Option<Vec<f64>> {
let n = self.n_dof;
let mut a = self.k_global.clone();
let mut b = self.f_global.clone();
for col in 0..n {
let mut max_row = col;
let mut max_val = a[col][col].abs();
for (row, a_row) in a.iter().enumerate().skip(col + 1) {
if a_row[col].abs() > max_val {
max_val = a_row[col].abs();
max_row = row;
}
}
if max_val < 1e-20 {
return None;
}
a.swap(col, max_row);
b.swap(col, max_row);
let pivot = a[col][col];
let col_slice: Vec<f64> = a[col][col..].to_vec();
for row in col + 1..n {
let factor = a[row][col] / pivot;
b[row] -= factor * b[col];
for (off, &cv) in col_slice.iter().enumerate() {
a[row][col + off] -= factor * cv;
}
}
}
let mut u = vec![0.0_f64; n];
for row in (0..n).rev() {
let mut s = b[row];
for c in row + 1..n {
s -= a[row][c] * u[c];
}
u[row] = s / a[row][row];
}
Some(u)
}
pub fn support_reactions(&self, u: &[f64], constrained: &[usize]) -> Vec<f64> {
let _n = self.n_dof;
let mut reactions = vec![0.0_f64; constrained.len()];
for (k, &dof) in constrained.iter().enumerate() {
let ku: f64 = self.k_global[dof]
.iter()
.zip(u.iter())
.map(|(&k, &u)| k * u)
.sum();
reactions[k] = ku - self.f_global[dof];
}
reactions
}
}
pub fn simply_supported_moment(w: f64, l: f64, s: f64) -> f64 {
(w * l / 2.0) * s - 0.5 * w * s * s
}
pub fn simply_supported_shear(w: f64, l: f64, s: f64) -> f64 {
w * l / 2.0 - w * s
}
pub fn simply_supported_max_moment(w: f64, l: f64) -> f64 {
w * l * l / 8.0
}
pub fn fixed_fixed_fem(w: f64, l: f64) -> f64 {
w * l * l / 12.0
}
pub const CARRYOVER_FIXED: f64 = 0.5;
pub fn distribution_factor(ei_over_l: f64, sum_stiff: f64) -> f64 {
if sum_stiff.abs() < f64::EPSILON {
0.0
} else {
ei_over_l / sum_stiff
}
}
pub fn moment_diagram(m_i: f64, m_j: f64, w: f64, l: f64, n_pts: usize) -> (Vec<f64>, Vec<f64>) {
let n = n_pts.max(2);
let mut pos = Vec::with_capacity(n);
let mut mom = Vec::with_capacity(n);
for k in 0..n {
let s = l * k as f64 / (n - 1) as f64;
let m_linear = m_i * (1.0 - s / l) + m_j * (s / l);
let m_udl = 0.5 * w * s * (l - s);
pos.push(s);
mom.push(m_linear + m_udl);
}
(pos, mom)
}
pub fn euler_buckling_load(elastic_modulus: f64, inertia: f64, length: f64, k_factor: f64) -> f64 {
let le = k_factor * length;
if le.abs() < MIN_LENGTH {
return f64::INFINITY;
}
PI * PI * elastic_modulus * inertia / (le * le)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum EndCondition {
PinnedPinned,
FixedFree,
FixedPinned,
FixedFixed,
}
impl EndCondition {
pub fn k_factor(&self) -> f64 {
match self {
Self::PinnedPinned => 1.0,
Self::FixedFree => 2.0,
Self::FixedPinned => 0.7,
Self::FixedFixed => 0.5,
}
}
}
pub fn radius_of_gyration(inertia: f64, area: f64) -> f64 {
if area < f64::EPSILON {
0.0
} else {
(inertia / area).sqrt()
}
}
pub fn slenderness_ratio(k_factor: f64, length: f64, r: f64) -> f64 {
if r.abs() < f64::EPSILON {
f64::INFINITY
} else {
k_factor * length / r
}
}
pub fn euler_critical_stress(elastic_modulus: f64, slenderness: f64) -> f64 {
if slenderness.abs() < f64::EPSILON {
f64::INFINITY
} else {
PI * PI * elastic_modulus / (slenderness * slenderness)
}
}
pub fn johnson_critical_stress(yield_stress: f64, elastic_modulus: f64, slenderness: f64) -> f64 {
yield_stress
* (1.0 - yield_stress * slenderness * slenderness / (4.0 * PI * PI * elastic_modulus))
}
#[derive(Debug, Clone, Copy)]
pub struct PortalFrame {
pub width: f64,
pub height: f64,
pub e_column: f64,
pub e_beam: f64,
pub i_column: f64,
pub i_beam: f64,
pub a_column: f64,
pub a_beam: f64,
}
impl PortalFrame {
pub fn column_stiffness(&self) -> f64 {
self.e_column * self.i_column / self.height
}
pub fn beam_stiffness(&self) -> f64 {
self.e_beam * self.i_beam / self.width
}
pub fn sway_stiffness(&self) -> f64 {
24.0 * self.e_column * self.i_column / (self.height * self.height * self.height)
}
pub fn sway_deflection(&self, horizontal_load: f64) -> f64 {
horizontal_load / self.sway_stiffness().max(f64::EPSILON)
}
pub fn column_moments(&self, horizontal_load: f64) -> f64 {
horizontal_load * self.height / 2.0
}
pub fn beam_midspan_moment(&self, w: f64) -> f64 {
w * self.width * self.width / 24.0
}
pub fn sway_buckling_load(&self) -> f64 {
self.sway_stiffness()
}
}
#[derive(Debug, Clone, Copy)]
pub struct FrameNode2D {
pub id: usize,
pub x: f64,
pub y: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Constraint {
FixedFixed,
PinnedSupport,
RollerVertical,
RollerHorizontal,
Free,
}
impl Constraint {
pub fn constrained_dofs(&self) -> [bool; 3] {
match self {
Self::FixedFixed => [true, true, true],
Self::PinnedSupport => [true, true, false],
Self::RollerVertical => [false, true, false],
Self::RollerHorizontal => [true, false, false],
Self::Free => [false, false, false],
}
}
}
pub struct PlaneFrame {
pub nodes: Vec<FrameNode2D>,
pub elements: Vec<(usize, usize, FrameElement2D)>,
pub constraints: Vec<Constraint>,
pub nodal_loads: Vec<[f64; 3]>,
}
impl PlaneFrame {
pub fn new(nodes: Vec<FrameNode2D>, constraints: Vec<Constraint>) -> Self {
let n = nodes.len();
Self {
nodes,
elements: Vec::new(),
constraints,
nodal_loads: vec![[0.0; 3]; n],
}
}
pub fn add_element(&mut self, ni: usize, nj: usize, e: f64, area: f64, inertia: f64) {
let elem = FrameElement2D {
elastic_modulus: e,
area,
inertia,
node_i: [self.nodes[ni].x, self.nodes[ni].y],
node_j: [self.nodes[nj].x, self.nodes[nj].y],
};
self.elements.push((ni, nj, elem));
}
pub fn apply_load(&mut self, n: usize, fx: f64, fy: f64, mz: f64) {
self.nodal_loads[n][0] += fx;
self.nodal_loads[n][1] += fy;
self.nodal_loads[n][2] += mz;
}
pub fn solve(&self) -> Option<Vec<f64>> {
let n_nodes = self.nodes.len();
let mut asm = FrameAssembler2D::new(n_nodes);
for (ni, loads) in self.nodal_loads.iter().enumerate() {
for (d, &load) in loads.iter().enumerate() {
asm.f_global[ni * 3 + d] += load;
}
}
for (ni, nj, elem) in &self.elements {
let ke = elem.global_stiffness();
asm.add_element(&ke, *ni, *nj);
}
let penalty = 1.0e30;
for (n_idx, constraint) in self.constraints.iter().enumerate() {
let flags = constraint.constrained_dofs();
for (d, &fixed) in flags.iter().enumerate() {
if fixed {
asm.apply_dirichlet(n_idx * 3 + d, 0.0, penalty);
}
}
}
asm.solve()
}
pub fn free_dof_count(&self) -> usize {
self.constraints
.iter()
.map(|c| {
let flags = c.constrained_dofs();
flags.iter().filter(|&&f| !f).count()
})
.sum()
}
}
pub fn transpose6(m: &[[f64; 6]; 6]) -> [[f64; 6]; 6] {
let mut t = [[0.0_f64; 6]; 6];
for i in 0..6 {
for j in 0..6 {
t[i][j] = m[j][i];
}
}
t
}
pub fn mat6_mul(a: &[[f64; 6]; 6], b: &[[f64; 6]; 6]) -> [[f64; 6]; 6] {
let mut c = [[0.0_f64; 6]; 6];
for i in 0..6 {
for k in 0..6 {
if a[i][k].abs() < f64::EPSILON * 1e-5 {
continue;
}
for j in 0..6 {
c[i][j] += a[i][k] * b[k][j];
}
}
}
c
}
pub fn mat6_vec_mul(m: &[[f64; 6]; 6], v: &[f64; 6]) -> [f64; 6] {
let mut r = [0.0_f64; 6];
for i in 0..6 {
for j in 0..6 {
r[i] += m[i][j] * v[j];
}
}
r
}
pub fn vec3_sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
pub fn vec3_norm(v: [f64; 3]) -> f64 {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
pub fn vec3_scale(v: [f64; 3], s: f64) -> [f64; 3] {
[v[0] * s, v[1] * s, v[2] * s]
}
pub fn vec3_cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
pub struct SpaceFrameAssembler {
pub n_dof: usize,
pub k_global: Vec<Vec<f64>>,
pub f_global: Vec<f64>,
}
impl SpaceFrameAssembler {
pub fn new(n_nodes: usize) -> Self {
let n_dof = n_nodes * FRAME3D_DOF_PER_NODE;
Self {
n_dof,
k_global: vec![vec![0.0_f64; n_dof]; n_dof],
f_global: vec![0.0_f64; n_dof],
}
}
pub fn add_element(&mut self, ke: &[[f64; 12]; 12], node_i: usize, node_j: usize) {
let mut dofs = [0usize; 12];
for d in 0..6 {
dofs[d] = node_i * 6 + d;
dofs[d + 6] = node_j * 6 + d;
}
for r in 0..12 {
for c in 0..12 {
self.k_global[dofs[r]][dofs[c]] += ke[r][c];
}
}
}
pub fn solve(&self) -> Option<Vec<f64>> {
let n = self.n_dof;
let mut a = self.k_global.clone();
let mut b = self.f_global.clone();
for col in 0..n {
let mut max_row = col;
let mut max_val = a[col][col].abs();
for (row, a_row) in a.iter().enumerate().skip(col + 1) {
if a_row[col].abs() > max_val {
max_val = a_row[col].abs();
max_row = row;
}
}
if max_val < 1e-20 {
return None;
}
a.swap(col, max_row);
b.swap(col, max_row);
let pivot = a[col][col];
let col_slice: Vec<f64> = a[col][col..].to_vec();
for row in col + 1..n {
let factor = a[row][col] / pivot;
b[row] -= factor * b[col];
for (off, &cv) in col_slice.iter().enumerate() {
a[row][col + off] -= factor * cv;
}
}
}
let mut u = vec![0.0_f64; n];
for row in (0..n).rev() {
let mut s = b[row];
for c in row + 1..n {
s -= a[row][c] * u[c];
}
u[row] = s / a[row][row];
}
Some(u)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_truss2d_length_horizontal() {
let e = TrussElement2D {
elastic_modulus: 2e11,
area: 1e-4,
node_i: [0.0, 0.0],
node_j: [3.0, 0.0],
};
assert!((e.length() - 3.0).abs() < 1e-12);
}
#[test]
fn test_truss2d_direction_cosines_horizontal() {
let e = TrussElement2D {
elastic_modulus: 2e11,
area: 1e-4,
node_i: [0.0, 0.0],
node_j: [4.0, 0.0],
};
let [l, m] = e.direction_cosines();
assert!((l - 1.0).abs() < 1e-12);
assert!(m.abs() < 1e-12);
}
#[test]
fn test_truss2d_direction_cosines_diagonal() {
let e = TrussElement2D {
elastic_modulus: 2e11,
area: 1e-4,
node_i: [0.0, 0.0],
node_j: [1.0, 1.0],
};
let [l, m] = e.direction_cosines();
let sq2 = 1.0 / 2.0_f64.sqrt();
assert!((l - sq2).abs() < 1e-10);
assert!((m - sq2).abs() < 1e-10);
}
#[test]
fn test_truss2d_global_stiffness_symmetry() {
let e = TrussElement2D {
elastic_modulus: 2e11,
area: 1e-4,
node_i: [0.0, 0.0],
node_j: [3.0, 4.0],
};
let k = e.global_stiffness();
for (i, row) in k.iter().enumerate() {
for (j, &val) in row.iter().enumerate() {
assert!((val - k[j][i]).abs() < 1e-6, "asymmetric at [{i}][{j}]");
}
}
}
#[test]
fn test_truss2d_axial_force_tension() {
let e = TrussElement2D {
elastic_modulus: 1.0,
area: 1.0,
node_i: [0.0, 0.0],
node_j: [1.0, 0.0],
};
let n = e.axial_force([0.0, 0.0, 0.1, 0.0]);
assert!((n - 0.1).abs() < 1e-12);
}
#[test]
fn test_truss2d_global_stiffness_row_sum_zero() {
let e = TrussElement2D {
elastic_modulus: 2e11,
area: 1e-4,
node_i: [0.0, 0.0],
node_j: [3.0, 4.0],
};
let k = e.global_stiffness();
for (i, row) in k.iter().enumerate() {
let row_sum: f64 = row.iter().sum();
assert!(row_sum.abs() < 1e-4, "row {i} sum = {row_sum}");
}
}
#[test]
fn test_truss3d_length() {
let e = TrussElement3D {
elastic_modulus: 2e11,
area: 1e-4,
node_i: [0.0, 0.0, 0.0],
node_j: [3.0, 4.0, 0.0],
};
assert!((e.length() - 5.0).abs() < 1e-10);
}
#[test]
fn test_truss3d_direction_cosines_unit_vector() {
let e = TrussElement3D {
elastic_modulus: 2e11,
area: 1e-4,
node_i: [0.0, 0.0, 0.0],
node_j: [1.0, 2.0, 2.0],
};
let dc = e.direction_cosines();
let norm = (dc[0] * dc[0] + dc[1] * dc[1] + dc[2] * dc[2]).sqrt();
assert!((norm - 1.0).abs() < 1e-10);
}
#[test]
fn test_truss3d_global_stiffness_symmetry() {
let e = TrussElement3D {
elastic_modulus: 2e11,
area: 1e-4,
node_i: [0.0, 0.0, 0.0],
node_j: [1.0, 2.0, 2.0],
};
let k = e.global_stiffness();
for (i, row) in k.iter().enumerate() {
for (j, &val) in row.iter().enumerate() {
assert!((val - k[j][i]).abs() < 1e-6);
}
}
}
#[test]
fn test_truss3d_axial_force_z() {
let e = TrussElement3D {
elastic_modulus: 1.0,
area: 1.0,
node_i: [0.0, 0.0, 0.0],
node_j: [0.0, 0.0, 2.0],
};
let n = e.axial_force([0.0, 0.0, 0.0, 0.0, 0.0, 0.2]);
assert!((n - 0.1).abs() < 1e-12);
}
#[test]
fn test_frame2d_local_stiffness_symmetry() {
let e = FrameElement2D {
elastic_modulus: 2e11,
area: 1e-3,
inertia: 1e-6,
node_i: [0.0, 0.0],
node_j: [5.0, 0.0],
};
let k = e.local_stiffness();
for (i, row) in k.iter().enumerate() {
for (j, &val) in row.iter().enumerate() {
assert!((val - k[j][i]).abs() < 1.0, "asymmetric [{i}][{j}]");
}
}
}
#[test]
fn test_frame2d_global_stiffness_symmetry() {
let e = FrameElement2D {
elastic_modulus: 2e11,
area: 1e-3,
inertia: 1e-6,
node_i: [0.0, 0.0],
node_j: [3.0, 4.0],
};
let k = e.global_stiffness();
for (i, row) in k.iter().enumerate() {
for (j, &val) in row.iter().enumerate() {
assert!((val - k[j][i]).abs() < 1.0, "asymmetric [{i}][{j}]");
}
}
}
#[test]
fn test_frame2d_angle_horizontal() {
let e = FrameElement2D {
elastic_modulus: 2e11,
area: 1e-3,
inertia: 1e-6,
node_i: [0.0, 0.0],
node_j: [1.0, 0.0],
};
assert!(e.angle().abs() < 1e-12);
}
#[test]
fn test_frame2d_angle_vertical() {
let e = FrameElement2D {
elastic_modulus: 2e11,
area: 1e-3,
inertia: 1e-6,
node_i: [0.0, 0.0],
node_j: [0.0, 1.0],
};
assert!((e.angle() - PI / 2.0).abs() < 1e-12);
}
#[test]
fn test_euler_buckling_pinned_pinned() {
let p_cr = euler_buckling_load(1.0, 1.0, 1.0, 1.0);
assert!((p_cr - PI * PI).abs() < 1e-10);
}
#[test]
fn test_euler_buckling_fixed_free() {
let p1 = euler_buckling_load(1.0, 1.0, 1.0, EndCondition::PinnedPinned.k_factor());
let p2 = euler_buckling_load(1.0, 1.0, 1.0, EndCondition::FixedFree.k_factor());
assert!((p2 - p1 / 4.0).abs() < 1e-10);
}
#[test]
fn test_euler_critical_stress() {
let sigma = euler_critical_stress(1.0, PI);
assert!((sigma - 1.0).abs() < 1e-10);
}
#[test]
fn test_radius_of_gyration() {
let r = radius_of_gyration(4.0, 1.0);
assert!((r - 2.0).abs() < 1e-12);
}
#[test]
fn test_slenderness_ratio() {
let lambda = slenderness_ratio(1.0, 10.0, 0.5);
assert!((lambda - 20.0).abs() < 1e-12);
}
#[test]
fn test_end_condition_k_factors() {
assert!((EndCondition::PinnedPinned.k_factor() - 1.0).abs() < 1e-12);
assert!((EndCondition::FixedFree.k_factor() - 2.0).abs() < 1e-12);
assert!((EndCondition::FixedFixed.k_factor() - 0.5).abs() < 1e-12);
}
#[test]
fn test_simply_supported_moment_midspan() {
let m = simply_supported_moment(1.0, 10.0, 5.0);
assert!((m - 12.5).abs() < 1e-10);
}
#[test]
fn test_simply_supported_max_moment() {
let m_max = simply_supported_max_moment(1.0, 10.0);
assert!((m_max - 12.5).abs() < 1e-10);
}
#[test]
fn test_simply_supported_shear_midspan() {
let v = simply_supported_shear(1.0, 10.0, 5.0);
assert!(v.abs() < 1e-12);
}
#[test]
fn test_fixed_fixed_fem() {
let fem = fixed_fixed_fem(1.0, 6.0);
assert!((fem - 3.0).abs() < 1e-10);
}
#[test]
fn test_distribution_factor_sum_one() {
let df1 = distribution_factor(1.0, 2.0);
let df2 = distribution_factor(1.0, 2.0);
assert!((df1 + df2 - 1.0).abs() < 1e-12);
}
#[test]
fn test_portal_sway_stiffness_positive() {
let p = PortalFrame {
width: 5.0,
height: 4.0,
e_column: 2e11,
e_beam: 2e11,
i_column: 1e-5,
i_beam: 2e-5,
a_column: 1e-3,
a_beam: 1e-3,
};
assert!(p.sway_stiffness() > 0.0);
}
#[test]
fn test_portal_sway_deflection_proportional() {
let p = PortalFrame {
width: 5.0,
height: 4.0,
e_column: 2e11,
e_beam: 2e11,
i_column: 1e-5,
i_beam: 2e-5,
a_column: 1e-3,
a_beam: 1e-3,
};
let d1 = p.sway_deflection(10.0);
let d2 = p.sway_deflection(20.0);
assert!((d2 / d1 - 2.0).abs() < 1e-10);
}
#[test]
fn test_portal_column_moments() {
let p = PortalFrame {
width: 5.0,
height: 4.0,
e_column: 2e11,
e_beam: 2e11,
i_column: 1e-5,
i_beam: 2e-5,
a_column: 1e-3,
a_beam: 1e-3,
};
assert!((p.column_moments(10.0) - 20.0).abs() < 1e-10);
}
#[test]
fn test_plane_frame_cantilever_tip_displacement() {
let nodes = vec![
FrameNode2D {
id: 0,
x: 0.0,
y: 0.0,
},
FrameNode2D {
id: 1,
x: 2.0,
y: 0.0,
},
];
let constraints = vec![Constraint::FixedFixed, Constraint::Free];
let mut frame = PlaneFrame::new(nodes, constraints);
let e = 1.0e4;
let a = 0.1;
let i = 1.0e-3;
frame.add_element(0, 1, e, a, i);
frame.apply_load(1, 0.0, -1.0, 0.0); let u = frame.solve().expect("solver failed");
let expected = -8.0 / (3.0 * e * i);
let got = u[4]; assert!(
(got - expected).abs() / expected.abs() < 1e-4,
"got={got} expected={expected}"
);
}
#[test]
fn test_plane_frame_free_dof_count() {
let nodes = vec![
FrameNode2D {
id: 0,
x: 0.0,
y: 0.0,
},
FrameNode2D {
id: 1,
x: 1.0,
y: 0.0,
},
];
let constraints = vec![Constraint::FixedFixed, Constraint::Free];
let frame = PlaneFrame::new(nodes, constraints);
assert_eq!(frame.free_dof_count(), 3);
}
#[test]
fn test_direction_cosine_matrix_orthonormal() {
let ni = [0.0, 0.0, 0.0];
let nj = [1.0, 0.0, 0.0];
let up = [0.0, 0.0, 1.0];
let gamma = direction_cosine_matrix(ni, nj, up);
for (i, row) in gamma.iter().enumerate() {
let norm: f64 = row.iter().map(|&x| x * x).sum::<f64>().sqrt();
assert!((norm - 1.0).abs() < 1e-10, "row {i} norm = {norm}");
}
}
#[test]
fn test_direction_cosine_matrix_vertical_member() {
let ni = [0.0, 0.0, 0.0];
let nj = [0.0, 0.0, 5.0];
let up = [0.0, 0.0, 1.0];
let gamma = direction_cosine_matrix(ni, nj, up);
assert!((gamma[0][2] - 1.0).abs() < 1e-10);
}
#[test]
fn test_moment_diagram_simply_supported_udl() {
let (pos, mom) = moment_diagram(0.0, 0.0, 1.0, 10.0, 11);
let mid = mom[5];
assert!((mid - 12.5).abs() < 1e-8, "mid moment = {mid}");
assert!(pos.len() == 11);
}
#[test]
fn test_moment_diagram_end_values() {
let m_i = 5.0;
let m_j = 3.0;
let (_pos, mom) = moment_diagram(m_i, m_j, 0.0, 4.0, 5);
assert!((mom[0] - m_i).abs() < 1e-10);
assert!((mom[4] - m_j).abs() < 1e-10);
}
#[test]
fn test_mat6_mul_identity() {
let mut id = [[0.0_f64; 6]; 6];
for (i, row) in id.iter_mut().enumerate() {
row[i] = 1.0;
}
let a = [[1.0_f64; 6]; 6];
let c = mat6_mul(&a, &id);
for (i, row) in c.iter().enumerate() {
for (j, &val) in row.iter().enumerate() {
assert!((val - a[i][j]).abs() < 1e-12);
}
}
}
#[test]
fn test_transpose6_double_transpose() {
let mut m = [[0.0_f64; 6]; 6];
for (i, row) in m.iter_mut().enumerate() {
for (j, v) in row.iter_mut().enumerate() {
*v = (i * 6 + j) as f64;
}
}
let tt = transpose6(&transpose6(&m));
for (i, row) in tt.iter().enumerate() {
for (j, &val) in row.iter().enumerate() {
assert!((val - m[i][j]).abs() < 1e-12);
}
}
}
#[test]
fn test_vec3_cross_orthogonal() {
let a = [1.0, 0.0, 0.0];
let b = [0.0, 1.0, 0.0];
let c = vec3_cross(a, b);
assert!((c[2] - 1.0).abs() < 1e-12);
assert!(c[0].abs() < 1e-12);
assert!(c[1].abs() < 1e-12);
}
#[test]
fn test_space_frame_axial_stiffness() {
let e = SpaceFrameElement {
elastic_modulus: 1.0,
shear_modulus: 0.5,
area: 2.0,
iy: 1.0,
iz: 1.0,
j_torsion: 1.0,
node_i: [0.0, 0.0, 0.0],
node_j: [4.0, 0.0, 0.0],
};
assert!((e.axial_stiffness() - 0.5).abs() < 1e-12);
}
#[test]
fn test_space_frame_torsional_stiffness() {
let e = SpaceFrameElement {
elastic_modulus: 2.0,
shear_modulus: 1.0,
area: 1.0,
iy: 1.0,
iz: 1.0,
j_torsion: 2.0,
node_i: [0.0, 0.0, 0.0],
node_j: [2.0, 0.0, 0.0],
};
assert!((e.torsional_stiffness() - 1.0).abs() < 1e-12);
}
#[test]
fn test_space_frame_axial_force() {
let e = SpaceFrameElement {
elastic_modulus: 1.0,
shear_modulus: 0.5,
area: 1.0,
iy: 1.0,
iz: 1.0,
j_torsion: 1.0,
node_i: [0.0, 0.0, 0.0],
node_j: [1.0, 0.0, 0.0],
};
let n = e.axial_force(0.0, 0.2);
assert!((n - 0.2).abs() < 1e-12);
}
}