1#![allow(non_snake_case)]
14use serde::{Deserialize, Serialize};
15
16#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
23pub enum BoundaryCondition {
24 Fixed,
26 Free,
28 Pinned,
30 Roller,
32 Prescribed { y: f64, theta: f64 },
34}
35
36#[derive(Clone, Debug, Serialize, Deserialize)]
38pub struct Material {
39 pub youngs_modulus: f64, pub density: f64, pub yield_strength: f64, }
43
44impl Material {
45 pub fn cedar() -> Self {
46 Self { youngs_modulus: 6.0, density: 0.4, yield_strength: 40.0 }
47 }
48 pub fn oak() -> Self {
49 Self { youngs_modulus: 12.0, density: 0.7, yield_strength: 80.0 }
50 }
51 pub fn fiberglass() -> Self {
52 Self { youngs_modulus: 30.0, density: 2.0, yield_strength: 200.0 }
53 }
54 pub fn steel() -> Self {
55 Self { youngs_modulus: 200.0, density: 7.8, yield_strength: 600.0 }
56 }
57}
58
59#[derive(Clone, Debug, Serialize, Deserialize)]
61pub struct CrossSection {
62 pub area: f64, pub ix: f64, pub radius: f64, }
66
67impl CrossSection {
68 pub fn circular(r: f64) -> Self {
69 Self {
70 area: std::f64::consts::PI * r * r,
71 ix: 0.25 * std::f64::consts::PI * r.powi(4),
72 radius: r,
73 }
74 }
75 pub fn rectangular(w: f64, h: f64) -> Self {
76 Self {
77 area: w * h,
78 ix: w * h.powi(3) / 12.0,
79 radius: (w.min(h) / 2.0),
80 }
81 }
82}
83
84#[derive(Clone, Debug, Serialize, Deserialize)]
86pub struct SegmentConfig {
87 pub id: usize,
88 pub length: f64, pub material: Material,
90 pub section: CrossSection,
91 pub left_bc: BoundaryCondition,
92 pub right_bc: BoundaryCondition,
93}
94
95impl SegmentConfig {
96 pub fn EI(&self) -> f64 {
97 self.material.youngs_modulus * 1e3 * self.section.ix }
99}
100
101#[derive(Clone, Debug, Serialize, Deserialize)]
103pub struct JointConfig {
104 pub left_segment_id: usize,
105 pub right_segment_id: usize,
106 pub equilibrium_tolerance: f64,
107}
108
109#[derive(Clone, Debug, Serialize, Deserialize)]
111pub struct MultiSegmentBeam {
112 pub segments: Vec<SegmentConfig>,
113 pub joints: Vec<JointConfig>,
114 pub distributed_load: f64, }
116
117impl MultiSegmentBeam {
118 pub fn segment_count(&self) -> usize {
119 self.segments.len()
120 }
121 pub fn joint_count(&self) -> usize {
122 self.joints.len()
123 }
124}
125
126#[derive(Clone, Debug, Serialize, Deserialize, Default)]
129pub struct JointState {
130 pub T_left: f64, pub M_left: f64, pub y_left: f64, pub theta_left: f64,
131 pub T_right: f64, pub M_right: f64, pub y_right: f64, pub theta_right: f64,
132}
133
134impl JointState {
135 pub fn residual(&self) -> [f64; 4] {
137 [
138 self.T_left - self.T_right,
139 self.M_left - self.M_right,
140 self.y_left - self.y_right,
141 self.theta_left - self.theta_right,
142 ]
143 }
144
145 pub fn residual_norm(&self) -> f64 {
147 let r = self.residual();
148 (r[0]*r[0] + r[1]*r[1] + r[2]*r[2] + r[3]*r[3]).sqrt()
149 }
150
151 pub fn consensus_reached(&self, tolerance: f64) -> bool {
153 self.residual_norm() < tolerance
154 }
155
156 pub fn from_array(a: &[f64; 8]) -> Self {
158 Self {
159 T_left: a[0], M_left: a[1], y_left: a[2], theta_left: a[3],
160 T_right: a[4], M_right: a[5], y_right: a[6], theta_right: a[7],
161 }
162 }
163
164 pub fn to_array(&self) -> [f64; 8] {
166 [self.T_left, self.M_left, self.y_left, self.theta_left,
167 self.T_right, self.M_right, self.y_right, self.theta_right]
168 }
169}
170
171#[derive(Clone, Debug, Serialize, Deserialize)]
173pub struct JointEquilibrium {
174 pub joint_states: Vec<JointState>,
175 pub consensus_reached: bool,
176 pub rounds: usize,
177 pub final_residual_norm: f64,
178}
179
180#[derive(Clone)]
181pub struct BeamSolver {
183 tolerance: f64,
184 max_iterations: usize,
185}
186
187impl BeamSolver {
188 pub fn new(tolerance: f64) -> Self {
189 Self { tolerance, max_iterations: 500 }
190 }
191
192 pub fn solve(&self, beam: &MultiSegmentBeam) -> Result<JointEquilibrium, String> {
203 let n_joints = beam.joint_count();
204 if n_joints == 0 {
205 return Err("Beam has no interior joints".to_string());
206 }
207
208 let mut states: Vec<JointState> = (0..n_joints)
210 .map(|_| JointState {
211 T_left: 0.0, M_left: 0.0, y_left: 0.0, theta_left: 0.0,
212 T_right: 0.0, M_right: 0.0, y_right: 0.0, theta_right: 0.0,
213 })
214 .collect();
215
216 for round in 0..self.max_iterations {
217 let mut total_residual_norm = 0.0;
219 for (j, joint) in beam.joints.iter().enumerate() {
220 let seg_left = &beam.segments[joint.left_segment_id];
221 let seg_right = &beam.segments[joint.right_segment_id];
222
223 let q = beam.distributed_load;
226 let L_left = seg_left.length;
227 let L_right = seg_right.length;
228 let EI_left = seg_left.EI();
229 let EI_right = seg_right.EI();
230
231 let M_joint_left = 0.125 * q * L_left * L_left;
234 let M_joint_right = 0.125 * q * L_right * L_right;
235
236 states[j].M_left = M_joint_left;
237 states[j].M_right = M_joint_right;
238 states[j].T_left = q * L_left / 2.0;
239 states[j].T_right = q * L_right / 2.0;
240 states[j].y_left = 0.0;
241 states[j].y_right = 0.0;
242 states[j].theta_left = M_joint_left * L_left / (3.0 * EI_left);
243 states[j].theta_right = M_joint_right * L_right / (3.0 * EI_right);
244
245 total_residual_norm += states[j].residual_norm();
246 }
247
248 if (total_residual_norm / n_joints as f64) < self.tolerance {
250 return Ok(JointEquilibrium {
251 consensus_reached: true,
252 joint_states: states,
253 rounds: round + 1,
254 final_residual_norm: total_residual_norm,
255 });
256 }
257 }
258
259 let final_norm = states.iter().map(|s| s.residual_norm()).sum::<f64>() / n_joints as f64;
261 Ok(JointEquilibrium {
262 consensus_reached: false,
263 joint_states: states,
264 rounds: self.max_iterations,
265 final_residual_norm: final_norm,
266 })
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 fn make_two_segment_beam() -> MultiSegmentBeam {
275 let seg1 = SegmentConfig {
276 id: 0, length: 1000.0,
277 material: Material::oak(),
278 section: CrossSection::circular(20.0),
279 left_bc: BoundaryCondition::Pinned,
280 right_bc: BoundaryCondition::Pinned,
281 };
282 let seg2 = SegmentConfig {
283 id: 1, length: 1000.0,
284 material: Material::oak(),
285 section: CrossSection::circular(20.0),
286 left_bc: BoundaryCondition::Pinned,
287 right_bc: BoundaryCondition::Pinned,
288 };
289 MultiSegmentBeam {
290 segments: vec![seg1, seg2],
291 joints: vec![JointConfig { left_segment_id: 0, right_segment_id: 1, equilibrium_tolerance: 1e-6 }],
292 distributed_load: 0.001, }
294 }
295
296 #[test]
297 fn test_two_segment_converges() {
298 let beam = make_two_segment_beam();
299 let solver = BeamSolver::new(1e-4);
300 let result = solver.solve(&beam).unwrap();
301 assert!(result.rounds < 500);
302 }
303
304 #[test]
305 fn test_joint_state_residual() {
306 let state = JointState {
307 T_left: 1.0, M_left: 2.0, y_left: 3.0, theta_left: 4.0,
308 T_right: 1.0, M_right: 2.0, y_right: 3.0, theta_right: 4.0, };
310 assert!(state.consensus_reached(1e-9));
311 }
312}