-
Notifications
You must be signed in to change notification settings - Fork 560
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
base: main
Are you sure you want to change the base?
add cuopt direct solver #3620
Changes from 23 commits
90adbf4
b4b1502
6f64094
d9e42ad
e2e9159
d4a5d07
61e19bf
432d8c0
5d8ec53
66b9aa8
46c5466
1a61334
17d8cad
f064d64
b978841
af1d91e
645abe3
caea3bd
5540913
1b65acd
19236fe
9670f41
6cfa3e4
4c87a9e
4bf03d5
7568a7d
3f5c2af
85a7bce
bdfd966
c5a5f7b
662d2ec
235293d
5031133
e433404
124a31a
0af4ba3
8741e7b
9f92494
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,360 @@ | ||
| # ___________________________________________________________________________ | ||
| # | ||
| # 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.common.dependencies import numpy as np | ||
| 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 | ||
Iroy30 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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 | ||
Iroy30 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| import time | ||
|
|
||
| logger = logging.getLogger("pyomo.solvers") | ||
Iroy30 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
|
|
||
| cuopt, cuopt_available = attempt_import("cuopt") | ||
|
|
||
|
|
||
| @SolverFactory.register("cuopt", doc="Direct python interface to CUOPT") | ||
| class CUOPTDirect(DirectSolver): | ||
| def __init__(self, **kwds): | ||
| kwds["type"] = "cuoptdirect" | ||
| super(CUOPTDirect, self).__init__(**kwds) | ||
| try: | ||
| import cuopt | ||
|
|
||
| self._version = tuple(int(k) for k in cuopt.__version__.split('.')) | ||
| self._python_api_exists = True | ||
| except ImportError: | ||
| self._python_api_exists = False | ||
| except Exception as e: | ||
| print("Import of cuopt failed - cuopt message=" + str(e) + "\n") | ||
Iroy30 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Iroy30 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| self._python_api_exists = False | ||
| # Note: Undefined capabilities default to None | ||
| self._capabilities.linear = True | ||
| self._capabilities.integer = True | ||
| self.referenced_vars = ComponentSet() | ||
|
|
||
| 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
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. This is fine, but just so you're aware, we have this lovely little utility called |
||
| self._wallclock_time = t1 - t0 | ||
| return Bunch(rc=None, log=None) | ||
|
|
||
| def _add_constraint(self, constraints): | ||
Iroy30 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| c_lb, c_ub = [], [] | ||
| matrix_data = [] | ||
| matrix_indptr = [0] | ||
| matrix_indices = [] | ||
|
|
||
| con_idx = 0 | ||
| for con in constraints: | ||
| if not con.active: | ||
| return None | ||
Iroy30 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| if self._skip_trivial_constraints and is_fixed(con.body): | ||
| return None | ||
|
||
|
|
||
| if not con.has_lb() and not con.has_ub(): | ||
| assert not con.equality | ||
| continue # non-binding, so skip | ||
|
|
||
| conname = self._symbol_map.getSymbol(con, self._labeler) | ||
| self._pyomo_con_to_solver_con_map[con] = con_idx | ||
| con_idx += 0 | ||
Iroy30 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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] | ||
Iroy30 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ) | ||
| self.referenced_vars.update(repn.linear_vars) | ||
| matrix_indptr.append(len(matrix_data)) | ||
| c_lb.append( | ||
| value(con.lower) - repn.constant if con.lower is not None else -np.inf | ||
| ) | ||
| c_ub.append( | ||
| value(con.upper) - repn.constant if con.upper is not None else np.inf | ||
| ) | ||
Iroy30 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| if len(matrix_data) == 0: | ||
| matrix_data = [0] | ||
| matrix_indices = [0] | ||
| matrix_indptr = [0, 1] | ||
| c_lb = [0] | ||
| c_ub = [0] | ||
|
|
||
| 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): | ||
Iroy30 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| # Map variable to index and get var bounds | ||
| self.var_name_dict = {} | ||
| v_lb, v_ub, v_type = [], [], [] | ||
|
|
||
| for i, v in enumerate(variables): | ||
Iroy30 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if v.is_binary(): | ||
| v_type.append("I") | ||
| v_lb.append(v.lb if v.lb is not None else 0) | ||
| v_ub.append(v.ub if v.ub is not None else 1) | ||
Iroy30 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| else: | ||
| if v.is_integer(): | ||
| v_type.append("I") | ||
| else: | ||
| v_type.append("C") | ||
Iroy30 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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) | ||
|
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. Again, I would recommend switching to |
||
| self.referenced_vars.update(repn.linear_vars) | ||
|
|
||
| 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) | ||
Iroy30 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| 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] | ||
Iroy30 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| msg = ( | ||
| "Unable to create CUOPT model. " | ||
| "Have you installed the Python " | ||
| "SDK for CUOPT?\n\n\t" + "Error message: {0}".format(e) | ||
| ) | ||
Iroy30 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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 | ||
| ) | ||
| ) | ||
| self._add_constraint( | ||
| block.component_data_objects( | ||
| ctype=Constraint, descend_into=True, active=True, sort=True | ||
| ) | ||
| ) | ||
| for sub_block in block.block_data_objects(descend_into=True, active=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) | ||
Iroy30 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| def _postsolve(self): | ||
| extract_duals = False | ||
| extract_slacks = False | ||
| extract_reduced_costs = False | ||
| for suffix in self._suffixes: | ||
| flag = False | ||
| if re.match(suffix, "dual"): | ||
Iroy30 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| extract_duals = True | ||
| flag = True | ||
| 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 | ||
| ) | ||
|
Comment on lines
+199
to
+203
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. 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 | ||
|
|
||
| prob_type = solution.problem_category | ||
|
|
||
| if status in [1]: | ||
Iroy30 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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]: | ||
Iroy30 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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() | ||
Iroy30 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| except Exception as e: | ||
| pass | ||
|
|
||
| var_map = self._pyomo_var_to_ndx_map | ||
| con_map = self._pyomo_con_to_solver_con_map | ||
|
|
||
| primal_solution = solution.get_primal_solution().tolist() | ||
| reduced_costs = None | ||
| dual_solution = None | ||
| if extract_reduced_costs and solution.get_problem_category() == 0: | ||
| reduced_costs = solution.get_reduced_cost() | ||
| if extract_duals and solution.get_problem_category() == 0: | ||
| dual_solution = solution.get_dual_solution() | ||
|
|
||
| if self._save_results: | ||
| soln_variables = soln.variable | ||
| soln_constraints = soln.constraint | ||
| for i, pyomo_var in enumerate(var_map.keys()): | ||
| if len(primal_solution) > 0 and pyomo_var in self.referenced_vars: | ||
| name = self._symbol_map.getSymbol(pyomo_var, self._labeler) | ||
| soln_variables[name] = {"Value": primal_solution[i]} | ||
| if reduced_costs is not None: | ||
| soln_variables[name]["Rc"] = reduced_costs[i] | ||
| for i, pyomo_con in enumerate(con_map.keys()): | ||
| if dual_solution is not None: | ||
| con_name = self._symbol_map.getSymbol(pyomo_con, self._labeler) | ||
| soln_constraints[con_name] = {"Dual": dual_solution[i]} | ||
|
|
||
| elif self._load_solutions: | ||
| if len(primal_solution) > 0: | ||
| for i, pyomo_var in enumerate(var_map.keys()): | ||
| pyomo_var.set_value(primal_solution[i], skip_validation=True) | ||
|
|
||
| if reduced_costs is not None: | ||
| self._load_rc() | ||
|
|
||
| if dual_solution is not None: | ||
| self._load_duals() | ||
|
|
||
| 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_cost() | ||
| for pyomo_var in vars_to_load: | ||
| rc[pyomo_var] = reduced_costs[var_map[pyomo_var]] | ||
|
|
||
| def load_rc(self, vars_to_load=None): | ||
| """ | ||
| 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) | ||
|
|
||
| def _load_duals(self, cons_to_load=None): | ||
| if not hasattr(self._pyomo_model, 'dual'): | ||
| self._pyomo_model.dual = Suffix(direction=Suffix.IMPORT) | ||
| dual = self._pyomo_model.dual | ||
| con_map = self._pyomo_con_to_solver_con_map | ||
| if cons_to_load is None: | ||
| cons_to_load = con_map.keys() | ||
| dual_solution = self.solution.get_reduced_cost() | ||
| for pyomo_con in cons_to_load: | ||
| dual[pyomo_con] = dual_solution[con_map[pyomo_con]] | ||
|
|
||
| def load_duals(self, cons_to_load=None): | ||
| """ | ||
| Load the duals into the 'dual' suffix. The 'dual' suffix must live on the parent model. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| cons_to_load: list of Constraint | ||
| """ | ||
| self._load_duals(cons_to_load) | ||
Uh oh!
There was an error while loading. Please reload this page.