Skip to content

Commit 9f8844a

Browse files
improve robust controllers (#243)
1 parent 6b38a83 commit 9f8844a

4 files changed

Lines changed: 398 additions & 1 deletion

File tree

numerical/robust_control/test/TestActiveDisturbanceRejection.cpp

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,37 @@ namespace
2929
}
3030
};
3131

32+
struct FirstOrderPlant
33+
{
34+
float y{ 0.0f };
35+
float bTrue;
36+
37+
explicit FirstOrderPlant(float b)
38+
: bTrue{ b }
39+
{}
40+
41+
float Step(float u, float disturbance = 0.0f)
42+
{
43+
y += kTs * (bTrue * u + disturbance);
44+
return y;
45+
}
46+
};
47+
3248
class TestActiveDisturbanceRejection : public ::testing::Test
3349
{
3450
protected:
3551
robust_control::ActiveDisturbanceRejectionControl<float, 2> adrc{ kWo, kWc, kB0, kTs };
3652
SecondOrderPlant plant{ kB0 };
3753
};
54+
55+
class TestActiveDisturbanceRejectionOrder1 : public ::testing::Test
56+
{
57+
protected:
58+
static constexpr float kWo1{ 20.0f };
59+
static constexpr float kWc1{ 5.0f };
60+
robust_control::ActiveDisturbanceRejectionControl<float, 1> adrc{ kWo1, kWc1, kB0, kTs };
61+
FirstOrderPlant plant{ kB0 };
62+
};
3863
}
3964

4065
TEST_F(TestActiveDisturbanceRejection, bandwidth_gain_mapping)
@@ -136,3 +161,64 @@ TEST_F(TestActiveDisturbanceRejection, near_model_free_robustness)
136161

137162
EXPECT_NEAR(mismatchedPlant.y, reference, 0.1f);
138163
}
164+
165+
TEST_F(TestActiveDisturbanceRejectionOrder1, bandwidth_gain_mapping_order1)
166+
{
167+
const auto og = robust_control::ActiveDisturbanceRejectionControl<float, 1>::ObserverGainFromBandwidth(kWo1);
168+
const auto cg = robust_control::ActiveDisturbanceRejectionControl<float, 1>::ControlGainFromBandwidth(kWc1);
169+
170+
EXPECT_NEAR(og.at(0, 0), 2.0f * kWo1, math::Tolerance<float>());
171+
EXPECT_NEAR(og.at(1, 0), 1.0f * kWo1 * kWo1, math::Tolerance<float>());
172+
173+
EXPECT_NEAR(cg.at(0, 0), 1.0f * kWc1, math::Tolerance<float>());
174+
}
175+
176+
TEST_F(TestActiveDisturbanceRejection, zero_input_zero_output_first_call)
177+
{
178+
const float u = adrc.Compute(0.0f, 0.0f);
179+
EXPECT_NEAR(u, 0.0f, math::Tolerance<float>());
180+
}
181+
182+
TEST_F(TestActiveDisturbanceRejection, long_horizon_bounded_output)
183+
{
184+
const float reference{ 1.0f };
185+
float maxOutput{ 0.0f };
186+
187+
for (int i = 0; i < 20000; ++i)
188+
{
189+
const float u = adrc.Compute(reference, plant.y);
190+
plant.Step(u);
191+
const float absY = std::abs(plant.y);
192+
if (absY > maxOutput)
193+
maxOutput = absY;
194+
}
195+
196+
EXPECT_LT(maxOutput, 10.0f);
197+
EXPECT_FALSE(std::isnan(plant.y));
198+
EXPECT_FALSE(std::isinf(plant.y));
199+
}
200+
201+
TEST_F(TestActiveDisturbanceRejection, reset_mid_run_matches_fresh_instance)
202+
{
203+
const float reference{ 1.0f };
204+
205+
for (int i = 0; i < 500; ++i)
206+
plant.Step(adrc.Compute(reference, plant.y));
207+
208+
adrc.Reset();
209+
plant = SecondOrderPlant{ kB0 };
210+
211+
robust_control::ActiveDisturbanceRejectionControl<float, 2> fresh{ kWo, kWc, kB0, kTs };
212+
SecondOrderPlant freshPlant{ kB0 };
213+
214+
for (int i = 0; i < 200; ++i)
215+
{
216+
const float uReset = adrc.Compute(reference, plant.y);
217+
const float uFresh = fresh.Compute(reference, freshPlant.y);
218+
plant.Step(uReset);
219+
freshPlant.Step(uFresh);
220+
}
221+
222+
EXPECT_FLOAT_EQ(plant.y, freshPlant.y);
223+
EXPECT_FLOAT_EQ(adrc.AppliedPrev(), fresh.AppliedPrev());
224+
}

