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 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
258 changes: 258 additions & 0 deletions pyomo/solvers/plugins/solvers/cuopt_direct.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
# ___________________________________________________________________________
#
# 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

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()
self._wallclock_time = t1 - t0
return Bunch(rc=None, log=None)

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

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L53-L61

Added lines #L53 - L61 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 70 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L64 - L70 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 78 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L73 - L78 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 84 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L82-L84

Added lines #L82 - L84 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 90 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L86-L90

Added lines #L86 - L90 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 96 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L92-L96

Added lines #L92 - L96 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 101 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L98 - L101 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 110 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L104 - L110 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 116 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L113 - L116 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 122 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L118-L122

Added lines #L118 - L122 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 127 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

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

Added line #L127 was not covered by tests

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

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
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 135 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L134-L135

Added lines #L134 - L135 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 139 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L138 - L139 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 144 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

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

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

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#L147

Added line #L147 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 159 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-L159

Added lines #L150 - L159 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 169 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L164-L169

Added lines #L164 - L169 were not covered by tests

prob_type = solution.problem_category

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L171 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 191 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L173-L191

Added lines #L173 - L191 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 197 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L194 - L197 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 203 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

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

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

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#L206

Added line #L206 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 210 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L208-L210

Added lines #L208 - L210 were not covered by tests

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

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#L212-L213

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

Check warning on line 215 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

Added line #L215 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 223 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/cuopt_direct.py#L217-L223

Added lines #L217 - L223 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 228 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L225 - L228 were not covered by tests

if extract_reduced_costs:
self._load_rc()

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#L230-L231

Added lines #L230 - L231 were not covered by tests

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

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

def warm_start_capable(self):
return False

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#L237

Added line #L237 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 248 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-L248

Added lines #L240 - L248 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 258 in pyomo/solvers/plugins/solvers/cuopt_direct.py

View check run for this annotation

Codecov / codecov/patch

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

Added line #L258 was not covered by tests
Loading