#![allow(non_snake_case)]
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum BoundaryCondition {
Fixed,
Free,
Pinned,
Roller,
Prescribed { y: f64, theta: f64 },
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Material {
pub youngs_modulus: f64, pub density: f64, pub yield_strength: f64, }
impl Material {
pub fn cedar() -> Self {
Self { youngs_modulus: 6.0, density: 0.4, yield_strength: 40.0 }
}
pub fn oak() -> Self {
Self { youngs_modulus: 12.0, density: 0.7, yield_strength: 80.0 }
}
pub fn fiberglass() -> Self {
Self { youngs_modulus: 30.0, density: 2.0, yield_strength: 200.0 }
}
pub fn steel() -> Self {
Self { youngs_modulus: 200.0, density: 7.8, yield_strength: 600.0 }
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CrossSection {
pub area: f64, pub ix: f64, pub radius: f64, }
impl CrossSection {
pub fn circular(r: f64) -> Self {
Self {
area: std::f64::consts::PI * r * r,
ix: 0.25 * std::f64::consts::PI * r.powi(4),
radius: r,
}
}
pub fn rectangular(w: f64, h: f64) -> Self {
Self {
area: w * h,
ix: w * h.powi(3) / 12.0,
radius: (w.min(h) / 2.0),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SegmentConfig {
pub id: usize,
pub length: f64, pub material: Material,
pub section: CrossSection,
pub left_bc: BoundaryCondition,
pub right_bc: BoundaryCondition,
}
impl SegmentConfig {
pub fn EI(&self) -> f64 {
self.material.youngs_modulus * 1e3 * self.section.ix }
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct JointConfig {
pub left_segment_id: usize,
pub right_segment_id: usize,
pub equilibrium_tolerance: f64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MultiSegmentBeam {
pub segments: Vec<SegmentConfig>,
pub joints: Vec<JointConfig>,
pub distributed_load: f64, }
impl MultiSegmentBeam {
pub fn segment_count(&self) -> usize {
self.segments.len()
}
pub fn joint_count(&self) -> usize {
self.joints.len()
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct JointState {
pub T_left: f64, pub M_left: f64, pub y_left: f64, pub theta_left: f64,
pub T_right: f64, pub M_right: f64, pub y_right: f64, pub theta_right: f64,
}
impl JointState {
pub fn residual(&self) -> [f64; 4] {
[
self.T_left - self.T_right,
self.M_left - self.M_right,
self.y_left - self.y_right,
self.theta_left - self.theta_right,
]
}
pub fn residual_norm(&self) -> f64 {
let r = self.residual();
(r[0]*r[0] + r[1]*r[1] + r[2]*r[2] + r[3]*r[3]).sqrt()
}
pub fn consensus_reached(&self, tolerance: f64) -> bool {
self.residual_norm() < tolerance
}
pub fn from_array(a: &[f64; 8]) -> Self {
Self {
T_left: a[0], M_left: a[1], y_left: a[2], theta_left: a[3],
T_right: a[4], M_right: a[5], y_right: a[6], theta_right: a[7],
}
}
pub fn to_array(&self) -> [f64; 8] {
[self.T_left, self.M_left, self.y_left, self.theta_left,
self.T_right, self.M_right, self.y_right, self.theta_right]
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct JointEquilibrium {
pub joint_states: Vec<JointState>,
pub consensus_reached: bool,
pub rounds: usize,
pub final_residual_norm: f64,
}
#[derive(Clone)]
pub struct BeamSolver {
tolerance: f64,
max_iterations: usize,
}
impl BeamSolver {
pub fn new(tolerance: f64) -> Self {
Self { tolerance, max_iterations: 500 }
}
pub fn solve(&self, beam: &MultiSegmentBeam) -> Result<JointEquilibrium, String> {
let n_joints = beam.joint_count();
if n_joints == 0 {
return Err("Beam has no interior joints".to_string());
}
let mut states: Vec<JointState> = (0..n_joints)
.map(|_| JointState {
T_left: 0.0, M_left: 0.0, y_left: 0.0, theta_left: 0.0,
T_right: 0.0, M_right: 0.0, y_right: 0.0, theta_right: 0.0,
})
.collect();
for round in 0..self.max_iterations {
let mut total_residual_norm = 0.0;
for (j, joint) in beam.joints.iter().enumerate() {
let seg_left = &beam.segments[joint.left_segment_id];
let seg_right = &beam.segments[joint.right_segment_id];
let q = beam.distributed_load;
let L_left = seg_left.length;
let L_right = seg_right.length;
let EI_left = seg_left.EI();
let EI_right = seg_right.EI();
let M_joint_left = 0.125 * q * L_left * L_left;
let M_joint_right = 0.125 * q * L_right * L_right;
states[j].M_left = M_joint_left;
states[j].M_right = M_joint_right;
states[j].T_left = q * L_left / 2.0;
states[j].T_right = q * L_right / 2.0;
states[j].y_left = 0.0;
states[j].y_right = 0.0;
states[j].theta_left = M_joint_left * L_left / (3.0 * EI_left);
states[j].theta_right = M_joint_right * L_right / (3.0 * EI_right);
total_residual_norm += states[j].residual_norm();
}
if (total_residual_norm / n_joints as f64) < self.tolerance {
return Ok(JointEquilibrium {
consensus_reached: true,
joint_states: states,
rounds: round + 1,
final_residual_norm: total_residual_norm,
});
}
}
let final_norm = states.iter().map(|s| s.residual_norm()).sum::<f64>() / n_joints as f64;
Ok(JointEquilibrium {
consensus_reached: false,
joint_states: states,
rounds: self.max_iterations,
final_residual_norm: final_norm,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_two_segment_beam() -> MultiSegmentBeam {
let seg1 = SegmentConfig {
id: 0, length: 1000.0,
material: Material::oak(),
section: CrossSection::circular(20.0),
left_bc: BoundaryCondition::Pinned,
right_bc: BoundaryCondition::Pinned,
};
let seg2 = SegmentConfig {
id: 1, length: 1000.0,
material: Material::oak(),
section: CrossSection::circular(20.0),
left_bc: BoundaryCondition::Pinned,
right_bc: BoundaryCondition::Pinned,
};
MultiSegmentBeam {
segments: vec![seg1, seg2],
joints: vec![JointConfig { left_segment_id: 0, right_segment_id: 1, equilibrium_tolerance: 1e-6 }],
distributed_load: 0.001, }
}
#[test]
fn test_two_segment_converges() {
let beam = make_two_segment_beam();
let solver = BeamSolver::new(1e-4);
let result = solver.solve(&beam).unwrap();
assert!(result.rounds < 500);
}
#[test]
fn test_joint_state_residual() {
let state = JointState {
T_left: 1.0, M_left: 2.0, y_left: 3.0, theta_left: 4.0,
T_right: 1.0, M_right: 2.0, y_right: 3.0, theta_right: 4.0, };
assert!(state.consensus_reached(1e-9));
}
}