numerical/robust_control/test/TestDisturbanceObserver.cpp

Lines changed: 156 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// Copyright (c) 2024, Numerical Toolbox Contributors. All rights reserved.
21
#include "numerical/filters/passive/BiquadCascade.hpp"
32
#include "numerical/math/LinearTimeInvariant.hpp"
43
#include "numerical/math/Matrix.hpp"
@@ -26,6 +25,18 @@ namespace
2625
return plant;
2726
}
2827

28+
math::LinearTimeInvariant<float, 2, 2, 2> MakeTwoChannelPlant()
29+
{
30+
math::LinearTimeInvariant<float, 2, 2, 2> plant{};
31+
plant.A.at(0, 0) = kPlantA;
32+
plant.A.at(1, 1) = kPlantA;
33+
plant.B.at(0, 0) = kPlantB;
34+
plant.B.at(1, 1) = kPlantB;
35+
plant.C.at(0, 0) = kPlantC;
36+
plant.C.at(1, 1) = kPlantC;
37+
return plant;
38+
}
39+
2940
filters::passive::BiquadCoeffs<float> MakeLowPassQ()
3041
{
3142
return filters::passive::Biquad<float>::LowPass(kCutoffHz, kSampleRateHz, kQ);
@@ -241,3 +252,147 @@ TEST_F(TestDisturbanceObserver, robust_to_small_model_mismatch)
241252

242253
EXPECT_LT(std::abs(dob.Disturbance().at(0, 0) - kDisturbance), kDisturbance);
243254
}
255+
256+
TEST_F(TestDisturbanceObserver, zero_disturbance_passthrough)
257+
{
258+
static constexpr int kSteps{ 4000 };
259+
260+
math::Vector<float, 1> x{};
261+
math::Vector<float, 1> u{};
262+
263+
math::Vector<float, 1> c{};
264+
c.at(0, 0) = 0.7f;
265+
266+
for (int k{ 0 }; k < kSteps; ++k)
267+
{
268+
const math::Vector<float, 1> y{ nominal.Output(x, u) };
269+
u = dob.Compute(c, y);
270+
x = nominal.Step(x, u);
271+
}
272+
273+
EXPECT_NEAR(u.at(0, 0), c.at(0, 0), 1e-2f);
274+
}
275+
276+
TEST_F(TestDisturbanceObserver, initial_disturbance_is_zero)
277+
{
278+
EXPECT_NEAR(dob.Disturbance().at(0, 0), 0.0f, math::Tolerance<float>());
279+
}
280+
281+
TEST_F(TestDisturbanceObserver, reset_then_reconverge)
282+
{
283+
static constexpr float kDisturbance{ 0.5f };
284+
static constexpr int kWarmup{ 4000 };
285+
286+
math::Vector<float, 1> x{};
287+
math::Vector<float, 1> c{};
288+
math::Vector<float, 1> u{};
289+
290+
for (int k{ 0 }; k < kWarmup; ++k)
291+
{
292+
const math::Vector<float, 1> y{ nominal.Output(x, u) };
293+
u = dob.Compute(c, y);
294+
math::Vector<float, 1> uActual{};
295+
uActual.at(0, 0) = u.at(0, 0) + kDisturbance;
296+
x = nominal.Step(x, uActual);
297+
}
298+
299+
dob.Reset();
300+
x = math::Vector<float, 1>{};
301+
u = math::Vector<float, 1>{};
302+
303+
for (int k{ 0 }; k < kWarmup; ++k)
304+
{
305+
const math::Vector<float, 1> y{ nominal.Output(x, u) };
306+
u = dob.Compute(c, y);
307+
math::Vector<float, 1> uActual{};
308+
uActual.at(0, 0) = u.at(0, 0) + kDisturbance;
309+
x = nominal.Step(x, uActual);
310+
}
311+
312+
EXPECT_NEAR(dob.Disturbance().at(0, 0), kDisturbance, 1e-2f);
313+
}
314+
315+
TEST_F(TestDisturbanceObserver, determinism_two_instances_agree)
316+
{
317+
static constexpr float kDisturbance{ 0.3f };
318+
static constexpr int kSteps{ 2000 };
319+
320+
robust_control::DisturbanceObserver<float, 1, 1, 1> dob2{ nominal, qCoeffs };
321+
322+
math::Vector<float, 1> x1{};
323+
math::Vector<float, 1> x2{};
324+
math::Vector<float, 1> c{};
325+
math::Vector<float, 1> u1{};
326+
math::Vector<float, 1> u2{};
327+
328+
for (int k{ 0 }; k < kSteps; ++k)
329+
{
330+
const math::Vector<float, 1> y1{ nominal.Output(x1, u1) };
331+
const math::Vector<float, 1> y2{ nominal.Output(x2, u2) };
332+
u1 = dob.Compute(c, y1);
333+
u2 = dob2.Compute(c, y2);
334+
math::Vector<float, 1> ua1{};
335+
math::Vector<float, 1> ua2{};
336+
ua1.at(0, 0) = u1.at(0, 0) + kDisturbance;
337+
ua2.at(0, 0) = u2.at(0, 0) + kDisturbance;
338+
x1 = nominal.Step(x1, ua1);
339+
x2 = nominal.Step(x2, ua2);
340+
}
341+
342+
EXPECT_FLOAT_EQ(dob.Disturbance().at(0, 0), dob2.Disturbance().at(0, 0));
343+
}
344+
345+
TEST_F(TestDisturbanceObserver, two_channel_independent_disturbance_estimation)
346+
{
347+
static constexpr float kDist0{ 0.4f };
348+
static constexpr float kDist1{ 0.8f };
349+
static constexpr int kSteps{ 4000 };
350+
351+
const math::LinearTimeInvariant<float, 2, 2, 2> plant2ch{ MakeTwoChannelPlant() };
352+
filters::passive::BiquadCoeffs<float> q2{ MakeLowPassQ() };
353+
robust_control::DisturbanceObserver<float, 2, 2, 2> dob2ch{ plant2ch, q2 };
354+
355+
math::Vector<float, 2> x{};
356+
math::Vector<float, 2> c{};
357+
math::Vector<float, 2> u{};
358+
359+
for (int k{ 0 }; k < kSteps; ++k)
360+
{
361+
const math::Vector<float, 2> y{ plant2ch.Output(x, u) };
362+
u = dob2ch.Compute(c, y);
363+
math::Vector<float, 2> uActual{};
364+
uActual.at(0, 0) = u.at(0, 0) + kDist0;
365+
uActual.at(1, 0) = u.at(1, 0) + kDist1;
366+
x = plant2ch.Step(x, uActual);
367+
}
368+
369+
EXPECT_NEAR(dob2ch.Disturbance().at(0, 0), kDist0, 1e-2f);
370+
EXPECT_NEAR(dob2ch.Disturbance().at(1, 0), kDist1, 1e-2f);
371+
}
372+
373+
TEST_F(TestDisturbanceObserver, step_disturbance_transient_crosses_half_value)
374+
{
375+
static constexpr float kDisturbance{ 1.0f };
376+
static constexpr int kMaxTransientSteps{ 500 };
377+
378+
math::Vector<float, 1> x{};
379+
math::Vector<float, 1> c{};
380+
math::Vector<float, 1> u{};
381+
382+
bool crossedHalf{ false };
383+
for (int k{ 0 }; k < kMaxTransientSteps; ++k)
384+
{
385+
const math::Vector<float, 1> y{ nominal.Output(x, u) };
386+
u = dob.Compute(c, y);
387+
math::Vector<float, 1> uActual{};
388+
uActual.at(0, 0) = u.at(0, 0) + kDisturbance;
389+
x = nominal.Step(x, uActual);
390+
if (dob.Disturbance().at(0, 0) >= 0.5f * kDisturbance)
391+
{
392+
crossedHalf = true;
393+
break;
394+
}
395+
}
396+
397+
EXPECT_TRUE(crossedHalf);
398+
}

