Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
90adbf4
add cuopt direct solver
Iroy30 Jun 4, 2025
b4b1502
add tests with lp amd milp capabilities
Iroy30 Jul 2, 2025
6f64094
Merge remote-tracking branch 'origin/main'
Iroy30 Jul 2, 2025
d9e42ad
Merge branch 'main' into add_cuopt_direct_solver_plugin
Iroy30 Jul 9, 2025
e2e9159
Update pyomo/solvers/plugins/solvers/cuopt_direct.py
Iroy30 Jul 9, 2025
d4a5d07
Merge remote-tracking branch 'origin'
Iroy30 Jul 9, 2025
61e19bf
formatting
Iroy30 Jul 10, 2025
432d8c0
pyomo/solvers/plugins/solvers/cuopt_direct.py
Iroy30 Jul 10, 2025
5d8ec53
Solver name incorrect - change to cuopt_direct
mrmundt Jul 10, 2025
66b9aa8
Merge branch 'main' into add_cuopt_direct_solver_plugin
Iroy30 Aug 6, 2025
46c5466
Merge branch 'main' into add_cuopt_direct_solver_plugin
Iroy30 Aug 7, 2025
1a61334
Merge branch 'main' into add_cuopt_direct_solver_plugin
Iroy30 Aug 11, 2025
17d8cad
add cuopt tests and fix CI fails
Iroy30 Aug 12, 2025
f064d64
Merge remote-tracking branch 'origin/add_cuopt_direct_solver_plugin' …
Iroy30 Aug 12, 2025
b978841
Merge branch 'main' into add_cuopt_direct_solver_plugin
Iroy30 Aug 12, 2025
af1d91e
update import
Iroy30 Aug 12, 2025
645abe3
Merge branch 'main' into add_cuopt_direct_solver_plugin
Iroy30 Aug 12, 2025
caea3bd
Merge remote-tracking branch 'origin/add_cuopt_direct_solver_plugin' …
Iroy30 Aug 12, 2025
5540913
Merge branch 'main' into add_cuopt_direct_solver_plugin
Iroy30 Aug 14, 2025
1b65acd
Merge branch 'main' into add_cuopt_direct_solver_plugin
Iroy30 Aug 20, 2025
19236fe
Merge branch 'main' into add_cuopt_direct_solver_plugin
Iroy30 Aug 20, 2025
9670f41
Merge branch 'main' into add_cuopt_direct_solver_plugin
Iroy30 Aug 21, 2025
6cfa3e4
Merge branch 'main' into add_cuopt_direct_solver_plugin
Iroy30 Aug 26, 2025
4c87a9e
Merge branch 'main' into add_cuopt_direct_solver_plugin
mrmundt Sep 29, 2025
4bf03d5
address review comments part 1
Iroy30 Oct 13, 2025
7568a7d
Merge branch 'main' into add_cuopt_direct_solver_plugin
mrmundt Oct 14, 2025
3f5c2af
address review comments part 2
Iroy30 Oct 17, 2025
85a7bce
Merge remote-tracking branch 'iroy30/add_cuopt_direct_solver_plugin'
Iroy30 Oct 17, 2025
bdfd966
Merge branch 'main' into add_cuopt_direct_solver_plugin
Iroy30 Oct 17, 2025
c5a5f7b
remove print
Iroy30 Oct 17, 2025
662d2ec
Merge branch 'main' into add_cuopt_direct_solver_plugin
Iroy30 Oct 21, 2025
235293d
Merge branch 'main' into add_cuopt_direct_solver_plugin
jsiirola Oct 27, 2025
5031133
Merge branch 'main' into add_cuopt_direct_solver_plugin
Iroy30 Oct 28, 2025
e433404
address review part 3, black formatting
Iroy30 Oct 28, 2025
124a31a
Merge remote-tracking branch 'iroy30/add_cuopt_direct_solver_plugin'
Iroy30 Oct 28, 2025
0af4ba3
Merge branch 'main' into add_cuopt_direct_solver_plugin
mrmundt Oct 28, 2025
8741e7b
Merge branch 'main' into add_cuopt_direct_solver_plugin
Iroy30 Oct 30, 2025
9f92494
Merge branch 'main' into add_cuopt_direct_solver_plugin
Iroy30 Nov 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyomo/solvers/plugins/solvers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
gurobi_persistent,
cplex_direct,
cplex_persistent,
cuopt_direct,
GAMS,
mosek_direct,
mosek_persistent,
Expand Down
261 changes: 261 additions & 0 deletions pyomo/solvers/plugins/solvers/cuopt_direct.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
# ___________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2025
# National Technology and Engineering Solutions of Sandia, LLC
# Under the terms of Contract DE-NA0003525 with National Technology and
# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
# rights in this software.
# This software is distributed under the 3-clause BSD License.
# ___________________________________________________________________________

