Source code for pyomo.solvers.plugins.solvers.gurobi_direct

#  ___________________________________________________________________________
#
#  Pyomo: Python Optimization Modeling Objects
#  Copyright (c) 2008-2024
#  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.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 pyomo.core.base.var


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


class DegreeError(ValueError):
    pass


def _is_numeric(x):
    try:
        float(x)
    except ValueError:
        return False
    return True


def _parse_gurobi_version(gurobipy, avail):
    if not avail:
        return
    GurobiDirect._version = gurobipy.gurobi.version()
    GurobiDirect._name = "Gurobi %s.%s%s" % GurobiDirect._version
    while len(GurobiDirect._version) < 4:
        GurobiDirect._version += (0,)
    GurobiDirect._version = GurobiDirect._version[:4]
    GurobiDirect._version_major = GurobiDirect._version[0]


gurobipy, gurobipy_available = attempt_import(
    'gurobipy',
    # Other forms of exceptions can be thrown by the gurobi python
    # import.  For example, a gurobipy.GurobiError exception is thrown
    # if all tokens for Gurobi are already in use; assuming, of course,
    # the license is a token license.  Unfortunately, you can't import
    # without a license, which means we can't explicitly test for that
    # exception!
    catch_exceptions=(Exception,),
    callback=_parse_gurobi_version,
)


def _set_options(model_or_env, options):
    # Set a parameters from the dictionary 'options' on the given gurobipy
    # model or environment.
    for key, option in options.items():
        # When options come from the pyomo command, all
        # values are string types, so we try to cast
        # them to a numeric value in the event that
        # setting the parameter fails.
        try:
            model_or_env.setParam(key, option)
        except TypeError:
            # we place the exception handling for
            # checking the cast of option to a float in
            # another function so that we can simply
            # call raise here instead of except
            # TypeError as e / raise e, because the
            # latter does not preserve the Gurobi stack
            # trace
            if not _is_numeric(option):
                raise
            model_or_env.setParam(key, float(option))


