Skip to content

add cuopt direct solver #3620

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
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 +70 to +72
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)
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)
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
)

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