import logging
import re
import sys

from pyomo.common.collections import ComponentSet, ComponentMap, Bunch
from pyomo.common.dependencies import attempt_import
from pyomo.core.base import Suffix, Var, Constraint, SOSConstraint, Objective
from pyomo.common.errors import ApplicationError
from pyomo.common.tempfiles import TempfileManager
from pyomo.common.tee import capture_output
from pyomo.core.expr.numvalue import is_fixed
from pyomo.core.expr.numvalue import value
from pyomo.core.staleflag import StaleFlagManager
from pyomo.repn import generate_standard_repn
from pyomo.solvers.plugins.solvers.direct_solver import DirectSolver
from pyomo.solvers.plugins.solvers.direct_or_persistent_solver import (
DirectOrPersistentSolver,
)
from pyomo.core.kernel.objective import minimize, maximize
from pyomo.opt.results.results_ import SolverResults
from pyomo.opt.results.solution import Solution, SolutionStatus
from pyomo.opt.results.solver import TerminationCondition, SolverStatus
from pyomo.opt.base import SolverFactory
from pyomo.core.base.suffix import Suffix
import numpy as np
import time

logger = logging.getLogger('pyomo.solvers')

cuopt, cuopt_available = attempt_import(
'cuopt',
)

@SolverFactory.register('cuopt_direct', doc='Direct python interface to CUOPT')
class CUOPTDirect(DirectSolver):
def __init__(self, **kwds):
kwds['type'] = 'cuoptdirect'
super(CUOPTDirect, self).__init__(**kwds)
self._python_api_exists = True
# Note: Undefined capabilities default to None
self._capabilities.linear = True
self._capabilities.integer = True

def _apply_solver(self):
StaleFlagManager.mark_all_as_stale()
log_file = None
if self._log_file:
log_file = self._log_file
t0 = time.time()
self.solution = cuopt.linear_programming.solver.Solve(self._solver_model)
t1 = time.time()
Comment on lines 62 to 67
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine, but just so you're aware, we have this lovely little utility called TicTocTimer that you may want to consider using: https://pyomo.readthedocs.io/en/latest/api/pyomo.common.timing.TicTocTimer.html

self._wallclock_time = t1 - t0
return Bunch(rc=None, log=None)

Check warning on line 64 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L56-L64

Added lines #L56 - L64 were not covered by tests

def _add_constraint(self, constraints):
c_lb, c_ub = [], []
matrix_data, matrix_indptr, matrix_indices = [], [0], []
for i, con in enumerate(constraints):
repn = generate_standard_repn(con.body, quadratic=False)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would strongly recommend using the pyomo.repn.linear.LinearRepnVisitor and not generate_standard_repn (it has known limitations and is being replaced by the other visitors in pyomo.repn).

matrix_data.extend(repn.linear_coefs)
matrix_indices.extend([self.var_name_dict[str(i)] for i in repn.linear_vars])
"""for v, c in zip(con.body.linear_vars, con.body.linear_coefs):

Check warning on line 73 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L67-L73

Added lines #L67 - L73 were not covered by tests
matrix_data.append(value(c))
matrix_indices.append(self.var_name_dict[str(v)])"""
matrix_indptr.append(len(matrix_data))
c_lb.append(value(con.lower) if con.lower is not None else -np.inf)
c_ub.append(value(con.upper) if con.upper is not None else np.inf)
self._solver_model.set_csr_constraint_matrix(np.array(matrix_data), np.array(matrix_indices), np.array(matrix_indptr))
self._solver_model.set_constraint_lower_bounds(np.array(c_lb))
self._solver_model.set_constraint_upper_bounds(np.array(c_ub))

Check warning on line 81 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L76-L81

Added lines #L76 - L81 were not covered by tests

def _add_var(self, variables):
# Map vriable to index and get var bounds
var_type_dict = {"Integers": 'I', "Reals": 'C', "Binary": 'I'} # NonNegativeReals ?
self.var_name_dict = {}
v_lb, v_ub, v_type = [], [], []

Check warning on line 87 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L85-L87

Added lines #L85 - L87 were not covered by tests

