-
Notifications
You must be signed in to change notification settings - Fork 547
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
Iroy30
wants to merge
9
commits into
Pyomo:main
Choose a base branch
from
Iroy30:add_cuopt_direct_solver_plugin
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+291
−0
Open
add cuopt direct solver #3620
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
90adbf4
add cuopt direct solver
Iroy30 b4b1502
add tests with lp amd milp capabilities
Iroy30 6f64094
Merge remote-tracking branch 'origin/main'
Iroy30 d9e42ad
Merge branch 'main' into add_cuopt_direct_solver_plugin
Iroy30 e2e9159
Update pyomo/solvers/plugins/solvers/cuopt_direct.py
Iroy30 d4a5d07
Merge remote-tracking branch 'origin'
Iroy30 61e19bf
formatting
Iroy30 432d8c0
pyomo/solvers/plugins/solvers/cuopt_direct.py
Iroy30 5d8ec53
Solver name incorrect - change to cuopt_direct
mrmundt File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,273 @@ | ||
# ___________________________________________________________________________ | ||
# | ||
# 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() | ||
self._wallclock_time = t1 - t0 | ||
return Bunch(rc=None, log=None) | ||
|
||
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): | ||
matrix_data.append(value(c)) | ||
matrix_indices.append(self.var_name_dict[str(v)])""" | ||
Iroy30 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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)) | ||
|
||
def _add_var(self, variables): | ||
# Map vriable to index and get var bounds | ||
Iroy30 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
var_type_dict = { | ||
"Integers": "I", | ||
"Reals": "C", | ||
"Binary": "I", | ||
} # NonNegativeReals ? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am fairly certain this is necessary. I got the tests running on our test machine, and I keep getting these errors:
I am also seeing errors like this:
Plus some other errors:
|
||
|
||
self.var_name_dict = {} | ||
v_lb, v_ub, v_type = [], [], [] | ||
|
||
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) | ||
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 | ||
|
||
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()))) | ||
|
||
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) | ||
|
||
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 | ||
|
||
try: | ||
self._solver_model = cuopt.linear_programming.DataModel() | ||
except Exception: | ||
e = sys.exc_info()[1] | ||
msg = ( | ||
"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) | ||
|
||
def _add_block(self, block): | ||
self._add_var( | ||
block.component_data_objects( | ||
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( | ||
ctype=Constraint, descend_into=False, active=True, sort=True | ||
) | ||
) | ||
obj_counter = 0 | ||
for obj in sub_block.component_data_objects( | ||
ctype=Objective, descend_into=False, active=True | ||
): | ||
obj_counter += 1 | ||
if obj_counter > 1: | ||
raise ValueError( | ||
"Solver interface does not support multiple objectives." | ||
) | ||
self._set_objective(obj) | ||
|
||
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( | ||
"***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 | ||
|
||
prob_type = solution.problem_category | ||
|
||
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 = ( | ||
TerminationCondition.maxIterations | ||
) | ||
soln.status = SolutionStatus.stoppedByLimit | ||
elif status in [5]: | ||
self.results.solver.status = SolverStatus.aborted | ||
self.results.solver.termination_condition = ( | ||
TerminationCondition.maxTimeLimit | ||
) | ||
soln.status = SolutionStatus.stoppedByLimit | ||
elif status in [7]: | ||
self.results.solver.status = SolverStatus.ok | ||
self.results.solver.termination_condition = TerminationCondition.other | ||
soln.status = SolutionStatus.other | ||
else: | ||
self.results.solver.status = SolverStatus.error | ||
self.results.solver.termination_condition = TerminationCondition.error | ||
soln.status = SolutionStatus.error | ||
|
||
if self._solver_model.maximize: | ||
self.results.problem.sense = maximize | ||
else: | ||
self.results.problem.sense = minimize | ||
|
||
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 | ||
|
||
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) | ||
|
||
if extract_reduced_costs: | ||
self._load_rc() | ||
|
||
self.results.solution.insert(soln) | ||
return DirectOrPersistentSolver._postsolve(self) | ||
|
||
def warm_start_capable(self): | ||
return False | ||
|
||
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]] | ||
|
||
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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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