[docs]@SolverFactory.register('gurobi_direct', doc='Direct python interface to Gurobi') class GurobiDirect(DirectSolver): """A direct interface to Gurobi using gurobipy. :param manage_env: Set to True if this solver instance should create and manage its own Gurobi environment (defaults to False) :type manage_env: bool :param options: Dictionary of Gurobi parameters to set :type options: dict If ``manage_env`` is set to True, the ``GurobiDirect`` object creates a local Gurobi environment and manage all associated Gurobi resources. Importantly, this enables Gurobi licenses to be freed and connections terminated when the solver context is exited:: with SolverFactory('gurobi', solver_io='python', manage_env=True) as opt: opt.solve(model) # All Gurobi models and environments are freed If ``manage_env`` is set to False (the default), the ``GurobiDirect`` object uses the global default Gurobi environment:: with SolverFactory('gurobi', solver_io='python') as opt: opt.solve(model) # Only models created by `opt` are freed, the global default # environment remains active ``manage_env=True`` is required when setting license or connection parameters programmatically. The ``options`` argument is used to pass parameters to the Gurobi environment. For example, to connect to a Gurobi Cluster Manager:: options = { "CSManager": "<url>", "CSAPIAccessID": "<access-id>", "CSAPISecret": "<api-key>", } with SolverFactory( 'gurobi', solver_io='python', manage_env=True, options=options ) as opt: opt.solve(model) # Model solved on compute server # Compute server connection terminated """ _name = None _version = 0 _version_major = 0 _default_env_started = False def __init__(self, manage_env=False, **kwds): if 'type' not in kwds: kwds['type'] = 'gurobi_direct' super(GurobiDirect, self).__init__(**kwds) self._pyomo_var_to_solver_var_map = ComponentMap() self._solver_var_to_pyomo_var_map = ComponentMap() self._pyomo_con_to_solver_con_map = dict() self._solver_con_to_pyomo_con_map = ComponentMap() self._needs_updated = True # flag that indicates if solver_model.update() needs called before getting variable and constraint attributes self._callback = None self._callback_func = None self._python_api_exists = gurobipy_available self._range_constraints = set() self._max_obj_degree = 2 self._max_constraint_degree = 2 # Note: Undefined capabilities default to None self._capabilities.linear = True self._capabilities.quadratic_objective = True self._capabilities.quadratic_constraint = True self._capabilities.integer = True self._capabilities.sos1 = True self._capabilities.sos2 = True # fix for compatibility with pre-5.0 Gurobi # # Note: Unfortunately, this will trigger the immediate import # of the gurobipy module if gurobipy_available and GurobiDirect._version_major < 5: self._max_constraint_degree = 1 self._capabilities.quadratic_constraint = False # remove the instance-level definition of the gurobi version: # because the version comes from an imported module, only one # version of gurobi is supported (and stored as a class attribute) del self._version self._manage_env = manage_env self._env = None self._env_options = None self._solver_model = None
[docs] def available(self, exception_flag=True): """Returns True if the solver is available. :param exception_flag: If True, raise an exception instead of returning False if the solver is unavailable (defaults to False) :type exception_flag: bool In general, ``available()`` does not need to be called by the user, as the check is run automatically when solving a model. However it is useful for a simple retry loop when using a shared Gurobi license:: with SolverFactory('gurobi', solver_io='python') as opt: while not available(exception_flag=False): time.sleep(1) opt.solve(model) """ # First check gurobipy is imported if not gurobipy_available: if exception_flag: gurobipy.log_import_warning(logger=__name__) raise ApplicationError( "No Python bindings available for %s solver plugin" % (type(self),) ) return False # Ensure environment is started to check for a valid license with capture_output(capture_fd=True) as OUT: try: self._init_env() return True except gurobipy.GurobiError as e: msg = "Could not create Model - gurobi message=%s\n" % (e,) if OUT.getvalue(): msg += "\n" + OUT.getvalue() # Didn't return, so environment start failed if exception_flag: logger.warning(msg) raise ApplicationError( "Could not create Model for %s solver plugin - gurobi message=%s" % (type(self), msg) ) else: return False
def _apply_solver(self): StaleFlagManager.mark_all_as_stale() if self._tee: self._solver_model.setParam('OutputFlag', 1) else: self._solver_model.setParam('OutputFlag', 0) if self._keepfiles: # Only save log file when the user wants to keep it. self._solver_model.setParam('LogFile', self._log_file) print("Solver log file: " + self._log_file) # Only pass along changed parameters to the model if self._env_options: new_options = { key: option for key, option in self.options.items() if key not in self._env_options or self._env_options[key] != option } else: new_options = self.options _set_options(self._solver_model, new_options) if self._version_major >= 5: for suffix in self._suffixes: if re.match(suffix, "dual"): self._solver_model.setParam(gurobipy.GRB.Param.QCPDual, 1) self._solver_model.optimize(self._callback) self._needs_updated = False if self._keepfiles: # Change LogFile to make Gurobi close the original log file. # May not work for all Gurobi versions, like ver. 9.5.0. self._solver_model.setParam('LogFile', 'default') # FIXME: can we get a return code indicating if Gurobi had a significant failure? return Bunch(rc=None, log=None) def _get_expr_from_pyomo_repn(self, repn, max_degree=2): referenced_vars = ComponentSet() degree = repn.polynomial_degree() if (degree is None) or (degree > max_degree): raise DegreeError( 'GurobiDirect does not support expressions of degree {0}.'.format( degree ) ) if len(repn.linear_vars) > 0: referenced_vars.update(repn.linear_vars) new_expr = gurobipy.LinExpr( repn.linear_coefs, [self._pyomo_var_to_solver_var_map[i] for i in repn.linear_vars], ) else: new_expr = 0.0 for i, v in enumerate(repn.quadratic_vars): x, y = v new_expr += ( repn.quadratic_coefs[i] * self._pyomo_var_to_solver_var_map[x] * self._pyomo_var_to_solver_var_map[y] ) referenced_vars.add(x) referenced_vars.add(y) new_expr += repn.constant return new_expr, referenced_vars def _get_expr_from_pyomo_expr(self, expr, max_degree=2): if max_degree == 2: repn = generate_standard_repn(expr, quadratic=True) else: repn = generate_standard_repn(expr, quadratic=False) try: gurobi_expr, referenced_vars = self._get_expr_from_pyomo_repn( repn, max_degree ) except DegreeError as e: msg = e.args[0] msg += '\nexpr: {0}'.format(expr) raise DegreeError(msg) return gurobi_expr, referenced_vars def _gurobi_lb_ub_from_var(self, var): if var.is_fixed(): val = var.value return val, val if var.has_lb(): lb = value(var.lb) else: lb = -gurobipy.GRB.INFINITY if var.has_ub(): ub = value(var.ub) else: ub = gurobipy.GRB.INFINITY return lb, ub def _add_var(self, var): varname = self._symbol_map.getSymbol(var, self._labeler) vtype = self._gurobi_vtype_from_var(var) lb, ub = self._gurobi_lb_ub_from_var(var) gurobipy_var = self._solver_model.addVar( lb=lb, ub=ub, vtype=vtype, name=varname ) self._pyomo_var_to_solver_var_map[var] = gurobipy_var self._solver_var_to_pyomo_var_map[gurobipy_var] = var self._referenced_variables[var] = 0 self._needs_updated = True
[docs] def close_global(self): """Frees all Gurobi models used by this solver, and frees the global default Gurobi environment. The default environment is used by all ``GurobiDirect`` solvers started with ``manage_env=False`` (the default). To guarantee that all Gurobi resources are freed, all instantiated ``GurobiDirect`` solvers must also be correctly closed. The following example will free all Gurobi resources assuming the user did not create any other models (e.g. via another ``GurobiDirect`` object with ``manage_env=False``):: opt = SolverFactory('gurobi', solver_io='python') try: opt.solve(model) finally: opt.close_global() # All Gurobi models created by `opt` are freed and the default # Gurobi environment is closed """ self.close() with capture_output(capture_fd=True): gurobipy.disposeDefaultEnv() GurobiDirect._default_env_started = False
def _init_env(self): if self._manage_env: # Ensure an environment is active for this instance if self._env is None: assert self._solver_model is None env = gurobipy.Env(empty=True) _set_options(env, self.options) env.start() # Successful start (no errors): store the environment self._env = env self._env_options = dict(self.options) else: # Ensure the (global) default env is started if not GurobiDirect._default_env_started: m = gurobipy.Model() m.close() GurobiDirect._default_env_started = True def _create_model(self, model): self._init_env() if self._solver_model is not None: self._solver_model.close() if model.name is not None: self._solver_model = gurobipy.Model(model.name, env=self._env) else: self._solver_model = gurobipy.Model(env=self._env)
[docs] def close(self): """Frees local Gurobi resources used by this solver instance. All Gurobi models created by the solver are freed. If the solver was created with ``manage_env=True``, this method also closes the Gurobi environment used by this solver instance. Calling ``.close()`` achieves the same result as exiting the solver context (although using context managers is preferred where possible):: opt = SolverFactory('gurobi', solver_io='python', manage_env=True) try: opt.solve(model) finally: opt.close() # Gurobi models and environments created by `opt` are freed As with the context manager, if ``manage_env=False`` (the default) was used, only the Gurobi models created by this solver are freed. The default global Gurobi environment will still be active:: opt = SolverFactory('gurobi', solver_io='python') try: opt.solve(model) finally: opt.close() # Gurobi models created by `opt` are freed; however the # default/global Gurobi environment is still active """ if self._solver_model is not None: self._solver_model.close() self._solver_model = None if self._manage_env: if self._env is not None: self._env.close() self._env = None self._env_options = None
def __exit__(self, t, v, traceback): super().__exit__(t, v, traceback) self.close() def _set_instance(self, model, kwds={}): self._range_constraints = set() DirectOrPersistentSolver._set_instance(self, model, kwds) self._pyomo_con_to_solver_con_map = dict() self._solver_con_to_pyomo_con_map = ComponentMap() self._pyomo_var_to_solver_var_map = ComponentMap() self._solver_var_to_pyomo_var_map = ComponentMap() try: self._create_model(model) except Exception: e = sys.exc_info()[1] msg = ( "Unable to create Gurobi model. " "Have you installed the Python " "bindings for Gurobi?\n\n\t" + "Error message: {0}".format(e) ) raise Exception(msg) self._add_block(model) for var, n_ref in self._referenced_variables.items(): if n_ref != 0: if var.fixed: if not self._output_fixed_variable_bounds: raise ValueError( "Encountered a fixed variable (%s) inside " "an active objective or constraint " "expression on model %s, which is usually " "indicative of a preprocessing error. Use " "the IO-option 'output_fixed_variable_bounds=True' " "to suppress this error and fix the variable " "by overwriting its bounds in the Gurobi instance." % (var.name, self._pyomo_model.name) ) def _add_block(self, block): DirectOrPersistentSolver._add_block(self, block) def _add_constraint(self, con): if not con.active: return None if self._skip_trivial_constraints and is_fixed(con.body): return None conname = self._symbol_map.getSymbol(con, self._labeler) if con._linear_canonical_form: gurobi_expr, referenced_vars = self._get_expr_from_pyomo_repn( con.canonical_form(), self._max_constraint_degree ) # elif isinstance(con, LinearCanonicalRepn): # gurobi_expr, referenced_vars = self._get_expr_from_pyomo_repn( # con, # self._max_constraint_degree) else: gurobi_expr, referenced_vars = self._get_expr_from_pyomo_expr( con.body, self._max_constraint_degree ) if con.has_lb(): if not is_fixed(con.lower): raise ValueError( "Lower bound of constraint {0} is not constant.".format(con) ) if con.has_ub(): if not is_fixed(con.upper): raise ValueError( "Upper bound of constraint {0} is not constant.".format(con) ) if con.equality: gurobipy_con = self._solver_model.addConstr( lhs=gurobi_expr, sense=gurobipy.GRB.EQUAL, rhs=value(con.lower), name=conname, ) elif con.has_lb() and con.has_ub(): gurobipy_con = self._solver_model.addRange( gurobi_expr, value(con.lower), value(con.upper), name=conname ) self._range_constraints.add(con) elif con.has_lb(): gurobipy_con = self._solver_model.addConstr( lhs=gurobi_expr, sense=gurobipy.GRB.GREATER_EQUAL, rhs=value(con.lower), name=conname, ) elif con.has_ub(): gurobipy_con = self._solver_model.addConstr( lhs=gurobi_expr, sense=gurobipy.GRB.LESS_EQUAL, rhs=value(con.upper), name=conname, ) else: raise ValueError( "Constraint does not have a lower " "or an upper bound: {0} \n".format(con) ) for var in referenced_vars: self._referenced_variables[var] += 1 self._vars_referenced_by_con[con] = referenced_vars self._pyomo_con_to_solver_con_map[con] = gurobipy_con self._solver_con_to_pyomo_con_map[gurobipy_con] = con self._needs_updated = True def _add_sos_constraint(self, con): if not con.active: return None conname = self._symbol_map.getSymbol(con, self._labeler) level = con.level if level == 1: sos_type = gurobipy.GRB.SOS_TYPE1 elif level == 2: sos_type = gurobipy.GRB.SOS_TYPE2 else: raise ValueError( "Solver does not support SOS level {0} constraints".format(level) ) gurobi_vars = [] weights = [] self._vars_referenced_by_con[con] = ComponentSet() if hasattr(con, 'get_items'): # aml sos constraint sos_items = list(con.get_items()) else: # kernel sos constraint sos_items = list(con.items()) for v, w in sos_items: self._vars_referenced_by_con[con].add(v) gurobi_vars.append(self._pyomo_var_to_solver_var_map[v]) self._referenced_variables[v] += 1 weights.append(w) gurobipy_con = self._solver_model.addSOS(sos_type, gurobi_vars, weights) self._pyomo_con_to_solver_con_map[con] = gurobipy_con self._solver_con_to_pyomo_con_map[gurobipy_con] = con self._needs_updated = True def _gurobi_vtype_from_var(self, var): """ This function takes a pyomo variable and returns the appropriate gurobi variable type :param var: pyomo.core.base.var.Var :return: gurobipy.GRB.CONTINUOUS or gurobipy.GRB.BINARY or gurobipy.GRB.INTEGER """ if var.is_binary(): vtype = gurobipy.GRB.BINARY elif var.is_integer(): vtype = gurobipy.GRB.INTEGER elif var.is_continuous(): vtype = gurobipy.GRB.CONTINUOUS else: raise ValueError( 'Variable domain type is not recognized for {0}'.format(var.domain) ) return vtype def _set_objective(self, obj): if self._objective is not None: for var in self._vars_referenced_by_obj: self._referenced_variables[var] -= 1 self._vars_referenced_by_obj = ComponentSet() self._objective = None if obj.active is False: raise ValueError('Cannot add inactive objective to solver.') if obj.sense == minimize: sense = gurobipy.GRB.MINIMIZE elif obj.sense == maximize: sense = gurobipy.GRB.MAXIMIZE else: raise ValueError('Objective sense is not recognized: {0}'.format(obj.sense)) gurobi_expr, referenced_vars = self._get_expr_from_pyomo_expr( obj.expr, self._max_obj_degree ) for var in referenced_vars: self._referenced_variables[var] += 1 self._solver_model.setObjective(gurobi_expr, sense=sense) self._objective = obj self._vars_referenced_by_obj = referenced_vars self._needs_updated = True def _postsolve(self): # the only suffixes that we extract from GUROBI are # constraint duals, constraint slacks, and variable # reduced-costs. scan through the solver suffix list # and throw an exception if the user has specified # any others. extract_duals = False extract_slacks = False extract_reduced_costs = False for suffix in self._suffixes: flag = False if re.match(suffix, "dual"): extract_duals = True flag = True if re.match(suffix, "slack"): extract_slacks = True flag = True if re.match(suffix, "rc"): extract_reduced_costs = True flag = True if not flag: raise RuntimeError( "***The gurobi_direct solver plugin cannot extract solution suffix=" + suffix ) gprob = self._solver_model grb = gurobipy.GRB status = gprob.Status if gprob.getAttr(gurobipy.GRB.Attr.IsMIP): if extract_reduced_costs: logger.warning("Cannot get reduced costs for MIP.") if extract_duals: logger.warning("Cannot get duals for MIP.") extract_reduced_costs = False extract_duals = False self.results = SolverResults() soln = Solution() self.results.solver.name = GurobiDirect._name self.results.solver.wallclock_time = gprob.Runtime if status == grb.LOADED: # problem is loaded, but no solution self.results.solver.status = SolverStatus.aborted self.results.solver.termination_message = ( "Model is loaded, but no solution information is available." ) self.results.solver.termination_condition = TerminationCondition.error soln.status = SolutionStatus.unknown elif status == grb.OPTIMAL: # optimal self.results.solver.status = SolverStatus.ok self.results.solver.termination_message = ( "Model was solved to optimality (subject to tolerances), " "and an optimal solution is available." ) self.results.solver.termination_condition = TerminationCondition.optimal soln.status = SolutionStatus.optimal elif status == grb.INFEASIBLE: self.results.solver.status = SolverStatus.warning self.results.solver.termination_message = ( "Model was proven to be infeasible" ) self.results.solver.termination_condition = TerminationCondition.infeasible soln.status = SolutionStatus.infeasible elif status == grb.INF_OR_UNBD: self.results.solver.status = SolverStatus.warning self.results.solver.termination_message = ( "Problem proven to be infeasible or unbounded." ) self.results.solver.termination_condition = ( TerminationCondition.infeasibleOrUnbounded ) soln.status = SolutionStatus.unsure elif status == grb.UNBOUNDED: self.results.solver.status = SolverStatus.warning self.results.solver.termination_message = ( "Model was proven to be unbounded." ) self.results.solver.termination_condition = TerminationCondition.unbounded soln.status = SolutionStatus.unbounded elif status == grb.CUTOFF: self.results.solver.status = SolverStatus.aborted self.results.solver.termination_message = ( "Optimal objective for model was proven to be worse than the " "value specified in the Cutoff parameter. No solution " "information is available." ) self.results.solver.termination_condition = ( TerminationCondition.minFunctionValue ) soln.status = SolutionStatus.unknown elif status == grb.ITERATION_LIMIT: self.results.solver.status = SolverStatus.aborted self.results.solver.termination_message = ( "Optimization terminated because the total number of simplex " "iterations performed exceeded the value specified in the " "IterationLimit parameter." ) self.results.solver.termination_condition = ( TerminationCondition.maxIterations ) soln.status = SolutionStatus.stoppedByLimit elif status == grb.NODE_LIMIT: self.results.solver.status = SolverStatus.aborted self.results.solver.termination_message = ( "Optimization terminated because the total number of " "branch-and-cut nodes explored exceeded the value specified " "in the NodeLimit parameter" ) self.results.solver.termination_condition = ( TerminationCondition.maxEvaluations ) soln.status = SolutionStatus.stoppedByLimit elif status == grb.TIME_LIMIT: self.results.solver.status = SolverStatus.aborted self.results.solver.termination_message = ( "Optimization terminated because the time expended exceeded " "the value specified in the TimeLimit parameter." ) self.results.solver.termination_condition = ( TerminationCondition.maxTimeLimit ) soln.status = SolutionStatus.stoppedByLimit elif status == grb.SOLUTION_LIMIT: self.results.solver.status = SolverStatus.aborted self.results.solver.termination_message = ( "Optimization terminated because the number of solutions found " "reached the value specified in the SolutionLimit parameter." ) self.results.solver.termination_condition = TerminationCondition.unknown soln.status = SolutionStatus.stoppedByLimit elif status == grb.INTERRUPTED: self.results.solver.status = SolverStatus.aborted self.results.solver.termination_message = ( "Optimization was terminated by the user." ) self.results.solver.termination_condition = TerminationCondition.error soln.status = SolutionStatus.error elif status == grb.NUMERIC: self.results.solver.status = SolverStatus.error self.results.solver.termination_message = ( "Optimization was terminated due to unrecoverable numerical " "difficulties." ) self.results.solver.termination_condition = TerminationCondition.error soln.status = SolutionStatus.error elif status == grb.SUBOPTIMAL: self.results.solver.status = SolverStatus.warning self.results.solver.termination_message = ( "Unable to satisfy optimality tolerances; a sub-optimal " "solution is available." ) self.results.solver.termination_condition = TerminationCondition.other soln.status = SolutionStatus.feasible # note that USER_OBJ_LIMIT was added in Gurobi 7.0, so it may not be present elif (status is not None) and (status == getattr(grb, 'USER_OBJ_LIMIT', None)): self.results.solver.status = SolverStatus.aborted self.results.solver.termination_message = ( "User specified an objective limit " "(a bound on either the best objective " "or the best bound), and that limit has " "been reached. Solution is available." ) self.results.solver.termination_condition = TerminationCondition.other soln.status = SolutionStatus.stoppedByLimit else: self.results.solver.status = SolverStatus.error self.results.solver.termination_message = ( "Unhandled Gurobi solve status (" + str(status) + ")" ) self.results.solver.termination_condition = TerminationCondition.error soln.status = SolutionStatus.error self.results.problem.name = gprob.ModelName if gprob.ModelSense == 1: self.results.problem.sense = minimize elif gprob.ModelSense == -1: self.results.problem.sense = maximize else: raise RuntimeError( 'Unrecognized gurobi objective sense: {0}'.format(gprob.ModelSense) ) self.results.problem.upper_bound = None self.results.problem.lower_bound = None if (gprob.NumBinVars + gprob.NumIntVars) == 0: try: self.results.problem.upper_bound = gprob.ObjVal self.results.problem.lower_bound = gprob.ObjVal except (gurobipy.GurobiError, AttributeError): pass elif gprob.ModelSense == 1: # minimizing try: self.results.problem.upper_bound = gprob.ObjVal except (gurobipy.GurobiError, AttributeError): pass try: self.results.problem.lower_bound = gprob.ObjBound except (gurobipy.GurobiError, AttributeError): pass elif gprob.ModelSense == -1: # maximizing try: self.results.problem.upper_bound = gprob.ObjBound except (gurobipy.GurobiError, AttributeError): pass try: self.results.problem.lower_bound = gprob.ObjVal except (gurobipy.GurobiError, AttributeError): pass else: raise RuntimeError( 'Unrecognized gurobi objective sense: {0}'.format(gprob.ModelSense) ) try: soln.gap = ( self.results.problem.upper_bound - self.results.problem.lower_bound ) except TypeError: soln.gap = None self.results.problem.number_of_constraints = ( gprob.NumConstrs + gprob.NumQConstrs + gprob.NumSOS ) self.results.problem.number_of_nonzeros = gprob.NumNZs self.results.problem.number_of_variables = gprob.NumVars self.results.problem.number_of_binary_variables = gprob.NumBinVars self.results.problem.number_of_integer_variables = gprob.NumIntVars self.results.problem.number_of_continuous_variables = ( gprob.NumVars - gprob.NumIntVars - gprob.NumBinVars ) self.results.problem.number_of_objectives = 1 self.results.problem.number_of_solutions = gprob.SolCount # if a solve was stopped by a limit, we still need to check to # see if there is a solution available - this may not always # be the case, both in LP and MIP contexts. if self._save_results: """ This code in this if statement is only needed for backwards compatibility. It is more efficient to set _save_results to False and use load_vars, load_duals, etc. """ if gprob.SolCount > 0: soln_variables = soln.variable soln_constraints = soln.constraint gurobi_vars = self._solver_model.getVars() gurobi_vars = list( set(gurobi_vars).intersection( set(self._pyomo_var_to_solver_var_map.values()) ) ) var_vals = self._solver_model.getAttr("X", gurobi_vars) names = self._solver_model.getAttr("VarName", gurobi_vars) for gurobi_var, val, name in zip(gurobi_vars, var_vals, names): pyomo_var = self._solver_var_to_pyomo_var_map[gurobi_var] if self._referenced_variables[pyomo_var] > 0: soln_variables[name] = {"Value": val} if extract_reduced_costs: vals = self._solver_model.getAttr("Rc", gurobi_vars) for gurobi_var, val, name in zip(gurobi_vars, vals, names): pyomo_var = self._solver_var_to_pyomo_var_map[gurobi_var] if self._referenced_variables[pyomo_var] > 0: soln_variables[name]["Rc"] = val if extract_duals or extract_slacks: gurobi_cons = self._solver_model.getConstrs() con_names = self._solver_model.getAttr("ConstrName", gurobi_cons) for name in con_names: soln_constraints[name] = {} if self._version_major >= 5: gurobi_q_cons = self._solver_model.getQConstrs() q_con_names = self._solver_model.getAttr( "QCName", gurobi_q_cons ) for name in q_con_names: soln_constraints[name] = {} if extract_duals: vals = self._solver_model.getAttr("Pi", gurobi_cons) for val, name in zip(vals, con_names): soln_constraints[name]["Dual"] = val if self._version_major >= 5: q_vals = self._solver_model.getAttr("QCPi", gurobi_q_cons) for val, name in zip(q_vals, q_con_names): soln_constraints[name]["Dual"] = val if extract_slacks: gurobi_range_con_vars = set(self._solver_model.getVars()) - set( self._pyomo_var_to_solver_var_map.values() ) vals = self._solver_model.getAttr("Slack", gurobi_cons) for gurobi_con, val, name in zip(gurobi_cons, vals, con_names): pyomo_con = self._solver_con_to_pyomo_con_map[gurobi_con] if pyomo_con in self._range_constraints: lin_expr = self._solver_model.getRow(gurobi_con) for i in reversed(range(lin_expr.size())): v = lin_expr.getVar(i) if v in gurobi_range_con_vars: Us_ = v.X Ls_ = v.UB - v.X if Us_ > Ls_: soln_constraints[name]["Slack"] = Us_ else: soln_constraints[name]["Slack"] = -Ls_ break else: soln_constraints[name]["Slack"] = val if self._version_major >= 5: q_vals = self._solver_model.getAttr("QCSlack", gurobi_q_cons) for val, name in zip(q_vals, q_con_names): soln_constraints[name]["Slack"] = val elif self._load_solutions: if gprob.SolCount > 0: self.load_vars() if extract_reduced_costs: self._load_rc() if extract_duals: self._load_duals() if extract_slacks: self._load_slacks() self.results.solution.insert(soln) # finally, clean any temporary files registered with the temp file # manager, created populated *directly* by this plugin. TempfileManager.pop(remove=not self._keepfiles) return DirectOrPersistentSolver._postsolve(self) def warm_start_capable(self): return True def _warm_start(self): for pyomo_var, gurobipy_var in self._pyomo_var_to_solver_var_map.items(): if pyomo_var.value is not None: gurobipy_var.setAttr(gurobipy.GRB.Attr.Start, value(pyomo_var)) self._needs_updated = True def _load_vars(self, vars_to_load=None): var_map = self._pyomo_var_to_solver_var_map ref_vars = self._referenced_variables if vars_to_load is None: vars_to_load = var_map.keys() gurobi_vars_to_load = [var_map[pyomo_var] for pyomo_var in vars_to_load] vals = self._solver_model.getAttr("X", gurobi_vars_to_load) for var, val in zip(vars_to_load, vals): if ref_vars[var] > 0: var.set_value(val, skip_validation=True) def _load_rc(self, vars_to_load=None): if not hasattr(self._pyomo_model, 'rc'): self._pyomo_model.rc = Suffix(direction=Suffix.IMPORT) var_map = self._pyomo_var_to_solver_var_map ref_vars = self._referenced_variables rc = self._pyomo_model.rc if vars_to_load is None: vars_to_load = var_map.keys() gurobi_vars_to_load = [var_map[pyomo_var] for pyomo_var in vars_to_load] vals = self._solver_model.getAttr("Rc", gurobi_vars_to_load) for var, val in zip(vars_to_load, vals): if ref_vars[var] > 0: rc[var] = val def _load_duals(self, cons_to_load=None): if not hasattr(self._pyomo_model, 'dual'): self._pyomo_model.dual = Suffix(direction=Suffix.IMPORT) con_map = self._pyomo_con_to_solver_con_map reverse_con_map = self._solver_con_to_pyomo_con_map dual = self._pyomo_model.dual if cons_to_load is None: linear_cons_to_load = self._solver_model.getConstrs() if self._version_major >= 5: quadratic_cons_to_load = self._solver_model.getQConstrs() else: gurobi_cons_to_load = set( [con_map[pyomo_con] for pyomo_con in cons_to_load] ) linear_cons_to_load = gurobi_cons_to_load.intersection( set(self._solver_model.getConstrs()) ) if self._version_major >= 5: quadratic_cons_to_load = gurobi_cons_to_load.intersection( set(self._solver_model.getQConstrs()) ) linear_vals = self._solver_model.getAttr("Pi", linear_cons_to_load) if self._version_major >= 5: quadratic_vals = self._solver_model.getAttr("QCPi", quadratic_cons_to_load) for gurobi_con, val in zip(linear_cons_to_load, linear_vals): pyomo_con = reverse_con_map[gurobi_con] dual[pyomo_con] = val if self._version_major >= 5: for gurobi_con, val in zip(quadratic_cons_to_load, quadratic_vals): pyomo_con = reverse_con_map[gurobi_con] dual[pyomo_con] = val def _load_slacks(self, cons_to_load=None): if not hasattr(self._pyomo_model, 'slack'): self._pyomo_model.slack = Suffix(direction=Suffix.IMPORT) con_map = self._pyomo_con_to_solver_con_map reverse_con_map = self._solver_con_to_pyomo_con_map slack = self._pyomo_model.slack gurobi_range_con_vars = set(self._solver_model.getVars()) - set( self._pyomo_var_to_solver_var_map.values() ) if cons_to_load is None: linear_cons_to_load = self._solver_model.getConstrs() if self._version_major >= 5: quadratic_cons_to_load = self._solver_model.getQConstrs() else: gurobi_cons_to_load = set( [con_map[pyomo_con] for pyomo_con in cons_to_load] ) linear_cons_to_load = gurobi_cons_to_load.intersection( set(self._solver_model.getConstrs()) ) if self._version_major >= 5: quadratic_cons_to_load = gurobi_cons_to_load.intersection( set(self._solver_model.getQConstrs()) ) linear_vals = self._solver_model.getAttr("Slack", linear_cons_to_load) if self._version_major >= 5: quadratic_vals = self._solver_model.getAttr( "QCSlack", quadratic_cons_to_load ) for gurobi_con, val in zip(linear_cons_to_load, linear_vals): pyomo_con = reverse_con_map[gurobi_con] if pyomo_con in self._range_constraints: lin_expr = self._solver_model.getRow(gurobi_con) for i in reversed(range(lin_expr.size())): v = lin_expr.getVar(i) if v in gurobi_range_con_vars: Us_ = v.X Ls_ = v.UB - v.X if Us_ > Ls_: slack[pyomo_con] = Us_ else: slack[pyomo_con] = -Ls_ break else: slack[pyomo_con] = val if self._version_major >= 5: for gurobi_con, val in zip(quadratic_cons_to_load, quadratic_vals): pyomo_con = reverse_con_map[gurobi_con] slack[pyomo_con] = val 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) 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) def load_slacks(self, cons_to_load=None): """ Load the values of the slack variables into the 'slack' suffix. The 'slack' suffix must live on the parent model. Parameters ---------- cons_to_load: list of Constraint """ self._load_slacks(cons_to_load) def _update(self): self._solver_model.update() self._needs_updated = False