Skip to content

[BUG] get_lp_stats() reports garbage dual residual and objective for QPs and SOCPs #1598

Description

@mlubin

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";
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    awaiting responseThis expects a response from maintainer or contributor depending on who requested in last comment.bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions