Severity: high
Domain: controllers
Status: VERIFIED — read + hand-traced against a210d34 on 2026-08-10
Suggested labels: bug, numerical-correctness, controllers
Summary
Mpc::ComputeControl() solves the unconstrained system and then clamps each control element
independently. For a coupled Hessian this does not produce the constrained optimum. There are also
no state or output constraints, despite the documentation presenting constrained MPC.
Location
numerical/controllers/implementations/Mpc.hpp
Evidence
auto uOptimal = solvers::SolveSystem<T, TotalControlDim, 1>(hessian, negG);
ApplyConstraints(uOptimal);
for (std::size_t k = 0; k < ControlHorizon; ++k)
for (std::size_t i = 0; i < InputSize; ++i)
{
auto& val = u.at(k * InputSize + i, 0);
val = std::max(std::min(val, constraints.uMax->at(i, 0)), constraints.uMin->at(i, 0));
}
Each element is clamped in isolation; no active-set re-optimisation occurs.
Trace
H = [[2,1],[1,2]], g = [−4,−2], constraint u₁ ≤ 1:
unconstrained optimum: H⁻¹(−g) = [2, 0]
clipped: [1, 0]
true constrained optimum (active set u₁ = 1):
minimise over u₂ with u₁ fixed at 1 => 2u₂ + 1 = 2 => u₂ = 0.5
[1, 0.5]
Clipping loses the compensation that the remaining free variables should provide.
Why tests do not catch it
The MPC test asserts only that the result lies within [uMin, uMax]. A clipped, non-optimal
sequence satisfies that.
Suggested fix
Implement a bounded active-set box-QP solver (no heap, fixed iteration cap) and use it in place
of SolveSystem + clamp. Add a test comparing against an analytically known constrained optimum
such as the one above.
SolveSystem is also called without a status check — see issue 004.
Notes
Depends on the "Priority 0" box-QP recommendation in the audit report. Related: issue 010 (reference
tracking).
Severity: high
Domain: controllers
Status: VERIFIED — read + hand-traced against
a210d34on 2026-08-10Suggested labels:
bug,numerical-correctness,controllersSummary
Mpc::ComputeControl()solves the unconstrained system and then clamps each control elementindependently. For a coupled Hessian this does not produce the constrained optimum. There are also
no state or output constraints, despite the documentation presenting constrained MPC.
Location
numerical/controllers/implementations/Mpc.hpp
Evidence
Each element is clamped in isolation; no active-set re-optimisation occurs.
Trace
H = [[2,1],[1,2]],g = [−4,−2], constraintu₁ ≤ 1:Clipping loses the compensation that the remaining free variables should provide.
Why tests do not catch it
The MPC test asserts only that the result lies within
[uMin, uMax]. A clipped, non-optimalsequence satisfies that.
Suggested fix
Implement a bounded active-set box-QP solver (no heap, fixed iteration cap) and use it in place
of
SolveSystem+ clamp. Add a test comparing against an analytically known constrained optimumsuch as the one above.
SolveSystemis also called without a status check — see issue 004.Notes
Depends on the "Priority 0" box-QP recommendation in the audit report. Related: issue 010 (reference
tracking).