for i, v in enumerate(variables):
v_type.append(var_type_dict[str(v.domain)])
if v.domain == "Binary":
v_lb.append(0)
v_ub.append(1)

Check warning on line 93 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L89-L93

Added lines #L89 - L93 were not covered by tests
else:
v_lb.append(v.lb if v.lb is not None else -np.inf)
v_ub.append(v.ub if v.ub is not None else np.inf)
self.var_name_dict[str(v)] = i
self._pyomo_var_to_ndx_map[v] = self._ndx_count
self._ndx_count += 1

Check warning on line 99 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L95-L99

Added lines #L95 - L99 were not covered by tests

self._solver_model.set_variable_lower_bounds(np.array(v_lb))
self._solver_model.set_variable_upper_bounds(np.array(v_ub))
self._solver_model.set_variable_types(np.array(v_type))
self._solver_model.set_variable_names(np.array(list(self.var_name_dict.keys())))

Check warning on line 104 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L101-L104

Added lines #L101 - L104 were not covered by tests

def _set_objective(self, objective):
repn = generate_standard_repn(objective.expr, quadratic=False)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, I would recommend switching to pyomo.repn.linear.LinearRepnVisitor

obj_coeffs = [0] * len(self.var_name_dict)
for i, coeff in enumerate(repn.linear_coefs):
obj_coeffs[self.var_name_dict[str(repn.linear_vars[i])]] = coeff
self._solver_model.set_objective_coefficients(np.array(obj_coeffs))
if objective.sense == maximize:
self._solver_model.set_maximize(True)

Check warning on line 113 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L107-L113

Added lines #L107 - L113 were not covered by tests

def _set_instance(self, model, kwds={}):
DirectOrPersistentSolver._set_instance(self, model, kwds)
self.var_name_dict = None
self._pyomo_var_to_ndx_map = ComponentMap()
self._ndx_count = 0

Check warning on line 119 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L116-L119

Added lines #L116 - L119 were not covered by tests

try:
self._solver_model = cuopt.linear_programming.DataModel()
except Exception:
e = sys.exc_info()[1]
msg = (

Check warning on line 125 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L121-L125

Added lines #L121 - L125 were not covered by tests
"Unable to create CUOPT model. "
"Have you installed the Python "
"SDK for CUOPT?\n\n\t" + "Error message: {0}".format(e)
)
self._add_block(model)

Check warning on line 130 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L130

Added line #L130 was not covered by tests

def _add_block(self, block):
self._add_var(block.component_data_objects(

Check warning on line 133 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L133

Added line #L133 was not covered by tests
ctype=Var, descend_into=True, active=True, sort=True)
)

for sub_block in block.block_data_objects(descend_into=True, active=True):
self._add_constraint(sub_block.component_data_objects(

Check warning on line 138 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L137-L138

Added lines #L137 - L138 were not covered by tests
ctype=Constraint, descend_into=False, active=True, sort=True)
)
obj_counter = 0
for obj in sub_block.component_data_objects(

Check warning on line 142 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L141-L142

Added lines #L141 - L142 were not covered by tests
ctype=Objective, descend_into=False, active=True
):
obj_counter += 1
if obj_counter > 1:
raise ValueError(

Check warning on line 147 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L145-L147

Added lines #L145 - L147 were not covered by tests
"Solver interface does not support multiple objectives."
)
self._set_objective(obj)

Check warning on line 150 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L150

Added line #L150 was not covered by tests

def _postsolve(self):
extract_duals = False
extract_slacks = False
extract_reduced_costs = False
for suffix in self._suffixes:
flag = False
if re.match(suffix, "rc"):
extract_reduced_costs = True
flag = True
if not flag:
raise RuntimeError(

Check warning on line 162 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L153-L162

Added lines #L153 - L162 were not covered by tests
"***The cuopt_direct solver plugin cannot extract solution suffix="
+ suffix
)
Comment on lines +199 to +203
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be better to log loudly than to raise an error here (no strong feelings either way, just wanted to offer an alternative option).


solution = self.solution
status = solution.get_termination_status()
self.results = SolverResults()
soln = Solution()
self.results.solver.name = "CUOPT"
self.results.solver.wallclock_time = self._wallclock_time

Check warning on line 172 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L167-L172

Added lines #L167 - L172 were not covered by tests

prob_type = solution.problem_category

Check warning on line 174 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L174

Added line #L174 was not covered by tests

