Skip to content

Handle cuOpt UnboundedOrInfeasible termination status (11)#3916

Open
rgsl888prabhu wants to merge 3 commits intoPyomo:mainfrom
rgsl888prabhu:cuopt-unboundedorinfeasible-status
Open

Handle cuOpt UnboundedOrInfeasible termination status (11)#3916
rgsl888prabhu wants to merge 3 commits intoPyomo:mainfrom
rgsl888prabhu:cuopt-unboundedorinfeasible-status

Conversation

@rgsl888prabhu
Copy link
Copy Markdown

Summary

cuOpt added a new termination status (value 11, `UnboundedOrInfeasible`) that its presolver returns when it cannot disambiguate infeasibility from unboundedness. The `cuopt_direct` plugin's status cascade does not recognize it, so it falls through to `TerminationCondition.error` and fails the `LP_unbounded` test variants (`test_cuopt_python`, `test_cuopt_python_nonsymbolic_labels`, `test_cuopt_python_symbolic_labels`).

Adds an `elif status == 11` branch mapping to `TerminationCondition.infeasibleOrUnbounded` (with `SolverStatus.warning` / `SolutionStatus.unsure`). Also extends the status-code comment block to include status 10 (WorkLimit) and 11 (UnboundedOrInfeasible).

Tracked upstream in NVIDIA/cuopt#1114.

cuOpt added a new termination status (value 11, UnboundedOrInfeasible)
that the PSLP presolver returns when it cannot disambiguate infeasibility
from unboundedness. The cuopt_direct plugin's status cascade did not
recognize it, falling through to TerminationCondition.error and failing
any LP_unbounded test.

Adds an elif branch mapping status 11 to TerminationCondition.
infeasibleOrUnbounded (with SolverStatus.warning / SolutionStatus.unsure).
Also extends the status-code comment block to include status 10 (WorkLimit)
and 11 (UnboundedOrInfeasible) for documentation.

Tracked in NVIDIA/cuopt#1114.

Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@jsiirola
Copy link
Copy Markdown
Member

This looks reasonable. Can you add a a test that exercises this result from cuOpt?

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 17, 2026

Codecov Report

❌ Patch coverage is 0% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.93%. Comparing base (34a3877) to head (326e154).

Files with missing lines Patch % Lines
pyomo/solvers/plugins/solvers/cuopt_direct.py 0.00% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3916      +/-   ##
==========================================
- Coverage   89.94%   89.93%   -0.01%     
==========================================
  Files         902      902              
  Lines      106457   106461       +4     
==========================================
- Hits        95748    95745       -3     
- Misses      10709    10716       +7     
Flag Coverage Δ
builders 29.18% <0.00%> (+<0.01%) ⬆️
default 86.24% <0.00%> (?)
expensive 35.64% <0.00%> (?)
linux 87.39% <0.00%> (-2.04%) ⬇️
linux_other 87.39% <0.00%> (-0.01%) ⬇️
oldsolvers 28.11% <0.00%> (+<0.01%) ⬆️
osx 82.72% <0.00%> (-0.01%) ⬇️
win 85.81% <0.00%> (-0.02%) ⬇️
win_other 85.81% <0.00%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

rgsl888prabhu and others added 2 commits April 17, 2026 13:19
An unbounded LP with no variable bounds triggers cuOpt's presolver
to return UnboundedOrInfeasible (status 11); the test asserts the
plugin maps it to TerminationCondition.infeasibleOrUnbounded,
SolverStatus.warning, and SolutionStatus.unsure.
@jsiirola
Copy link
Copy Markdown
Member

Note: the test appears to require a specific version of cuOpt:

18:21:10 ________________ CUOPTTests.test_unbounded_or_infeasible_status ________________
18:21:10 
18:21:10 self = <pyomo.solvers.tests.checks.test_cuopt_direct.CUOPTTests testMethod=test_unbounded_or_infeasible_status>
18:21:10 
18:21:10     @unittest.skipIf(not cuopt_available, "The CuOpt solver is not available")
18:21:10     def test_unbounded_or_infeasible_status(self):
18:21:10         # An LP with no variable bounds and an unbounded objective triggers
18:21:10         # cuOpt's presolver to return UnboundedOrInfeasible (status 11), which
18:21:10         # the plugin maps to TerminationCondition.infeasibleOrUnbounded.
18:21:10         m = ConcreteModel()
18:21:10         m.x = Var()
18:21:10         m.y = Var()
18:21:10         m.obj = Objective(expr=m.x + m.y, sense=minimize)
18:21:10     
18:21:10         opt = SolverFactory('cuopt')
18:21:10         res = opt.solve(m, load_solutions=False)
18:21:10     
18:21:10 >       self.assertEqual(res.solver.termination_condition, "infeasibleOrUnbounded")
18:21:10 E       AssertionError: <TerminationCondition.unbounded: 'unbounded'> != 'infeasibleOrUnbounded'
18:21:10 
18:21:10 pyomo/pyomo/solvers/tests/checks/test_cuopt_direct.py:141: AssertionError
18:21:10 ----------------------------- Captured stdout call -----------------------------
18:21:10 Setting parameter log_file to /tmp/tmpalrh5rvw.log
18:21:10 cuOpt version: 25.10.0, git hash: 99e549c, host arch: x86_64, device archs: 70-real,75-real,80-real,86-real,90a-real,100f-real,120a-real,120
18:21:10 CPU: Intel(R) Xeon(R) Silver 4410Y, threads (physical/logical): 12/24, RAM: 4.94 GiB
18:21:10 CUDA 12.9, device: NVIDIA T400 4GB (ID 0), VRAM: 3.63 GiB
18:21:10 CUDA device UUID: 
18:21:10 
18:21:10 Solving a problem with 1 constraints, 2 variables (0 integers), and 1 nonzeros
18:21:10 Problem scaling:
18:21:10 Objective coefficents range:          [1e+00, 1e+00]
18:21:10 Constraint matrix coefficients range: [0e+00, 0e+00]
18:21:10 Constraint rhs / bounds range:        [0e+00, 0e+00]
18:21:10 Variable bounds range:                [0e+00, 0e+00]
18:21:10 
18:21:10 Third-party presolve is disabled, skipping
18:21:10 Objective offset 0.000000 scaling_factor 1.000000
18:21:10 Running concurrent
18:21:10 
18:21:10 Dual simplex finished in 0.00 seconds, total time 0.00
18:21:10 Barrier finished in 0.00 seconds
18:21:10    Iter    Primal Obj.      Dual Obj.    Gap        Primal Res.  Dual Res.   Time
18:21:10       0 +0.00000000e+00 +0.00000000e+00  0.00e+00   0.00e+00     1.41e+00   0.017s
18:21:10 PDLP finished
18:21:10 Dual Simplex Solve status Dual Infeasible
18:21:10 Concurrent time:  0.019s, total time 0.019s
18:21:10 Solved with dual simplex
18:21:10 Status: Dual Infeasible   Objective: -inf  Iterations: 0  Time: 0.019s

