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}