Describe the bug
For quadratic problems (quadratic objective or quadratic constraints), which are solved via the barrier path, the convergence statistics surfaced through the solution — l2_dual_residual and gap (i.e. Python's get_lp_stats()["dual_residual"] / ["gap"]) — are wrong:
- l2_dual_residual omits the quadratic term. At a converged QP optimum it reports ‖Aᵀy + z − c‖ (the linear stationarity residual) instead of ‖Qx + c − Aᵀy − z‖. The Qx term is dropped, so a fully optimal solution reports a large dual residual.
- gap is a hardcoded 0.0, and primal_objective / dual_objective are both set to the same user_objective, regardless of the actual duality/complementarity gap the barrier computed.
l2_primal_residual and nb_iterations are correct. (For SOCP/quadratic constraints, dual variables are additionally filled with NaN, so any dual-derived stat is meaningless.)
Steps/Code to reproduce bug
// Expected behavior for a converged QP: the reported convergence stats should
// reflect the actual optimality of the solution. This test encodes that
// expectation and currently FAILS, demonstrating the bug.
//
// Minimize x1^2 + 4 x2^2 - 8 x1 - 16 x2 = (1/2) x^T H x + c^T x,
// H = diag(2, 8), c = (-8, -16)
// s.t. x1 + x2 >= 5, 0 <= x1,x2 <= 10.
// Unconstrained optimum (4, 2): H x + c = (0,0), constraint slack (y=0),
// x interior to bounds (z=0). True stationarity residual Q x + c - A^T y - z ~ 0.
TEST(lp_parser_solve, qp_dual_residual_is_wrong)
{
raft::handle_t handle;
auto problem = io::read_lp_from_string<int, double>(R"LP(
Minimize
obj: -8 x1 - 16 x2 + [ 2 x1 ^ 2 + 8 x2 ^ 2 ] / 2
Subject To
c1: x1 + x2 >= 5
Bounds
0 <= x1 <= 10
0 <= x2 <= 10
End
)LP");
auto settings = pdlp_solver_settings_t<int, double>();
auto solution = solve_lp(&handle, problem, settings);
ASSERT_EQ(solution.get_termination_status(), pdlp_termination_status_t::Optimal);
EXPECT_NEAR(solution.get_objective_value(), -32.0, 1e-4);
auto info = solution.get_additional_termination_information();
// Primal side is correct: the solution is genuinely feasible and converged.
EXPECT_NEAR(info.l2_primal_residual, 0.0, 1e-4);
// Expected: at the optimum the true stationarity residual is ~0.
// BUG: reports ||A^T y + z - c|| = ||c|| = 16, dropping the Q x term.
EXPECT_NEAR(info.l2_dual_residual, 0.0, 1e-4);
// Expected: gap is the real primal-dual gap and the objectives are
// independently computed bounds.
// BUG: gap is hardcoded 0.0 and dual_objective is a copy of primal.
EXPECT_NE(info.primal_objective, info.dual_objective)
<< "primal and dual objectives are set to the same value; gap is not computed";
}
Describe the bug
For quadratic problems (quadratic objective or quadratic constraints), which are solved via the barrier path, the convergence statistics surfaced through the solution — l2_dual_residual and gap (i.e. Python's get_lp_stats()["dual_residual"] / ["gap"]) — are wrong:
l2_primal_residual and nb_iterations are correct. (For SOCP/quadratic constraints, dual variables are additionally filled with NaN, so any dual-derived stat is meaningless.)
Steps/Code to reproduce bug