@rgsl888prabhu
Copy link
Copy Markdown
Author

Note: the test appears to require a specific version of cuOpt:

18:21:10 ________________ CUOPTTests.test_unbounded_or_infeasible_status ________________
18:21:10 
18:21:10 self = <pyomo.solvers.tests.checks.test_cuopt_direct.CUOPTTests testMethod=test_unbounded_or_infeasible_status>
18:21:10 
18:21:10     @unittest.skipIf(not cuopt_available, "The CuOpt solver is not available")
18:21:10     def test_unbounded_or_infeasible_status(self):
18:21:10         # An LP with no variable bounds and an unbounded objective triggers
18:21:10         # cuOpt's presolver to return UnboundedOrInfeasible (status 11), which
18:21:10         # the plugin maps to TerminationCondition.infeasibleOrUnbounded.
18:21:10         m = ConcreteModel()
18:21:10         m.x = Var()
18:21:10         m.y = Var()
18:21:10         m.obj = Objective(expr=m.x + m.y, sense=minimize)
18:21:10     
18:21:10         opt = SolverFactory('cuopt')
18:21:10         res = opt.solve(m, load_solutions=False)
18:21:10     
18:21:10 >       self.assertEqual(res.solver.termination_condition, "infeasibleOrUnbounded")
18:21:10 E       AssertionError: <TerminationCondition.unbounded: 'unbounded'> != 'infeasibleOrUnbounded'
18:21:10 
18:21:10 pyomo/pyomo/solvers/tests/checks/test_cuopt_direct.py:141: AssertionError
18:21:10 ----------------------------- Captured stdout call -----------------------------
18:21:10 Setting parameter log_file to /tmp/tmpalrh5rvw.log
18:21:10 cuOpt version: 25.10.0, git hash: 99e549c, host arch: x86_64, device archs: 70-real,75-real,80-real,86-real,90a-real,100f-real,120a-real,120
18:21:10 CPU: Intel(R) Xeon(R) Silver 4410Y, threads (physical/logical): 12/24, RAM: 4.94 GiB
18:21:10 CUDA 12.9, device: NVIDIA T400 4GB (ID 0), VRAM: 3.63 GiB
18:21:10 CUDA device UUID: 
18:21:10 
18:21:10 Solving a problem with 1 constraints, 2 variables (0 integers), and 1 nonzeros
18:21:10 Problem scaling:
18:21:10 Objective coefficents range:          [1e+00, 1e+00]
18:21:10 Constraint matrix coefficients range: [0e+00, 0e+00]
18:21:10 Constraint rhs / bounds range:        [0e+00, 0e+00]
18:21:10 Variable bounds range:                [0e+00, 0e+00]
18:21:10 
18:21:10 Third-party presolve is disabled, skipping
18:21:10 Objective offset 0.000000 scaling_factor 1.000000
18:21:10 Running concurrent
18:21:10 
18:21:10 Dual simplex finished in 0.00 seconds, total time 0.00
18:21:10 Barrier finished in 0.00 seconds
18:21:10    Iter    Primal Obj.      Dual Obj.    Gap        Primal Res.  Dual Res.   Time
18:21:10       0 +0.00000000e+00 +0.00000000e+00  0.00e+00   0.00e+00     1.41e+00   0.017s
18:21:10 PDLP finished
18:21:10 Dual Simplex Solve status Dual Infeasible
18:21:10 Concurrent time:  0.019s, total time 0.019s
18:21:10 Solved with dual simplex
18:21:10 Status: Dual Infeasible   Objective: -inf  Iterations: 0  Time: 0.019s

Status 11 was introduced in cuOpt v26.04.00 (PR #941, tagged 2026-04-09). The CI runs 25.10.0, so PSLP isn't available and status 11 is never produced

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants