|
| 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