Skip to content

Commit 79e2fdc

Browse files
fix(lqr): forward MaxIterations through Lqr and IntegralStateFeedbackLqi (#279)
Systems sampled at high frequencies produce discrete poles near the unit circle that require far more than the default 300 DARE iterations to converge. Without a way to raise the cap, Lqr hard-asserted on the converged=false result regardless of whether the caller could tolerate a slower solve. Add MaxIterations = 300 as a fourth template parameter to Lqr and a fifth to IntegralStateFeedbackLqi, forwarding it to DiscreteAlgebraicRiccatiEquation. All existing call sites are unaffected by the defaulted parameter. Closes #274. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d89092b commit 79e2fdc

5 files changed

Lines changed: 62 additions & 36 deletions

File tree

‎doc/controllers/IntegralStateFeedbackLqi.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ Consider a scalar plant ($n=1$, $m=1$, $p=1$, $T_s = 0.01$):
8282
- **Sample-time mismatch**: the discrete integral $x_i$ accumulates $T_s$-scaled errors.
8383
Using the wrong $T_s$ at run time shifts the effective integral gain and breaks zero-error
8484
convergence.
85+
- **Slow DARE convergence**: the augmented plant $(A_a, B_a)$ has eigenvalues at exactly 1, and at high sampling rates those eigenvalues approach 1 from many directions, requiring many more DARE iterations than the default 300. Raise the cap via the `MaxIterations` template parameter: `IntegralStateFeedbackLqi<float, StateSize, InputSize, OutputSize, 30000>`.
8586

8687
## Variants & Generalizations
8788

‎doc/controllers/Lqr.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ $$Q = \begin{bmatrix} 100 & 0 \\ 0 & 1 \end{bmatrix}, \quad R = [1]$$
8787
- **Q/R scaling.** Only the *ratio* of $Q$ to $R$ matters. Scaling both by the same factor does not change $K$.
8888
- **Full state required.** LQR assumes all states are measured. If only partial measurements are available, combine LQR with a [Kalman Filter](../filters/active/KalmanFilter.md) to form an LQG controller.
8989
- **Fixed-point limitations.** The DARE involves matrix inversions and multiplications that can overflow Q15/Q31 ranges. Prefer floating-point for the offline solve; use the pre-computed $K$ constructor for fixed-point runtime.
90+
- **Slow DARE convergence.** Systems with eigenvalues near the unit circle (e.g., high-frequency sampling, integrating plants) may require far more than the default 300 iterations to converge. Use the `MaxIterations` template parameter to raise the cap: `Lqr<float, StateSize, InputSize, 30000>`. A hard-assert fires if the solver does not converge within the specified limit.
9091

9192
## Variants & Generalizations
9293

‎numerical/controllers/implementations/IntegralStateFeedbackLqi.hpp‎

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
namespace controllers
1313
{
14-
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize>
14+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize, std::size_t MaxIterations = 300>
1515
class IntegralStateFeedbackLqi
1616
{
1717
static_assert(std::is_floating_point_v<T>, "IntegralStateFeedbackLqi supports floating-point types");
@@ -53,16 +53,16 @@ namespace controllers
5353

5454
// Implementation //
5555

56-
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize>
57-
IntegralStateFeedbackLqi<T, StateSize, InputSize, OutputSize>::IntegralStateFeedbackLqi(
56+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize, std::size_t MaxIterations>
57+
IntegralStateFeedbackLqi<T, StateSize, InputSize, OutputSize, MaxIterations>::IntegralStateFeedbackLqi(
5858
const GainStateMatrix& kx, const GainIntegralMatrix& ki, T ts)
5959
: gainState{ kx }
6060
, gainIntegral{ ki }
6161
, sampleTime{ ts }
6262
{}
6363

64-
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize>
65-
IntegralStateFeedbackLqi<T, StateSize, InputSize, OutputSize>::IntegralStateFeedbackLqi(
64+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize, std::size_t MaxIterations>
65+
IntegralStateFeedbackLqi<T, StateSize, InputSize, OutputSize, MaxIterations>::IntegralStateFeedbackLqi(
6666
const math::LinearTimeInvariant<T, StateSize, InputSize, OutputSize>& plant,
6767
const math::SquareMatrix<T, AugmentedSize>& Q,
6868
const math::SquareMatrix<T, InputSize>& R,
@@ -78,17 +78,17 @@ namespace controllers
7878
Aa.at(StateSize + r, StateSize + r) = T(1);
7979
Ba.SetBlock(plant.B, 0, 0);
8080

81-
Lqr<T, AugmentedSize, InputSize> lqr{ Aa, Ba, Q, R };
81+
Lqr<T, AugmentedSize, InputSize, MaxIterations> lqr{ Aa, Ba, Q, R };
8282
const auto& Ka = lqr.GetGain();
8383

8484
gainState = Ka.template GetBlock<InputSize, StateSize>(0, 0);
8585
gainIntegral = Ka.template GetBlock<InputSize, OutputSize>(0, StateSize);
8686
}
8787

88-
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize>
88+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize, std::size_t MaxIterations>
8989
OPTIMIZE_FOR_SPEED
90-
typename IntegralStateFeedbackLqi<T, StateSize, InputSize, OutputSize>::InputVector
91-
IntegralStateFeedbackLqi<T, StateSize, InputSize, OutputSize>::ComputeControl(
90+
typename IntegralStateFeedbackLqi<T, StateSize, InputSize, OutputSize, MaxIterations>::InputVector
91+
IntegralStateFeedbackLqi<T, StateSize, InputSize, OutputSize, MaxIterations>::ComputeControl(
9292
const StateVector& x,
9393
const OutputVector& reference,
9494
const OutputVector& measured)
@@ -98,22 +98,22 @@ namespace controllers
9898
return (gainState * x + gainIntegral * integral) * T(-1);
9999
}
100100

101-
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize>
102-
void IntegralStateFeedbackLqi<T, StateSize, InputSize, OutputSize>::Reset()
101+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize, std::size_t MaxIterations>
102+
void IntegralStateFeedbackLqi<T, StateSize, InputSize, OutputSize, MaxIterations>::Reset()
103103
{
104104
integral = IntegralVector{};
105105
}
106106

107-
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize>
108-
const typename IntegralStateFeedbackLqi<T, StateSize, InputSize, OutputSize>::GainStateMatrix&
109-
IntegralStateFeedbackLqi<T, StateSize, InputSize, OutputSize>::GetGainState() const
107+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize, std::size_t MaxIterations>
108+
const typename IntegralStateFeedbackLqi<T, StateSize, InputSize, OutputSize, MaxIterations>::GainStateMatrix&
109+
IntegralStateFeedbackLqi<T, StateSize, InputSize, OutputSize, MaxIterations>::GetGainState() const
110110
{
111111
return gainState;
112112
}
113113

114-
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize>
115-
const typename IntegralStateFeedbackLqi<T, StateSize, InputSize, OutputSize>::GainIntegralMatrix&
116-
IntegralStateFeedbackLqi<T, StateSize, InputSize, OutputSize>::GetGainIntegral() const
114+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize, std::size_t MaxIterations>
115+
const typename IntegralStateFeedbackLqi<T, StateSize, InputSize, OutputSize, MaxIterations>::GainIntegralMatrix&
116+
IntegralStateFeedbackLqi<T, StateSize, InputSize, OutputSize, MaxIterations>::GetGainIntegral() const
117117
{
118118
return gainIntegral;
119119
}

‎numerical/controllers/implementations/Lqr.hpp‎

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
namespace controllers
1515
{
16-
template<typename T, std::size_t StateSize, std::size_t InputSize>
16+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t MaxIterations = 300>
1717
class Lqr
1818
: public StateFeedbackController<T, StateSize, InputSize>
1919
{
@@ -45,12 +45,12 @@ namespace controllers
4545
bool riccatiSolutionAvailable = false;
4646
};
4747

48-
template<typename T, std::size_t StateSize, std::size_t InputSize>
49-
Lqr<T, StateSize, InputSize>::Lqr(
48+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t MaxIterations>
49+
Lqr<T, StateSize, InputSize, MaxIterations>::Lqr(
5050
const StateMatrix& A, const InputMatrix& B, const StateMatrix& Q, const InputWeightMatrix& R)
5151
: riccatiSolution([&A, &B, &Q, &R]
5252
{
53-
auto r = solvers::DiscreteAlgebraicRiccatiEquation<T, StateSize, InputSize>{}.Solve(A, B, Q, R);
53+
auto r = solvers::DiscreteAlgebraicRiccatiEquation<T, StateSize, InputSize, MaxIterations>{}.Solve(A, B, Q, R);
5454
really_assert(r.converged);
5555
return r.value;
5656
}())
@@ -59,43 +59,43 @@ namespace controllers
5959
ComputeGain(A, B, riccatiSolution, R);
6060
}
6161

62-
template<typename T, std::size_t StateSize, std::size_t InputSize>
63-
Lqr<T, StateSize, InputSize>::Lqr(const GainMatrix& precomputedGain)
62+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t MaxIterations>
63+
Lqr<T, StateSize, InputSize, MaxIterations>::Lqr(const GainMatrix& precomputedGain)
6464
: gain(precomputedGain)
6565
{}
6666

67-
template<typename T, std::size_t StateSize, std::size_t InputSize>
67+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t MaxIterations>
6868
OPTIMIZE_FOR_SPEED
69-
typename Lqr<T, StateSize, InputSize>::InputVector
70-
Lqr<T, StateSize, InputSize>::ComputeControl(const StateVector& state)
69+
typename Lqr<T, StateSize, InputSize, MaxIterations>::InputVector
70+
Lqr<T, StateSize, InputSize, MaxIterations>::ComputeControl(const StateVector& state)
7171
{
7272
return gain * state * T(-1.0f);
7373
}
7474

75-
template<typename T, std::size_t StateSize, std::size_t InputSize>
76-
const typename Lqr<T, StateSize, InputSize>::GainMatrix&
77-
Lqr<T, StateSize, InputSize>::GetGain() const
75+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t MaxIterations>
76+
const typename Lqr<T, StateSize, InputSize, MaxIterations>::GainMatrix&
77+
Lqr<T, StateSize, InputSize, MaxIterations>::GetGain() const
7878
{
7979
return gain;
8080
}
8181

82-
template<typename T, std::size_t StateSize, std::size_t InputSize>
83-
const typename Lqr<T, StateSize, InputSize>::StateMatrix&
84-
Lqr<T, StateSize, InputSize>::GetRiccatiSolution() const
82+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t MaxIterations>
83+
const typename Lqr<T, StateSize, InputSize, MaxIterations>::StateMatrix&
84+
Lqr<T, StateSize, InputSize, MaxIterations>::GetRiccatiSolution() const
8585
{
8686
really_assert(riccatiSolutionAvailable);
8787
return riccatiSolution;
8888
}
8989

90-
template<typename T, std::size_t StateSize, std::size_t InputSize>
91-
Lqr<T, StateSize, InputSize>::Lqr(
90+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t MaxIterations>
91+
Lqr<T, StateSize, InputSize, MaxIterations>::Lqr(
9292
const math::LinearTimeInvariant<T, StateSize, InputSize>& plant,
9393
const StateMatrix& Q, const InputWeightMatrix& R)
9494
: Lqr(plant.A, plant.B, Q, R)
9595
{}
9696

97-
template<typename T, std::size_t StateSize, std::size_t InputSize>
98-
OPTIMIZE_FOR_SPEED void Lqr<T, StateSize, InputSize>::ComputeGain(
97+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t MaxIterations>
98+
OPTIMIZE_FOR_SPEED void Lqr<T, StateSize, InputSize, MaxIterations>::ComputeGain(
9999
const StateMatrix& A, const InputMatrix& B, const StateMatrix& P, const InputWeightMatrix& R)
100100
{
101101
auto BtP = B.Transpose() * P;

‎numerical/controllers/implementations/test/TestLqr.cpp‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,30 @@ TEST_F(TestLqr, lti_constructor_produces_identical_gain_to_matrix_constructor)
145145
EXPECT_NEAR(lqrMat.GetGain().at(0, 1), lqrLti.GetGain().at(0, 1), math::Tolerance<float>());
146146
}
147147

148+
TEST_F(TestLqr, high_frequency_plant_converges_with_raised_iteration_cap)
149+
{
150+
math::SquareMatrix<float, 2> A{
151+
{ 1.0f, 0.001f },
152+
{ 0.0f, 1.0f }
153+
};
154+
math::Matrix<float, 2, 1> B{
155+
{ 0.0f },
156+
{ 0.001f }
157+
};
158+
math::SquareMatrix<float, 2> Q{
159+
{ 10.0f, 0.0f },
160+
{ 0.0f, 1.0f }
161+
};
162+
math::SquareMatrix<float, 1> R{ { 0.1f } };
163+
164+
controllers::Lqr<float, 2, 1, 30000> lqr{ A, B, Q, R };
165+
166+
EXPECT_NEAR(lqr.GetGain().at(0, 0), 9.972051f, 0.1f);
167+
EXPECT_NEAR(lqr.GetGain().at(0, 1), 5.472012f, 0.1f);
168+
EXPECT_NEAR(lqr.GetRiccatiSolution().at(0, 0), 5487.013672f, 5.0f);
169+
EXPECT_NEAR(lqr.GetRiccatiSolution().at(1, 1), 549.203735f, 1.0f);
170+
}
171+
148172
TEST_F(TestLqr, two_instances_with_same_matrices_produce_identical_gains)
149173
{
150174
controllers::Lqr<float, 2, 1> lqr1{ A2, B2, Q2, R2 };

0 commit comments

Comments
 (0)