numerical/robust_control/test/TestHInfinityStateFeedback.cpp

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,18 @@ namespace
3737
Plant plant{ MakeGeneralizedPlant() };
3838
HInf hinf{ plant };
3939
};
40+
41+
class TestHInfinityStateFeedbackSynthesized : public ::testing::Test
42+
{
43+
protected:
44+
Plant plant{ MakeGeneralizedPlant() };
45+
HInf hinf{ plant };
46+
47+
void SetUp() override
48+
{
49+
hinf.Synthesize(0.5f, 100.0f, 1e-3f);
50+
}
51+
};
4052
}
4153

4254
TEST_F(TestHInfinityStateFeedback, reduces_to_lqr_as_gamma_large)
@@ -205,3 +217,84 @@ TEST_F(TestHInfinityStateFeedback, compute_control_is_negative_feedback)
205217
const float expected = -(K.at(0, 0) * x.at(0, 0) + K.at(0, 1) * x.at(1, 0));
206218
EXPECT_NEAR(u.at(0, 0), expected, math::Tolerance<float>());
207219
}
220+
221+
TEST_F(TestHInfinityStateFeedbackSynthesized, zero_state_produces_zero_control)
222+
{
223+
const math::Vector<float, 2> x{};
224+
const auto u = hinf.ComputeControl(x);
225+
EXPECT_NEAR(u.at(0, 0), 0.0f, math::Tolerance<float>());
226+
}
227+
228+
TEST_F(TestHInfinityStateFeedbackSynthesized, closed_loop_state_converges_from_nonzero_ic)
229+
{
230+
const auto& K = hinf.Gain();
231+
auto clA = plant.A - plant.B2 * K;
232+
233+
math::Vector<float, 2> x{};
234+
x.at(0, 0) = 1.0f;
235+
x.at(1, 0) = 0.0f;
236+
237+
for (int step = 0; step < 200; ++step)
238+
x = clA * x;
239+
240+
EXPECT_NEAR(x.at(0, 0), 0.0f, 1e-2f);
241+
EXPECT_NEAR(x.at(1, 0), 0.0f, 1e-2f);
242+
}
243+
244+
TEST_F(TestHInfinityStateFeedbackSynthesized, compute_control_is_homogeneous)
245+
{
246+
math::Vector<float, 2> x{};
247+
x.at(0, 0) = 0.7f;
248+
x.at(1, 0) = -0.3f;
249+
250+
const float alpha = 3.5f;
251+
math::Vector<float, 2> xScaled{};
252+
xScaled.at(0, 0) = alpha * x.at(0, 0);
253+
xScaled.at(1, 0) = alpha * x.at(1, 0);
254+
255+
const auto u = hinf.ComputeControl(x);
256+
const auto uScaled = hinf.ComputeControl(xScaled);
257+
258+
EXPECT_NEAR(uScaled.at(0, 0), alpha * u.at(0, 0), math::Tolerance<float>());
259+
}
260+
261+
TEST_F(TestHInfinityStateFeedbackSynthesized, synthesize_is_deterministic)
262+
{
263+
const float gamma1 = hinf.Gamma();
264+
const auto K1 = hinf.Gain();
265+
266+
HInf hinf2{ plant };
267+
hinf2.Synthesize(0.5f, 100.0f, 1e-3f);
268+
269+
EXPECT_FLOAT_EQ(hinf2.Gamma(), gamma1);
270+
for (std::size_t c = 0; c < 2; ++c)
271+
EXPECT_FLOAT_EQ(hinf2.Gain().at(0, c), K1.at(0, c));
272+
}
273+
274+
TEST_F(TestHInfinityStateFeedback, synthesize_fails_with_gammamax_too_small)
275+
{
276+
const bool ok = hinf.Synthesize(0.001f, 0.01f, 1e-4f);
277+
EXPECT_FALSE(ok);
278+
}
279+
280+
TEST_F(TestHInfinityStateFeedbackSynthesized, riccati_solution_diagonal_is_positive)
281+
{
282+
constexpr std::size_t AugInputSize = 2;
283+
math::Matrix<float, 2, AugInputSize> B{};
284+
B.at(0, 0) = plant.B2.at(0, 0);
285+
B.at(1, 0) = plant.B2.at(1, 0);
286+
B.at(0, 1) = plant.B1.at(0, 0);
287+
B.at(1, 1) = plant.B1.at(1, 0);
288+
289+
const float g = hinf.Gamma();
290+
math::SquareMatrix<float, AugInputSize> Rtilde{};
291+
Rtilde.at(0, 0) = 1.0f;
292+
Rtilde.at(1, 1) = -(g * g);
293+
294+
auto Q = plant.C1.Transpose() * plant.C1;
295+
solvers::DiscreteAlgebraicRiccatiEquation<float, 2, AugInputSize> dare{};
296+
auto Xref = dare.Solve(plant.A, B, Q, Rtilde);
297+
298+
EXPECT_GT(Xref.at(0, 0), 0.0f);
299+
EXPECT_GT(Xref.at(1, 1), 0.0f);
300+
}

0 commit comments

Comments
 (0)