Skip to main content

carapace_core/
testkit.rs

1//! Shared helpers for driving a `DisplacementControl` cyclic protocol on a
2//! single controlled DOF — used by `tests/analysis/cyclic_protocol.rs`
3//! and `examples/cyclic_material.rs` (the openseespy comparison harness in
4//! `comparison/`).
5
6use crate::analysis::{Analysis, Integrator};
7use crate::model::NodeId;
8
9/// One `DisplacementControl` leg via `Analysis::set_integrator` (no
10/// rebuild) targeting an *absolute* displacement — the increment passed to
11/// `DisplacementControl` is computed fresh from wherever the controlled DOF
12/// actually is right now, not assumed from the previous leg's target.
13pub fn leg(analysis: &mut Analysis, node: NodeId, dof: usize, target: f64) -> f64 {
14    let current = analysis.domain().node(node).displacement[dof];
15    analysis.set_integrator(Integrator::DisplacementControl {
16        node,
17        dof,
18        increment: target - current,
19    });
20    analysis
21        .step()
22        .expect("cyclic protocol leg should converge")
23        .load_factor
24}
25
26/// Walks the controlled DOF from wherever it is to `target` in steps no
27/// larger than `step`, via repeated `leg` calls. Calls `on_point` after
28/// every leg with `(displacement, load_factor)` so callers can record a
29/// full trace, not just the endpoint.
30pub fn ramp_to(
31    analysis: &mut Analysis,
32    node: NodeId,
33    dof: usize,
34    target: f64,
35    step: f64,
36    mut on_point: impl FnMut(f64, f64),
37) {
38    loop {
39        let current = analysis.domain().node(node).displacement[dof];
40        let remaining = target - current;
41        if remaining.abs() < 1e-12 {
42            return;
43        }
44        let next = current + remaining.clamp(-step, step);
45        let load_factor = leg(analysis, node, dof, next);
46        on_point(next, load_factor);
47    }
48}
49
50/// Runs a reverse-cyclic protocol: for each `(amplitude, cycles)` tier, in
51/// order, ramps 0 -> +amplitude -> -amplitude, `cycles` times, then back to
52/// 0 before the next tier. Calls `on_point` after every leg — this is the
53/// full recorded trace a stress-strain (or force-displacement) plot is
54/// built from.
55pub fn run_cyclic_protocol(
56    analysis: &mut Analysis,
57    node: NodeId,
58    dof: usize,
59    tiers: &[(f64, usize)],
60    step: f64,
61    mut on_point: impl FnMut(f64, f64),
62) {
63    for &(amplitude, cycles) in tiers {
64        for _ in 0..cycles {
65            ramp_to(analysis, node, dof, amplitude, step, &mut on_point);
66            ramp_to(analysis, node, dof, -amplitude, step, &mut on_point);
67        }
68        ramp_to(analysis, node, dof, 0.0, step, &mut on_point);
69    }
70}