if status in [1]:
self.results.solver.status = SolverStatus.ok
self.results.solver.termination_condition = TerminationCondition.optimal
soln.status = SolutionStatus.optimal
elif status in [3]:
self.results.solver.status = SolverStatus.warning
self.results.solver.termination_condition = TerminationCondition.unbounded
soln.status = SolutionStatus.unbounded
elif status in [8]:
self.results.solver.status = SolverStatus.ok
self.results.solver.termination_condition = TerminationCondition.feasible
soln.status = SolutionStatus.feasible
elif status in [2]:
self.results.solver.status = SolverStatus.warning
self.results.solver.termination_condition = TerminationCondition.infeasible
soln.status = SolutionStatus.infeasible
elif status in [4]:
self.results.solver.status = SolverStatus.aborted
self.results.solver.termination_condition = (

Check warning on line 194 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L176-L194

Added lines #L176 - L194 were not covered by tests
TerminationCondition.maxIterations
)
soln.status = SolutionStatus.stoppedByLimit
elif status in [5]:
self.results.solver.status = SolverStatus.aborted
self.results.solver.termination_condition = (

Check warning on line 200 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L197-L200

Added lines #L197 - L200 were not covered by tests
TerminationCondition.maxTimeLimit
)
soln.status = SolutionStatus.stoppedByLimit
elif status in [7]:
self.results.solver.status = SolverStatus.ok
self.results.solver.termination_condition = (

Check warning on line 206 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L203-L206

Added lines #L203 - L206 were not covered by tests
TerminationCondition.other
)
soln.status = SolutionStatus.other

Check warning on line 209 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L209

Added line #L209 was not covered by tests
else:
self.results.solver.status = SolverStatus.error
self.results.solver.termination_condition = TerminationCondition.error
soln.status = SolutionStatus.error

Check warning on line 213 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L211-L213

Added lines #L211 - L213 were not covered by tests

if self._solver_model.maximize:
self.results.problem.sense = maximize

Check warning on line 216 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L215-L216

Added lines #L215 - L216 were not covered by tests
else:
self.results.problem.sense = minimize

Check warning on line 218 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L218

Added line #L218 was not covered by tests

self.results.problem.upper_bound = None
self.results.problem.lower_bound = None
try:
self.results.problem.upper_bound = solution.get_primal_objective()
self.results.problem.lower_bound = solution.get_primal_objective()
except Exception as e:
pass

Check warning on line 226 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L220-L226

Added lines #L220 - L226 were not covered by tests

var_map = self._pyomo_var_to_ndx_map
primal_solution = solution.get_primal_solution().tolist()
for i, pyomo_var in enumerate(var_map.keys()):
pyomo_var.set_value(primal_solution[i], skip_validation=True)

Check warning on line 231 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L228-L231

Added lines #L228 - L231 were not covered by tests

if extract_reduced_costs:
self._load_rc()

Check warning on line 234 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L233-L234

Added lines #L233 - L234 were not covered by tests

self.results.solution.insert(soln)
return DirectOrPersistentSolver._postsolve(self)

Check warning on line 237 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L236-L237

Added lines #L236 - L237 were not covered by tests

def warm_start_capable(self):
return False

Check warning on line 240 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L240

Added line #L240 was not covered by tests

def _load_rc(self, vars_to_load=None):
if not hasattr(self._pyomo_model, 'rc'):
self._pyomo_model.rc = Suffix(direction=Suffix.IMPORT)
rc = self._pyomo_model.rc
var_map = self._pyomo_var_to_ndx_map
if vars_to_load is None:
vars_to_load = var_map.keys()
reduced_costs = self.solution.get_reduced_costs()
for pyomo_var in vars_to_load:
rc[pyomo_var] = reduced_costs[var_map[pyomo_var]]

Check warning on line 251 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L243-L251

Added lines #L243 - L251 were not covered by tests

def load_rc(self, vars_to_load):
"""
Load the reduced costs into the 'rc' suffix. The 'rc' suffix must live on the parent model.

Parameters
----------
vars_to_load: list of Var
"""
self._load_rc(vars_to_load)

Check warning on line 261 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L261

Added line #L261 was not covered by tests
17 changes: 17 additions & 0 deletions pyomo/solvers/tests/solvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,23 @@ def test_solver_cases(*args):

logging.disable(logging.NOTSET)

#
# CUOPT
#
_cuopt_capabilities = set(
[
'linear',
'integer',
]
)

_test_solver_cases['cuopt', 'python'] = initialize(
name='cuopt_direct',
io='python',
capabilities=_cuopt_capabilities,
import_suffixes=['rc'],
)

#
# Error Checks
#
Expand Down
Loading