Skip to main content

carapace_core/model/materials/
elastic_like.rs

1use super::Material;
2
3impl Material {
4    pub fn elastic_pp(e: f64, eyp: f64) -> Self {
5        Material::ElasticPP { e, eyp, ep: 0.0 }
6    }
7}
8
9pub(super) fn evaluate_elastic(e: f64, strain: f64) -> (f64, f64, Material) {
10    (e * strain, e, Material::Elastic { e })
11}
12
13pub(super) fn evaluate_elastic_pp(e: f64, eyp: f64, ep: f64, strain: f64) -> (f64, f64, Material) {
14    let yield_stress = e * eyp;
15    // Elastic predictor relative to the *committed* plastic
16    // strain — standard 1D return mapping.
17    let trial_stress = e * (strain - ep);
18    // Deliberately `<=` yield: OpenSees' `fYieldSurface = -E * DBL_EPSILON`
19    // slack makes a trial exactly on the surface plastic (tangent 0), which
20    // sends Newton oscillating between the two kinks when unloading from a
21    // yielded state (OpenSees itself fails the
22    // unload-from-yield case in `core/tests/materials/state.rs`). Elastic on the surface converges.
23    if trial_stress.abs() <= yield_stress {
24        (trial_stress, e, Material::ElasticPP { e, eyp, ep })
25    } else {
26        let stress = yield_stress.copysign(trial_stress);
27        // Return to the yield surface: the plastic strain
28        // absorbs whatever additional strain the elastic
29        // predictor overshot by.
30        let next_ep = strain - stress / e;
31        (
32            stress,
33            0.0,
34            Material::ElasticPP {
35                e,
36                eyp,
37                ep: next_ep,
38            },
39        )
40    }
41}
42
43pub(super) fn evaluate_gap(e: f64, gap: f64, strain: f64) -> (f64, f64, Material) {
44    // `<=`, not `<`: at exactly zero closure the tangent must
45    // stay nonzero (matching the just-closed regime) or a
46    // system starting from zero displacement forms a singular
47    // initial tangent — there is no elastic-vs-open distinction
48    // to make at a single point anyway.
49    let closure = strain + gap;
50    if closure <= 0.0 {
51        (e * closure, e, Material::Gap { e, gap })
52    } else {
53        (0.0, 0.0, Material::Gap { e, gap })
54    }
55}
56
57pub(super) fn evaluate_ent(e: f64, strain: f64) -> (f64, f64, Material) {
58    // See `evaluate_gap` above for why this is `<=`, not `<`.
59    if strain <= 0.0 {
60        (e * strain, e, Material::Ent { e })
61    } else {
62        (0.0, 0.0, Material::Ent { e })
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn elastic_pp_clamps_symmetrically_on_first_loading() {
72        // First loading from ep=0: identical to the old reversible-envelope
73        // behavior, since nothing has been committed yet to make history
74        // matter.
75        let m = Material::elastic_pp(100.0, 0.01);
76        assert_eq!(m.trial_stress_tangent(0.005), (0.5, 100.0));
77        assert_eq!(m.trial_stress_tangent(0.02), (1.0, 0.0));
78        assert_eq!(m.trial_stress_tangent(-0.02), (-1.0, 0.0));
79    }
80
81    #[test]
82    fn elastic_pp_remembers_permanent_set_after_commit() {
83        let m = Material::elastic_pp(100.0, 0.01);
84
85        // Load past yield (strain=0.02 well beyond eyp=0.01) and commit.
86        let m = m.commit(0.02);
87        let Material::ElasticPP { ep, .. } = m else {
88            panic!()
89        };
90        assert!((ep - 0.01).abs() < 1e-12, "expected ep=0.01, got {ep}");
91
92        // Partially unload to strain=0.015 (still positive, but now less
93        // than the peak). A stateless envelope would still clamp this to
94        // the yield force (|100*0.015|=1.5 > fy=1.0), but real plasticity
95        // unloads *elastically* from the committed ep, not back through
96        // the origin: trial_stress = e*(strain-ep) = 100*(0.015-0.01)=0.5,
97        // well inside the yield surface.
98        let (stress, tangent) = m.trial_stress_tangent(0.015);
99        assert!(
100            (stress - 0.5).abs() < 1e-12,
101            "expected elastic unload stress=0.5, got {stress}"
102        );
103        assert_eq!(
104            tangent, 100.0,
105            "should be elastic (unloading), not the plastic tangent"
106        );
107    }
108
109    #[test]
110    fn gap_only_engages_past_closure() {
111        let m = Material::Gap {
112            e: 100.0,
113            gap: 0.01,
114        };
115        assert_eq!(m.trial_stress_tangent(0.0), (0.0, 0.0));
116        assert_eq!(m.trial_stress_tangent(-0.005), (0.0, 0.0));
117        assert_eq!(
118            m.trial_stress_tangent(-0.02),
119            (100.0 * (-0.02 + 0.01), 100.0)
120        );
121    }
122
123    #[test]
124    fn ent_carries_no_tension() {
125        let m = Material::Ent { e: 100.0 };
126        assert_eq!(m.trial_stress_tangent(0.01), (0.0, 0.0));
127        assert_eq!(m.trial_stress_tangent(-0.01), (-1.0, 100.0));
128    }
129}