Skip to content

Commit e5c7383

Browse files
baggepinnenclaude
andcommitted
Add stabilization and setpoint-tracking tests for all models
Add test/test_stabilization.jl covering every closed-loop model (continuous and discrete: angle, cascade, cascade + feed-forward). Each model is simulated through the DyadInterface.TransientAnalysis path and checked to confirm the controller actually works, not just that the solve succeeds: - the body tilt angle stays bounded (the robot never falls over) and settles back to ~0 over the final second (the inner loop reaches the upright setpoint); - for the cascade models, the wheel position tracks the filtered position reference at the end of the run (the outer loop reaches its setpoint). Tolerances are set with comfortable margin over the observed behavior (max|theta| <= 0.12 vs bound 0.2; settled |theta| <= 0.005 vs 0.02; position tracking error <= 0.008 vs 0.02). Wired into test/runtests.jl. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FxiqD9h4podoj5e4tpLeGk
1 parent 11635c2 commit e5c7383

2 files changed

Lines changed: 84 additions & 0 deletions

File tree

test/runtests.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,6 @@ end
3030
end
3131

3232
end
33+
34+
# Self-contained (defines its own `using`s and `@testset`), so include at top level.
35+
include("test_stabilization.jl")

test/test_stabilization.jl

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Closed-loop stabilization and setpoint-tracking tests for every DyadBot model.
2+
#
3+
# Each model is simulated through the same `DyadInterface.TransientAnalysis` path
4+
# the generated case tests use (which applies the SynchToolkit clock pass needed
5+
# by the discrete models). For every model we check that
6+
# * the solver reports success,
7+
# * the controller keeps the robot upright — the body tilt angle stays bounded
8+
# (never falls over) and settles back to ~0, and
9+
# * for the cascade models, the wheel position tracks the filtered position
10+
# reference by the end of the run (the outer loop reaches its setpoint).
11+
12+
using DyadBotComponents
13+
using DiscreteComponents # loads SynchToolkit so the clocked models get the Lustre pass
14+
using DyadInterface
15+
using ModelingToolkit
16+
using Test
17+
18+
# Simulate a model to `stop` and return (model, solution).
19+
function simulate_model(constructor; stop)
20+
model = constructor(; name = :model)
21+
result = TransientAnalysis(; model, alg = DyadInterface.ODEAlg.Auto(),
22+
start = 0.0, stop, abstol = 1e-8, reltol = 1e-8,
23+
automatic_discontinuity_detection = false)
24+
return model, DyadInterface.rebuild_sol(result)
25+
end
26+
27+
succeeded(sol) = Symbol(sol.retcode) == :Success
28+
29+
# Maximum absolute value of a signal over the whole trajectory.
30+
maxabs(sol, acc) = maximum(abs, sol[acc])
31+
32+
# Maximum absolute value of `acc` sampled over the time window [t0, t1].
33+
function maxabs_window(sol, acc, t0, t1)
34+
ts = range(t0, t1; length = 50)
35+
maximum(abs, sol(ts; idxs = acc).u)
36+
end
37+
38+
# Maximum absolute tracking error x - r sampled over the window [t0, t1].
39+
function maxabs_error_window(sol, x, r, t0, t1)
40+
ts = range(t0, t1; length = 50)
41+
maximum(abs, sol(ts; idxs = x).u .- sol(ts; idxs = r).u)
42+
end
43+
44+
# The body tilt angle must never exceed this — well below "fallen over".
45+
const THETA_BOUND = 0.2
46+
# Settled tolerances, evaluated over the final second of the run.
47+
const THETA_SETTLE = 0.02
48+
const POS_TRACK_TOL = 0.02
49+
50+
@testset "Stabilization and setpoint tracking" begin
51+
# Angle-only models: the sole setpoint is the upright pose (theta = 0). The
52+
# wheel position is intentionally uncontrolled and free to drift.
53+
@testset "$(nameof(ctor))" for ctor in (
54+
DyadBotComponents.AngleControlledDyadBot,
55+
DyadBotComponents.DiscreteAngleControlledDyadBot,
56+
)
57+
stop = 5.0
58+
m, sol = simulate_model(ctor; stop)
59+
@test succeeded(sol)
60+
@test maxabs(sol, m.plant.theta) < THETA_BOUND # never falls over
61+
@test maxabs_window(sol, m.plant.theta, stop - 1, stop) < THETA_SETTLE # settles upright
62+
end
63+
64+
# Cascade models: the inner loop holds the robot upright while the outer loop
65+
# drives the wheel position to the filtered square-wave reference.
66+
@testset "$(nameof(ctor))" for (ctor, reffun) in (
67+
(DyadBotComponents.CascadeControlledDyadBot, m -> m.firstorder1.y),
68+
(DyadBotComponents.DiscreteCascadeControlledDyadBot, m -> m.firstorder1.y),
69+
(DyadBotComponents.CascadeFFDyadBot, m -> m.pos_ref.y),
70+
(DyadBotComponents.DiscreteCascadeFFDyadBot, m -> m.pos_ref.y),
71+
)
72+
stop = 20.0
73+
m, sol = simulate_model(ctor; stop)
74+
@test succeeded(sol)
75+
@test maxabs(sol, m.plant.theta) < THETA_BOUND # inner loop keeps it upright
76+
@test maxabs_window(sol, m.plant.theta, stop - 1, stop) < THETA_SETTLE
77+
# Outer loop reaches its setpoint: position tracks the reference at the
78+
# end of the run (the reference has been constant for the last 5 s).
79+
@test maxabs_error_window(sol, m.plant.x, reffun(m), stop - 1, stop) < POS_TRACK_TOL
80+
end
81+
end

0 commit comments

Comments
 (0)