[BUG] Dual simplex logs an incorrect objective after a bound flip
@chris-maes is already working on this.
Since Sep 13, 2026.
- Dominant language
- Cuda
- Stars
- 1k
- Forks
- 233
- Avg merge
- 4d 4h
- Merged PRs (30d)
- 95
Description
(Agent-written bug report follows.)
Reproduced on commit 28f247d167ec9b18c7f33bf6492681cb03f30e0d using the public C API.
For the LP
minimize x + 2y
subject to x + y = 2
0 ≤ x ≤ 1
y ≥ 0
the optimal solution is x = y = 1, with objective 3. Dual simplex's iteration log reports objective 2 at the final iteration, although the final summary and
returned solution correctly report 3.
Reproduction
#include <cuopt/mathematical_optimization/cuopt_c.h>
#include <cuopt/mathematical_optimization/constants.h>
#include <stdio.h>
#include <stdlib.h>
#define CHECK(call) do { \
cuopt_int_t rc = (call); \
if (rc != CUOPT_SUCCESS) { \
fprintf(stderr, "%s failed: %d\n", #call, rc); \
exit(1); \
} \
} while (0)
int main(void)
{
cuOptOptimizationProblem problem = NULL;
cuOptSolverSettings settings = NULL;
cuOptSolution solution = NULL;
cuopt_int_t offsets[] = {0, 2}, columns[] = {0, 1};
cuopt_float_t values[] = {1, 1}, objective[] = {1, 2};
cuopt_float_t row_lower[] = {2}, row_upper[] = {2};
cuopt_float_t lower[] = {0, 0}, upper[] = {1, CUOPT_INFINITY};
char types[] = {CUOPT_CONTINUOUS, CUOPT_CONTINUOUS};
CHECK(cuOptCreateRangedProblem(
1, 2, CUOPT_MINIMIZE, 0, objective,
offsets, columns, values, row_lower, row_upper,
lower, upper, types, &problem));
CHECK(cuOptCreateSolverSettings(&settings));
CHECK(cuOptSetIntegerParameter(
settings, CUOPT_METHOD, CUOPT_METHOD_DUAL_SIMPLEX));
CHECK(cuOptSetIntegerParameter(settings, CUOPT_PRESOLVE, 0));
CHECK(cuOptSetIntegerParameter(settings, CUOPT_LOG_TO_CONSOLE, 1));
CHECK(cuOptSolve(problem, settings, &solution));
cuopt_int_t status;
cuopt_float_t obj, x[2];
CHECK(cuOptGetTerminationStatus(solution, &status));
CHECK(cuOptGetObjectiveValue(solution, &obj));
CHECK(cuOptGetPrimalSolution(solution, x));
printf("PUBLIC_API_RESULT status=%d x=%.17g y=%.17g "
"objective=%.17g recomputed=%.17g\n",
status, x[0], x[1], obj, x[0] + 2*x[1]);
cuOptDestroySolution(&solution);
cuOptDestroySolverSettings(&settings);
cuOptDestroyProblem(&problem);
return 0;
}
Actual output
Relevant excerpt from a run on an NVIDIA RTX PRO 6000 Blackwell Workstation Edition:
cuOpt version: 26.10.0, git hash: 28f247d1, host arch: x86_64, device archs: 120a-real
Dual Simplex Phase 1
Dual feasible solution found.
Dual Simplex Phase 2
Iter Objective Num Inf. Sum Inf. Perturb Time
0 +0.0000000000000000e+00 1 4.00000000e+00 0.00e+00 0.04
1 +2.0000000000000000e+00 0 0.00000000e+00 1.00e-07 0.04
Optimal solution found in 1 iterations and 0.04s
Objective +3.00000000e+00
Primal infeasibility (abs): 0.00e+00
Dual infeasibility (abs): 0.00e+00
Perturbation: 1.00e-07
Dual simplex finished in 0.04 seconds
PUBLIC_API_RESULT status=1 x=1 y=1 objective=3 recomputed=3
Expected behavior
The iteration-1 objective should be 3, consistent with the current primal solution and final summary.
Apparent cause
In cpp/src/dual_simplex/phase2.cpp, adjust_for_flips() changes the primal variables without updating the running objective. The subsequent update_objective()
accounts for the pivot displacement but omits the bound-flip displacement.
In this example, flipping x from 0 to 1 contributes the missing +1. This discrepancy is not explained by objective perturbation: the reported perturbation
is only 1e-7.
The final objective is recomputed in prepare_optimality(), which explains why the returned objective and final summary are correct in this reproduction.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.