Skip to main content

carapace_core/model/
integration.rs

1/// Numerical integration scheme along a frame element's length, relating
2/// per-section response (`FiberSection`) to element-level response — needed
3/// by `DispBeamColumn`. Closed enum.
4///
5/// Point/weight tables are copied directly from OpenSees's own
6/// (`xara/SRC/quadrature/Frame/{Legendre,Lobatto}BeamIntegration.cpp`),
7/// not computed via a general-purpose quadrature crate — domain-specific
8/// numerics like this are copied from the reference implementation, the
9/// same way element/material formulations are, not sourced from a generic
10/// library (unlike heavily-optimized linear algebra, e.g. `nalgebra`/
11/// `faer`, where using a library is the right call). Supports 2-6 points,
12/// matching the range `DispBeamColumn` actually needs in practice — the
13/// C++ source supports up to 10; extend the tables if that's ever needed.
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub enum BeamIntegration {
16    /// Gauss-Legendre: interior points only, `n`-point rule exact to
17    /// degree `2n-1`. `DispBeamColumn`'s usual default.
18    Legendre { points: usize },
19    /// Gauss-Lobatto: includes both end points, `n`-point rule exact to
20    /// degree `2n-3` — favored for `ForceBeamColumn` since end-section
21    /// behavior matters most for plastic hinges there, but not needed by
22    /// `DispBeamColumn` itself. Included now since both were asked for
23    /// together and the table-driven implementation cost is the
24    /// same either way.
25    Lobatto { points: usize },
26}
27
28impl BeamIntegration {
29    /// `(xi, weight)` pairs, `xi` in `[0,1]` along the element length,
30    /// weights summing to 1 — already in the same mapped-to-`[0,1]` form
31    /// `getSectionLocations`/`getSectionWeights` return in the C++ source
32    /// (`xi = 0.5*(xi+1)`, `wt *= 0.5`), not the raw `[-1,1]` table.
33    pub fn points(&self) -> Vec<(f64, f64)> {
34        let (raw_xi, raw_wt): (&[f64], &[f64]) = match self {
35            BeamIntegration::Legendre { points } => legendre_table(*points),
36            BeamIntegration::Lobatto { points } => lobatto_table(*points),
37        };
38        raw_xi
39            .iter()
40            .zip(raw_wt)
41            .map(|(&xi, &wt)| (0.5 * (xi + 1.0), 0.5 * wt))
42            .collect()
43    }
44}
45
46fn legendre_table(points: usize) -> (&'static [f64], &'static [f64]) {
47    match points {
48        2 => (&[-0.577350269189626, 0.577350269189626], &[1.0, 1.0]),
49        3 => (
50            &[-0.774596669241483, 0.0, 0.774596669241483],
51            &[0.555555555555556, 0.888888888888889, 0.555555555555556],
52        ),
53        4 => (
54            &[
55                -0.861136311594053,
56                -0.339981043584856,
57                0.339981043584856,
58                0.861136311594053,
59            ],
60            &[
61                0.347854845137454,
62                0.652145154862546,
63                0.652145154862546,
64                0.347854845137454,
65            ],
66        ),
67        5 => (
68            &[
69                -0.906179845938664,
70                -0.538469310105683,
71                0.0,
72                0.538469310105683,
73                0.906179845938664,
74            ],
75            &[
76                0.236926885056189,
77                0.478628670499366,
78                0.568888888888889,
79                0.478628670499366,
80                0.236926885056189,
81            ],
82        ),
83        6 => (
84            &[
85                -0.932469514203152,
86                -0.661209386466265,
87                -0.238619186083197,
88                0.238619186083197,
89                0.661209386466265,
90                0.932469514203152,
91            ],
92            &[
93                0.171324492379170,
94                0.360761573048139,
95                0.467913934572691,
96                0.467913934572691,
97                0.360761573048139,
98                0.171324492379170,
99            ],
100        ),
101        _ => panic!("BeamIntegration::Legendre supports 2-6 points, got {points}"),
102    }
103}
104
105fn lobatto_table(points: usize) -> (&'static [f64], &'static [f64]) {
106    match points {
107        2 => (&[-1.0, 1.0], &[1.0, 1.0]),
108        3 => (
109            &[-1.0, 0.0, 1.0],
110            &[0.333333333333333, 1.333333333333333, 0.333333333333333],
111        ),
112        4 => (
113            &[-1.0, -0.44721360, 0.44721360, 1.0],
114            &[
115                0.166666666666667,
116                0.833333333333333,
117                0.833333333333333,
118                0.166666666666667,
119            ],
120        ),
121        5 => (
122            &[-1.0, -0.65465367, 0.0, 0.65465367, 1.0],
123            &[0.1, 0.5444444444, 0.7111111111, 0.5444444444, 0.1],
124        ),
125        6 => (
126            &[
127                -1.0,
128                -0.7650553239,
129                -0.2852315164,
130                0.2852315164,
131                0.7650553239,
132                1.0,
133            ],
134            &[
135                0.06666666667,
136                0.3784749562,
137                0.5548583770,
138                0.5548583770,
139                0.3784749562,
140                0.06666666667,
141            ],
142        ),
143        _ => panic!("BeamIntegration::Lobatto supports 2-6 points, got {points}"),
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn points_are_symmetric_and_weights_sum_to_one() {
153        for points in 2..=6 {
154            for integration in [
155                BeamIntegration::Legendre { points },
156                BeamIntegration::Lobatto { points },
157            ] {
158                let pts = integration.points();
159                assert_eq!(pts.len(), points);
160                let sum: f64 = pts.iter().map(|(_, w)| w).sum();
161                // 1e-8, not tighter: OpenSees's own source literals for
162                // higher-order Lobatto weights are truncated to ~10
163                // significant digits (e.g. `0.06666666667`), not full f64
164                // precision.
165                assert!(
166                    (sum - 1.0).abs() < 1e-8,
167                    "{integration:?} weights should sum to 1, got {sum}"
168                );
169            }
170        }
171    }
172
173    #[test]
174    fn lobatto_includes_the_endpoints() {
175        let pts = BeamIntegration::Lobatto { points: 4 }.points();
176        assert!((pts.first().unwrap().0 - 0.0).abs() < 1e-12);
177        assert!((pts.last().unwrap().0 - 1.0).abs() < 1e-12);
178    }
179}