Skip to main content

fleet_coordinate/
beam.rs

1//! Beam Joint Equilibrium — spline-physics Phase D, re-exported for fleet-coordinate
2//!
3//! A multi-segment beam is a fleet of segment-agents joined at interior joints.
4//! Joint equilibrium = all segment agents reach consensus on (T, M, y, θ) at each joint.
5//!
6//! The key insight: beam equilibrium IS a consensus problem in R⁴⁽ᴺ⁻¹⁾.
7//! The "trust topology" is the joint adjacency graph. Consensus is reached
8//! when the sheaf cohomology H⁰(S) is non-empty (global section exists).
9//!
10//! This module re-uses the multi_segment module from spline-physics via
11//! the multi_agent/joint_debate module.
12
13#![allow(non_snake_case)]
14use serde::{Deserialize, Serialize};
15
16// =====================================================================
17// Re-exported types from spline-physics multi_segment module
18// These would normally come from the spline-physics crate as a dependency
19// =====================================================================
20
21/// Boundary condition at a beam endpoint
22#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
23pub enum BoundaryCondition {
24    /// Fully fixed (displacement + slope = 0)
25    Fixed,
26    /// Free (zero internal force)
27    Free,
28    /// Pinned (displacement = 0, moment free)
29    Pinned,
30    /// Roller (one direction free)
31    Roller,
32    /// Prescribed displacement and/or slope
33    Prescribed { y: f64, theta: f64 },
34}
35
36/// Material properties for beam segments
37#[derive(Clone, Debug, Serialize, Deserialize)]
38pub struct Material {
39    pub youngs_modulus: f64, // GPa
40    pub density: f64,         // g/cm³
41    pub yield_strength: f64, // MPa
42}
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/// Cross-sectional shape of a beam
60#[derive(Clone, Debug, Serialize, Deserialize)]
61pub struct CrossSection {
62    pub area: f64,      // mm²
63    pub ix: f64,        // second moment of area, mm⁴
64    pub radius: f64,    // effective radius, mm
65}
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/// One segment of a multi-segment beam
85#[derive(Clone, Debug, Serialize, Deserialize)]
86pub struct SegmentConfig {
87    pub id: usize,
88    pub length: f64,        // mm
89    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 // N·mm²
98    }
99}
100
101/// One interior joint connecting adjacent segments
102#[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/// The full multi-segment beam problem
110#[derive(Clone, Debug, Serialize, Deserialize)]
111pub struct MultiSegmentBeam {
112    pub segments: Vec<SegmentConfig>,
113    pub joints: Vec<JointConfig>,
114    pub distributed_load: f64, // N/mm
115}
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/// Joint state vector — the R⁴ × 2 compatibility vector
127/// (T, M, y, θ) at left side and right side of one joint
128#[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    /// Compute the residual (difference between left and right states)
136    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    /// Norm of the residual vector
146    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    /// True if residual norm is below tolerance (consensus reached)
152    pub fn consensus_reached(&self, tolerance: f64) -> bool {
153        self.residual_norm() < tolerance
154    }
155
156    /// Create from a flat array [T_left, M_left, y_left, theta_left, T_right, ...]
157    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    /// Flatten to array
165    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/// Result of joint equilibrium solving
172#[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)]
181/// Beam solver — joint equilibrium via Newton-Raphson in R^{4(N-1)}
182pub 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    /// Solve joint equilibrium for a multi-segment beam
193    ///
194    /// Algorithm:
195    /// 1. Initialize joint state guesses (zero internal forces)
196    /// 2. For each segment, integrate Euler elastica via shooting
197    /// 3. Collect residuals at each joint: R_j = state_left - state_right
198    /// 4. Newton-Raphson step in R^{4(N-1)}
199    /// 5. Iterate until ||R||₂ < tolerance or max iterations
200    ///
201    /// This is O(N) in segment count — each iteration is one forward pass.
202    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        // Initialize joint state (zero internal forces, flat beam)
209        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            // Compute residuals for all joints
218            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                // Simplified: use Euler elastica linear approximation
224                // In full implementation: shooting method integration
225                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                // Linear approximation for demonstration:
232                // M at joint = q * L² / 8 for simply supported
233                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            // Check convergence
249            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        // Max iterations reached
260        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, // N/mm
293        }
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, // perfect match
309        };
310        assert!(state.consensus_reached(1e-9));
311    }
312}