Skip to main content

carapace_core/model/materials/
steel02.rs

1use super::Material;
2
3/// `Steel02`'s `kon` history flag — `Initial` (0) until the first strain
4/// increment, then `Positive`/`Negative` (1/2) for which asymptote the
5/// current Menegotto-Pinto curve is heading toward. OpenSees also has a
6/// `kon == 3` "pinned to `sigini`" state, not ported since `Material`
7/// doesn't support `Steel02`'s optional initial-stress offset.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum Steel02Kon {
10    Initial,
11    Positive,
12    Negative,
13}
14
15impl Material {
16    /// `sigini` (OpenSees' optional initial-stress offset) is not
17    /// supported — see the variant's doc comment. Ten parameters is
18    /// `Steel02`'s actual OpenSees signature (minus `sigini`/`density`),
19    /// not something to trim for its own sake.
20    #[allow(clippy::too_many_arguments)]
21    pub fn steel02(
22        fy: f64,
23        e0: f64,
24        b: f64,
25        r0: f64,
26        cr1: f64,
27        cr2: f64,
28        a1: f64,
29        a2: f64,
30        a3: f64,
31        a4: f64,
32    ) -> Self {
33        let epsy = fy / e0;
34        Material::Steel02 {
35            fy,
36            e0,
37            b,
38            r0,
39            cr1,
40            cr2,
41            a1,
42            a2,
43            a3,
44            a4,
45            min_strain: -epsy,
46            max_strain: epsy,
47            pl_strain: 0.0,
48            asymptote_strain: 0.0,
49            asymptote_stress: 0.0,
50            reversal_strain: 0.0,
51            reversal_stress: 0.0,
52            kon: Steel02Kon::Initial,
53            strain: 0.0,
54            stress: 0.0,
55        }
56    }
57}
58
59/// Ported from `Steel02::setTrialStrain` (`Steel02.cpp`). OpenSees'
60/// `sigini`-pinned quick return (`kon == 0 || kon == 3` with `|dStrain|`
61/// under a `DBL_EPSILON`-scale tolerance) is not ported — it exists to
62/// let the material sit at a nonzero initial stress before any strain
63/// increment, a feature `Material::steel02` doesn't expose. Letting a
64/// zero (or tiny) `dstrain` fall through the general formula instead is
65/// safe here for the same reason it is in `steel01::evaluate`/
66/// `concrete01::evaluate`: plugging `strain == self.strain` back in is
67/// idempotent, since the committed state was itself produced by this
68/// same formula.
69pub(super) fn evaluate(m: &Material, strain: f64) -> (f64, f64, Material) {
70    let Material::Steel02 {
71        fy,
72        e0,
73        b,
74        r0,
75        cr1,
76        cr2,
77        a1,
78        a2,
79        a3,
80        a4,
81        min_strain,
82        max_strain,
83        pl_strain,
84        asymptote_strain,
85        asymptote_stress,
86        reversal_strain,
87        reversal_stress,
88        kon,
89        strain: cstrain,
90        stress: cstress,
91    } = m
92    else {
93        unreachable!()
94    };
95    let (fy, e0, b, r0, cr1, cr2, a1, a2, a3, a4) =
96        (*fy, *e0, *b, *r0, *cr1, *cr2, *a1, *a2, *a3, *a4);
97    let (mut min_strain, mut max_strain, mut pl_strain) = (*min_strain, *max_strain, *pl_strain);
98    let (mut asymptote_strain, mut asymptote_stress) = (*asymptote_strain, *asymptote_stress);
99    let (mut reversal_strain, mut reversal_stress) = (*reversal_strain, *reversal_stress);
100    let mut kon = *kon;
101    let (cstrain, cstress) = (*cstrain, *cstress);
102
103    let esh = b * e0;
104    let epsy = fy / e0;
105    let dstrain = strain - cstrain;
106
107    if kon == Steel02Kon::Initial {
108        max_strain = epsy;
109        min_strain = -epsy;
110        if dstrain < 0.0 {
111            kon = Steel02Kon::Negative;
112            asymptote_strain = min_strain;
113            asymptote_stress = -fy;
114            pl_strain = min_strain;
115        } else {
116            kon = Steel02Kon::Positive;
117            asymptote_strain = max_strain;
118            asymptote_stress = fy;
119            pl_strain = max_strain;
120        }
121    }
122
123    // Load reversal: recompute the intersection of the elastic and
124    // strain-hardening asymptotes, shifted by the isotropic-hardening
125    // factor (a1/a2 compression side, a3/a4 tension side) — see
126    // `Steel02.cpp`'s comments on the corresponding branches.
127    if kon == Steel02Kon::Negative && dstrain > 0.0 {
128        kon = Steel02Kon::Positive;
129        reversal_strain = cstrain;
130        reversal_stress = cstress;
131        if cstrain < min_strain {
132            min_strain = cstrain;
133        }
134        let d1 = (max_strain - min_strain) / (2.0 * (a4 * epsy));
135        let shift = 1.0 + a3 * d1.powf(0.8);
136        asymptote_strain =
137            (fy * shift - esh * epsy * shift - reversal_stress + e0 * reversal_strain) / (e0 - esh);
138        asymptote_stress = fy * shift + esh * (asymptote_strain - epsy * shift);
139        pl_strain = max_strain;
140    } else if kon == Steel02Kon::Positive && dstrain < 0.0 {
141        kon = Steel02Kon::Negative;
142        reversal_strain = cstrain;
143        reversal_stress = cstress;
144        if cstrain > max_strain {
145            max_strain = cstrain;
146        }
147        let d1 = (max_strain - min_strain) / (2.0 * (a2 * epsy));
148        let shift = 1.0 + a1 * d1.powf(0.8);
149        asymptote_strain = (-fy * shift + esh * epsy * shift - reversal_stress
150            + e0 * reversal_strain)
151            / (e0 - esh);
152        asymptote_stress = -fy * shift + esh * (asymptote_strain + epsy * shift);
153        pl_strain = min_strain;
154    }
155
156    // Menegotto-Pinto transition curve between the reversal point
157    // `(reversal_strain, reversal_stress)` and the asymptote
158    // intersection `(asymptote_strain, asymptote_stress)`; `xi` (the
159    // normalized plastic excursion) softens the curvature `r` further
160    // from `r0` the larger the excursion has been.
161    let xi = ((pl_strain - asymptote_strain) / epsy).abs();
162    let r = r0 * (1.0 - (cr1 * xi) / (cr2 + xi));
163    let eps_ratio = (strain - reversal_strain) / (asymptote_strain - reversal_strain);
164    let dum1 = 1.0 + eps_ratio.abs().powf(r);
165    let dum2 = dum1.powf(1.0 / r);
166
167    let stress = (b * eps_ratio + (1.0 - b) * eps_ratio / dum2)
168        * (asymptote_stress - reversal_stress)
169        + reversal_stress;
170    let tangent = (b + (1.0 - b) / (dum1 * dum2)) * (asymptote_stress - reversal_stress)
171        / (asymptote_strain - reversal_strain);
172
173    (
174        stress,
175        tangent,
176        Material::Steel02 {
177            fy,
178            e0,
179            b,
180            r0,
181            cr1,
182            cr2,
183            a1,
184            a2,
185            a3,
186            a4,
187            min_strain,
188            max_strain,
189            pl_strain,
190            asymptote_strain,
191            asymptote_stress,
192            reversal_strain,
193            reversal_stress,
194            kon,
195            strain,
196            stress,
197        },
198    )
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn steel02_gives_exact_initial_tangent_at_zero_strain() {
207        let m = Material::steel02(60.0, 29000.0, 0.01, 18.5, 0.925, 0.15, 0.9, 5.0, 0.9, 5.0);
208        // At strain=reversal_strain=0 the Menegotto-Pinto curve's
209        // eps_ratio is exactly 0 regardless of R, so this must be exact,
210        // not just approximately E0.
211        assert_eq!(m.trial_stress_tangent(0.0), (0.0, 29000.0));
212    }
213
214    #[test]
215    fn steel02_tangent_approaches_hardening_slope_far_past_yield() {
216        let m = Material::steel02(60.0, 29000.0, 0.01, 18.5, 0.925, 0.15, 0.9, 5.0, 0.9, 5.0);
217        let esh = 0.01 * 29000.0;
218        // Far enough past the yield asymptote (epsy ≈ 0.00207) that the
219        // Menegotto-Pinto curve has converged onto the strain-hardening
220        // line.
221        let (_stress, tangent) = m.trial_stress_tangent(0.05);
222        assert!(
223            (tangent - esh).abs() < 1.0,
224            "expected tangent near Esh={esh}, got {tangent}"
225        );
226    }
227
228    #[test]
229    fn steel02_reversal_starts_out_near_elastic_stiffness() {
230        let m = Material::steel02(60.0, 29000.0, 0.01, 18.5, 0.925, 0.15, 0.9, 5.0, 0.9, 5.0);
231        // Push well past yield and commit, then reverse by a small strain
232        // step — right after a reversal the curve is close to its elastic
233        // (E0) asymptote, unlike deep into either the loading or
234        // hardening branch.
235        let m = m.commit(0.02);
236        let (_stress, tangent) = m.trial_stress_tangent(0.0199);
237        assert!(
238            tangent > 0.9 * 29000.0,
239            "expected near-elastic unloading tangent, got {tangent}"
240        );
241    }
242
243    #[test]
244    fn steel02_tracks_peak_strain_across_a_reversal() {
245        let m = Material::steel02(60.0, 29000.0, 0.01, 18.5, 0.925, 0.15, 0.9, 5.0, 0.9, 5.0);
246        let m = m.commit(0.02);
247        let m = m.commit(-0.01);
248        let Material::Steel02 { max_strain, .. } = m else {
249            panic!()
250        };
251        // max_strain should have latched onto the 0.02 peak on reversal,
252        // not stayed at the virgin epsy ≈ 0.00207.
253        assert!((max_strain - 0.02).abs() < 1e-9, "got {max_strain}");
254    }
255}