diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 8c520fc4b9a..116f67806d7 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -9,11 +9,55 @@ # ___________________________________________________________________________ -from pyomo.common.config import ConfigBlock, ConfigValue, NonNegativeFloat +from pyomo.common.config import ConfigBlock, ConfigValue, NonNegativeFloat, In from pyomo.common.errors import DeveloperError from pyomo.common.deprecation import deprecated +from pyomo.core.base.suffix import Suffix +import six +import abc +import enum + + +class TerminationCondition(enum.Enum): + """ + An enumeration for checking the termination condition of solvers + """ + unknown = 0 + """unknown serves as both a default value, and it is used when no other enum member makes sense""" + + maxTimeLimit = 1 + """The solver exited due to a time limit""" + + maxIterations = 2 + """The solver exited due to an iteration limit """ + + objectiveLimit = 3 + """The solver exited due to an objective limit""" + + minStepLength = 4 + """The solver exited due to a minimum step length""" + + optimal = 5 + """The solver exited with the optimal solution""" + + unbounded = 8 + """The solver exited because the problem is unbounded""" + + infeasible = 9 + """The solver exited because the problem is infeasible""" + + infeasibleOrUnbounded = 10 + """The solver exited because the problem is either infeasible or unbounded""" + + error = 11 + """The solver exited due to an error""" + + interrupted = 12 + """The solver exited because it was interrupted""" + + licensingProblems = 13 + """The solver exited due to licensing problems""" -from pyomo.opt.results import SolverResults class Solver(object): """A generic optimization solver""" @@ -94,3 +138,224 @@ class MIPSolver(Solver): domain=bool, )) + +class SolutionLoaderBase(six.with_metaclass(abc.ABCMeta, object)): + @abc.abstractmethod + def load_solution(self): + """ + Load the solution into the model. This will load the values of the primal variables into + the value attribute of the variables, the duals into the model.dual suffix if it exists, + the slacks into the model.slack suffix if it exists, and reduced costs into the + model.rc suffix if it exists. + """ + pass + + @abc.abstractmethod + def load_vars(self, vars_to_load=None): + """ + Load the solution of the primal variables into the value attribut of the variables. + + Parameters + ---------- + vars_to_load: list + A list of the variables whose solution should be loaded. If vars_to_load is None, then the solution + to all primal variables will be loaded. + """ + pass + + @abc.abstractmethod + def load_duals(self, cons_to_load=None): + """ + Load the duals into the model.dual suffix. If the model.dual suffix does not exist it will be created. + + Parameters + ---------- + cons_to_load: list + A list of the constraints whose duals should be loaded. If cons_to_load is None, then the duals for all + constraints will be loaded. + """ + pass + + @abc.abstractmethod + def load_slacks(self, cons_to_load=None): + """ + Load the slacks into the model.slack suffix. If the model.slack suffix does not exist it will be created. + + Parameters + ---------- + cons_to_load: list + A list of the constraints whose slacks should be loaded. If cons_to_load is None, then the slacks for all + constraints will be loaded. + """ + pass + + @abc.abstractmethod + def load_reduced_costs(self, vars_to_load=None): + """ + Load the reduced costs into the model.rc suffix. If the model.rc suffix does not exist it will be created. + + Parameters + ---------- + vars_to_load: list + A list of the variables whose reduced cost should be loaded. If vars_to_load is None, then all reduced costs + will be loaded. + """ + pass + + +class SolutionLoader(SolutionLoaderBase): + def __init__(self, model, primals, duals, slacks, reduced_costs): + """ + Parameters + ---------- + model: the pyomo model + primals: ComponentMap + maps Var to value + duals: ComponentMap + maps Constraint to dual value + slacks: ComponentMap + maps Constraint to slack value + reduced_costs: ComponentMap + maps Var to reduced cost + """ + self._model = model + self._primals = primals + self._duals = duals + self._slacks = slacks + self._reduced_costs = reduced_costs + + def load_solution(self): + for v, val in self._primals.items(): + v.value = val + + if hasattr(self._model, 'dual'): + for c, val in self._duals.items(): + self._model.dual[c] = val + + if hasattr(self._model, 'slack'): + for c, val in self._slacks.items(): + self._model.slack[c] = val + + if hasattr(self._model, 'rc'): + for v, val in self._reduced_costs.items(): + self._model.rc[v] = val + + def load_vars(self, vars_to_load=None): + if vars_to_load is None: + for v, val in self._primals.items(): + v.value = val + else: + for v in vars_to_load: + v.value = self._primals[v] + + def load_duals(self, cons_to_load=None): + if not hasattr(self._model, 'dual'): + self._model.dual = Suffix(direction=Suffix.IMPORT) + + if cons_to_load is None: + for c, val in self._duals.items(): + self._model.dual[c] = val + else: + for c in cons_to_load: + self._model.dual[c] = self._duals[c] + + def load_slacks(self, cons_to_load=None): + if not hasattr(self._model, 'slack'): + self._model.slack = Suffix(direction=Suffix.IMPORT) + + if cons_to_load is None: + for c, val in self._slacks.items(): + self._model.slack[c] = val + else: + for c in cons_to_load: + self._model.slack[c] = self._slacks[c] + + def load_reduced_costs(self, vars_to_load=None): + if not hasattr(self._model, 'rc'): + self._model.rc = Suffix(direction=Suffix.IMPORT) + + if vars_to_load is None: + for v, val in self._reduced_costs.items(): + self._model.rc[v] = val + else: + for v in vars_to_load: + self._model.rc[v] = self._reduced_costs[v] + + +class ResultsBase(six.with_metaclass(abc.ABCMeta, object)): + """ + The results object has three primary roles: + + 1. Report information from the solver. This is done through results.solver, which stores three or more attributes + with information from the solver. At a minimum, results.solver must have a termination_condition, + best_feasible_objective, and best_objective_bound. + + 2. The results object has a method called found_feasible_solution, which returns True if at least one + feasible solution was found and False otherwise. + + 3. The results object has an attribute called solution_loader which should be an instance of SolutionLoader. The + SolutionLoader has a method called load_solution which handles loading solutions into the model. + + Here is an example workflow: + >>> import pyomo.environ as pe + >>> m = pe.ConcreteModel() + >>> m.x = pe.Var() + >>> m.obj = pe.Objective(expr=m.x**2) + >>> opt = pe.SolverFactory('my_solver') + >>> results = opt.solve(m, load_solution=False) + >>> if results.solver.termination_condition == TerminationCondition.optimal: + >>> print('optimal solution found: ', results.solver.best_feasible_objective) + >>> results.solution_loader.load_solution() + >>> print('the optimal value of x is ', m.x.value) + >>> elif results.found_feasible_solution(): + >>> print('sub-optimal but feasible solution found: ', results.solver.best_feasible_objective) + >>> results.solution_loader.load_vars(vars_to_load=[m.x]) + >>> print('The value of x in the feasible solution is ', m.x.value) + >>> elif results.solver.termination_condition in {TerminationCondition.maxIterations, + ... TerminationCondition.maxTimeLimit}: + >>> print('No feasible solution was found. The best lower bound found was ', + ... results.solver.best_objective_bound) + >>> else: + >>> print('The following termination condition was encountered: ', + ... results.solver.termination_condition) + """ + def __init__(self): + self.solution_loader = None + self.solver = ConfigBlock() + self.solver.declare('termination_condition', + ConfigValue(default=TerminationCondition.unknown, + domain=In(TerminationCondition), + doc="The reason the solver exited. This is a member of the " + "TerminationCondition enum.")) + self.solver.declare('best_feasible_objective', + ConfigValue(default=None, + domain=float, + doc="If a feasible solution was found, this is the objective value of " + "the best solution found. If no feasible solution was found, this is" + "None.")) + self.solver.declare('best_objective_bound', + ConfigValue(default=None, + domain=float, + doc="The best objective bound found. For minimization problems, this is " + "the lower bound. For maximization problems, this is the upper bound." + "For solvers that do not provide an objective bound, this should be -inf " + "(minimization) or inf (maximization)")) + + @abc.abstractmethod + def found_feasible_solution(self): + """ + Returns + ------- + found_feasible_solution: bool + True if at least one feasible solution was found. False otherwise. + """ + pass + + +class Results(ResultsBase): + def __init__(self, found_feasible_solution): + super(Results, self).__init__() + self._found_feasible_solution = found_feasible_solution + + def found_feasible_solution(self): + return self._found_feasible_solution diff --git a/pyomo/solver/gurobi_auto.py b/pyomo/solver/gurobi_auto.py deleted file mode 100644 index 7c9c6009cac..00000000000 --- a/pyomo/solver/gurobi_auto.py +++ /dev/null @@ -1,892 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright 2017 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 -import pyomo.common -from pyutilib.misc import Bunch -from pyutilib.services import TempfileManager -from pyomo.core.expr.numvalue import is_fixed, value, is_constant -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.core.kernel.component_set import ComponentSet -from pyomo.core.kernel.component_map import ComponentMap -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 -from pyomo.core.base.var import Var -from pyomo.core.base.param import Param, _ParamData, SimpleParam -from pyomo.core.base.constraint import Constraint -from pyomo.core.base.sos import SOSConstraint -from pyomo.core.base.objective import Objective -from pyomo.common.config import ConfigBlock, ConfigValue, add_docstring_list -from pyomo.solver.base import MIPSolver, Solver -from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler -from pyomo.core.expr.visitor import StreamBasedExpressionVisitor -from pyomo.core.expr.numvalue import native_numeric_types, native_types -import pyomo.core.expr.numeric_expr as numeric_expr -from pyomo.core.base.expression import _GeneralExpressionData, SimpleExpression - - -logger = logging.getLogger('pyomo.solvers') - - -class DegreeError(ValueError): - pass - - -def _is_numeric(x): - try: - float(x) - except ValueError: - return False - return True - - -def _get_objective(block): - obj = None - for o in block.component_data_objects(Objective, descend_into=True, active=True, sort=True): - if obj is not None: - raise ValueError('Multiple active objectives found') - obj = o - return obj - - -class _MutableLinearCoefficient(object): - def __init__(self): - self.expr = None - self.var = None - self.con = None - self.gurobi_model = None - - def update(self): - self.gurobi_model.chgCoeff(self.con, self.var, value(self.expr)) - - def __str__(self): - s = str(self.var) + ': ' + str(self.expr) - return s - - -class _MutableRangeConstant(object): - def __init__(self): - self.lhs_expr = None - self.rhs_expr = None - self.con = None - self.slack = None - - def update(self): - rhs_val = value(self.rhs_expr) - lhs_val = value(self.lhs_expr) - self.con.rhs = rhs_val - self.slack.ub = rhs_val - lhs_val - - -class _MutableConstant(object): - def __init__(self): - self.expr = None - self.con = None - - def update(self): - self.con.rhs = value(self.expr) - - -class _MutableQuadraticConstraint(object): - def __init__(self, gurobi_model, gurobi_con, constant, linear_coefs, quadratic_coefs): - self.con = gurobi_con - self.gurobi_model = gurobi_model - self.constant = constant - self.last_constant_value = value(self.constant) - self.linear_coefs = linear_coefs - self.last_linear_coef_values = [value(i.expr) for i in self.linear_coefs] - self.quadratic_coefs = quadratic_coefs - self.last_quadratic_coef_values = [value(i.expr) for i in self.quadratic_coefs] - - def get_updated_expression(self): - gurobi_expr = self.gurobi_model.getQCRow(self.con) - for ndx, coef in enumerate(self.linear_coefs): - new_coef_value = value(coef.expr) - self.last_linear_coef_values[ndx] - gurobi_expr += new_coef_value * coef.var - self.last_linear_coef_values[ndx] = new_coef_value - for ndx, coef in enumerate(self.quadratic_coefs): - new_coef_value = value(coef.expr) - self.last_quadratic_coef_values[ndx] - gurobi_expr += new_coef_value * coef.var1 * coef.var2 - self.last_quadratic_coef_values[ndx] = new_coef_value - return gurobi_expr - - def get_updated_rhs(self): - return value(self.constant.expr) - - -class _MutableObjective(object): - def __init__(self, gurobi_model, constant, linear_coefs, quadratic_coefs): - self.gurobi_model = gurobi_model - self.constant = constant - self.last_constant_value = value(self.constant.expr) - self.linear_coefs = linear_coefs - self.last_linear_coef_values = [value(i.expr) for i in self.linear_coefs] - self.quadratic_coefs = quadratic_coefs - self.last_quadratic_coef_values = [value(i.expr) for i in self.quadratic_coefs] - - def get_updated_expression(self): - gurobi_expr = self.gurobi_model.getObjective() - new_const_value = value(self.constant.expr) - self.last_constant_value - gurobi_expr += new_const_value - self.last_constant_value = new_const_value - for ndx, coef in enumerate(self.linear_coefs): - new_coef_value = value(coef.expr) - self.last_linear_coef_values[ndx] - gurobi_expr += new_coef_value * coef.var - self.last_linear_coef_values[ndx] = new_coef_value - for ndx, coef in enumerate(self.quadratic_coefs): - new_coef_value = value(coef.expr) - self.last_quadratic_coef_values[ndx] - gurobi_expr += new_coef_value * coef.var1 * coef.var2 - self.last_quadratic_coef_values[ndx] = new_coef_value - return gurobi_expr - - -class _MutableQuadraticCoefficient(object): - def __init__(self): - self.expr = None - self.var1 = None - self.var2 = None - - -@SolverFactory.register('gurobi_auto', doc='Direct python interface to Gurobi with automatic updates') -class GurobiAuto(MIPSolver): - """ - Direct interface to Gurobi - """ - CONFIG = Solver.CONFIG() - MAPPED_OPTIONS = MIPSolver.MAPPED_OPTIONS() - - CONFIG.declare('symbolic_solver_labels', ConfigValue(default=False, domain=bool, - doc='If True, the gurobi variable and constraint names ' - 'will match those of the pyomo variables and constrains')) - CONFIG.declare('stream_solver', ConfigValue(default=False, domain=bool, - doc='If True, show the Gurobi output')) - CONFIG.declare('load_solutions', ConfigValue(default=True, domain=bool, - doc='If True, load the solution back into the Pyomo model')) - - __doc__ = add_docstring_list(__doc__, CONFIG) - - def __init__(self): - super(GurobiAuto, self).__init__() - - self._pyomo_model = None - self._solver_model = None - self._symbol_map = SymbolMap() - self._labeler = None - self._pyomo_var_to_solver_var_map = dict() - self._solver_var_to_pyomo_var_map = dict() - self._pyomo_con_to_solver_con_map = dict() - self._solver_con_to_pyomo_con_map = dict() - self._vars_referenced_by_con = dict() - self._vars_referenced_by_obj = dict() - self._objective = None - self._objective_expr = None - self._referenced_variables = dict() - self._referenced_params = dict() - self._range_constraints = set() - self._tmp_config = None - self._tmp_mapped_options = None - self._tmp_options = None - self._mutable_helpers = list() - self._mutable_quadratic_helpers = list() - self._mutable_objective = None - - try: - import gurobipy - self._gurobipy = gurobipy - self._python_api_exists = True - except Exception as e: - logger.warning("Import of gurobipy failed - gurobi message=" + str(e) + "\n") - self._python_api_exists = False - - def available(self): - return self._python_api_exists - - def license_status(self): - try: - tmp = self._gurobipy.Model() - return True - except self._gurobipy.GurobiError: - return False - - def solve(self, model, options=None, mapped_options=None, **config_options): - """ - solve a model - """ - self._tmp_options = self.options(options) - self._tmp_mapped_options = self.mapped_options(mapped_options) - self._tmp_config = self.config(config_options) - if model is self._pyomo_model: - self._update() - else: - self._set_instance(model) - self._apply_solver() - return self._postsolve() - - def _apply_solver(self): - if self._tmp_config.stream_solver: - self._solver_model.setParam('OutputFlag', 1) - else: - self._solver_model.setParam('OutputFlag', 0) - - for key, option in self._tmp_options.items(): - self._solver_model.setParam(key, option) - self._solver_model.optimize() - - def _add_var(self, var): - varname = self._symbol_map.getSymbol(var, self._labeler) - vtype = self._gurobi_vtype_from_var(var) - if var.has_lb(): - lb = value(var.lb) - else: - lb = -self._gurobipy.GRB.INFINITY - if var.has_ub(): - ub = value(var.ub) - else: - ub = self._gurobipy.GRB.INFINITY - if var.is_fixed(): - lb = value(var.value) - ub = value(var.value) - - gurobipy_var = self._solver_model.addVar(lb=lb, ub=ub, vtype=vtype, name=varname) - - self._pyomo_var_to_solver_var_map[id(var)] = gurobipy_var - self._solver_var_to_pyomo_var_map[id(gurobipy_var)] = var - self._referenced_variables[id(var)] = 0 - - def _set_instance(self, model): - self._pyomo_model = model - self._symbol_map = SymbolMap() - self._labeler = None - self._pyomo_var_to_solver_var_map = dict() - self._solver_var_to_pyomo_var_map = dict() - self._pyomo_con_to_solver_con_map = dict() - self._solver_con_to_pyomo_con_map = dict() - self._vars_referenced_by_con = dict() - self._vars_referenced_by_obj = dict() - self._objective = None - self._referenced_variables = dict() - self._referenced_params = dict() - self._param_to_con_map = dict() - self._range_constraints = set() - self._mutable_helpers = list() - self._mutable_quadratic_helpers = list() - - if self._tmp_config.symbolic_solver_labels: - self._labeler = TextLabeler() - else: - self._labeler = NumericLabeler('x') - - if model.name is not None: - self._solver_model = self._gurobipy.Model(model.name) - else: - self._solver_model = self._gurobipy.Model() - - for var in model.component_data_objects(Var, descend_into=True, sort=True): - self._add_var(var) - - for con in model.component_data_objects(Constraint, descend_into=True, active=True, sort=True): - self._add_constraint(con) - - for con in model.component_data_objects(SOSConstraint, descend_into=True, active=True, sort=True): - self._add_sos_constraint(con) - - self._set_objective(_get_objective(model)) - - def _get_expr_from_pyomo_expr(self, expr): - mutable_linear_coefficients = list() - mutable_quadratic_coefficients = list() - repn = generate_standard_repn(expr, quadratic=True, compute_values=False) - referenced_vars = ComponentSet() - - degree = repn.polynomial_degree() - if (degree is None) or (degree > 2): - raise DegreeError('GurobiAuto does not support expressions of degree {0}.'.format(degree)) - - if len(repn.linear_vars) > 0: - referenced_vars.update(repn.linear_vars) - linear_coef_vals = list() - for ndx, coef in enumerate(repn.linear_coefs): - if not is_constant(coef): - mutable_linear_coefficient = _MutableLinearCoefficient() - mutable_linear_coefficient.expr = coef - mutable_linear_coefficient.var = self._pyomo_var_to_solver_var_map[id(repn.linear_vars[ndx])] - mutable_linear_coefficients.append(mutable_linear_coefficient) - linear_coef_vals.append(value(coef)) - new_expr = self._gurobipy.LinExpr(linear_coef_vals, [self._pyomo_var_to_solver_var_map[id(i)] for i in repn.linear_vars]) - else: - new_expr = 0.0 - - for ndx,v in enumerate(repn.quadratic_vars): - x,y = v - gurobi_x = self._pyomo_var_to_solver_var_map[id(x)] - gurobi_y = self._pyomo_var_to_solver_var_map[id(y)] - coef = repn.quadratic_coefs[ndx] - if not is_constant(coef): - mutable_quadratic_coefficient = _MutableQuadraticCoefficient() - mutable_quadratic_coefficient.expr = coef - mutable_quadratic_coefficient.var1 = gurobi_x - mutable_quadratic_coefficient.var2 = gurobi_y - mutable_quadratic_coefficients.append(mutable_quadratic_coefficient) - coef_val = value(coef) - new_expr += coef_val * gurobi_x * gurobi_y - referenced_vars.add(x) - referenced_vars.add(y) - - #if not is_constant(repn.constant): - # mutable_constant = _MutableConstant() - # mutable_constant.expr = repn.constant - #constant_val = value(repn.constant) - #new_expr += constant_val - - return new_expr, referenced_vars, repn.constant, mutable_linear_coefficients, mutable_quadratic_coefficients - - def _add_constraint(self, con): - assert con.active - - conname = self._symbol_map.getSymbol(con, self._labeler) - - (gurobi_expr, - referenced_vars, - repn_constant, - mutable_linear_coefficients, - mutable_quadratic_coefficients) = self._get_expr_from_pyomo_expr(con.body) - - if gurobi_expr.__class__ is self._gurobipy.LinExpr: - if con.equality: - rhs_expr = con.lower - repn_constant - rhs_val = value(rhs_expr) - gurobipy_con = self._solver_model.addLConstr(gurobi_expr, - self._gurobipy.GRB.EQUAL, - rhs_val, - name=conname) - if not is_constant(rhs_expr): - mutable_constant = _MutableConstant() - mutable_constant.expr = rhs_expr - mutable_constant.con = gurobipy_con - self._mutable_helpers.append(mutable_constant) - elif con.has_lb() and con.has_ub(): - lhs_expr = con.lower - repn_constant - rhs_expr = con.upper - repn_constant - lhs_val = value(lhs_expr) - rhs_val = value(rhs_expr) - gurobipy_con = self._solver_model.addRange(gurobi_expr, lhs_val, rhs_val, name=conname) - self._range_constraints.add(con) - if not is_constant(lhs_expr) or not is_constant(rhs_expr): - mutable_range_constant = _MutableRangeConstant() - mutable_range_constant.lhs_expr = lhs_expr - mutable_range_constant.rhs_expr = rhs_expr - mutable_range_constant.con = gurobipy_con - mutable_range_constant.slack = self._solver_model.getVarByName('Rg'+conname) - self._mutable_helpers.append(mutable_range_constant) - elif con.has_lb(): - rhs_expr = con.lower - repn_constant - rhs_val = value(rhs_expr) - gurobipy_con = self._solver_model.addLConstr(gurobi_expr, self._gurobipy.GRB.GREATER_EQUAL, rhs_val, name=conname) - if not is_constant(rhs_expr): - mutable_constant = _MutableConstant() - mutable_constant.expr = rhs_expr - mutable_constant.con = gurobipy_con - self._mutable_helpers.append(mutable_constant) - elif con.has_ub(): - rhs_expr = con.upper - repn_constant - rhs_val = value(rhs_expr) - gurobipy_con = self._solver_model.addLConstr(gurobi_expr, self._gurobipy.GRB.LESS_EQUAL, rhs_val, name=conname) - if not is_constant(rhs_expr): - mutable_constant = _MutableConstant() - mutable_constant.expr = rhs_expr - mutable_constant.con = gurobipy_con - self._mutable_helpers.append(mutable_constant) - else: - raise ValueError("Constraint does not have a lower " - "or an upper bound: {0} \n".format(con)) - for tmp in mutable_linear_coefficients: - tmp.con = gurobipy_con - tmp.gurobi_model = self._solver_model - self._mutable_helpers.extend(mutable_linear_coefficients) - elif gurobi_expr.__class__ is self._gurobipy.QuadExpr: - if con.equality: - raise NotImplementedError('Quadratic equality constraints are not supported') - elif con.has_lb() and con.has_ub(): - raise NotImplementedError('Quadratic range constraints are not supported') - elif con.has_lb(): - rhs_expr = con.lower - repn_constant - rhs_val = value(rhs_expr) - gurobipy_con = self._solver_model.addQConstr(gurobi_expr, self._gurobipy.GRB.GREATER_EQUAL, rhs_val, name=conname) - elif con.has_ub(): - rhs_expr = con.upper - repn_constant - rhs_val = value(rhs_expr) - gurobipy_con = self._solver_model.addQConstr(gurobi_expr, self._gurobipy.GRB.LESS_EQUAL, rhs_val, name=conname) - else: - raise ValueError("Constraint does not have a lower " - "or an upper bound: {0} \n".format(con)) - if len(mutable_linear_coefficients) > 0 or len(mutable_quadratic_coefficients) > 0 or not is_constant(repn_constant): - mutable_constant = _MutableConstant() - mutable_constant.expr = repn_constant - mutable_quadratic_constraint = _MutableQuadraticConstraint(self._solver_model, gurobipy_con, - mutable_constant, - mutable_linear_coefficients, - mutable_quadratic_coefficients) - self._mutable_quadratic_helpers.append(mutable_quadratic_constraint) - - for var in referenced_vars: - self._referenced_variables[id(var)] += 1 - self._vars_referenced_by_con[id(con)] = referenced_vars - self._pyomo_con_to_solver_con_map[id(con)] = gurobipy_con - self._solver_con_to_pyomo_con_map[id(gurobipy_con)] = con - - def _add_sos_constraint(self, con): - assert con.active() - - conname = self._symbol_map.getSymbol(con, self._labeler) - level = con.level - if level == 1: - sos_type = self._gurobipy.GRB.SOS_TYPE1 - elif level == 2: - sos_type = self._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[id(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[id(con)].add(v) - gurobi_vars.append(self._pyomo_var_to_solver_var_map[id(v)]) - self._referenced_variables[id(v)] += 1 - weights.append(w) - - gurobipy_con = self._solver_model.addSOS(sos_type, gurobi_vars, weights) - self._pyomo_con_to_solver_con_map[id(con)] = gurobipy_con - self._solver_con_to_pyomo_con_map[id(gurobipy_con)] = con - - def _remove_constraint(self, con): - solver_con = self._pyomo_con_to_solver_con_map[id(con)] - self._solver_model.remove(solver_con) - self._symbol_map.removeSymbol(con) - self._labeler.remove_obj(con) - for var in self._vars_referenced_by_con[id(con)]: - self._referenced_variables[id(var)] -= 1 - del self._vars_referenced_by_con[id(con)] - del self._pyomo_con_to_solver_con_map[id(con)] - del self._solver_con_to_pyomo_con_map[id(solver_con)] - self._range_constraints.discard(con) - - def _remove_sos_constraint(self, con): - solver_sos_con = self._pyomo_con_to_solver_con_map[id(con)] - self._symbol_map.removeSymbol(con) - self._labeler.remove_obj(con) - for var in self._vars_referenced_by_con[id(con)]: - self._referenced_variables[id(var)] -= 1 - del self._vars_referenced_by_con[id(con)] - del self._pyomo_con_to_solver_con_map[id(con)] - del self._solver_con_to_pyomo_con_map[id(solver_con)] - - def _remove_var(self, solver_var): - if self._referenced_variables[id(var)] != 0: - raise ValueError('Cannot remove Var {0} because it is still referenced by the objective or one or more constraints'.format(var)) - solver_var = self._pyomo_var_to_solver_var_map[id(var)] - self._solver_model.remove(solver_var) - self._symbol_map.removeSymbol(var) - self._labeler.remove_obj(var) - del self._referenced_variables[id(var)] - del self._pyomo_var_to_solver_var_map[id(var)] - del self._solver_var_to_pyomo_var_map[id(solver_var)] - - 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 = self._gurobipy.GRB.BINARY - elif var.is_integer(): - vtype = self._gurobipy.GRB.INTEGER - elif var.is_continuous(): - vtype = self._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[id(var)] -= 1 - self._vars_referenced_by_obj = ComponentSet() - self._objective = None - self._objective_expr = None - - if obj.active is False: - raise ValueError('Cannot add inactive objective to solver.') - - if obj.sense == minimize: - sense = self._gurobipy.GRB.MINIMIZE - elif obj.sense == maximize: - sense = self._gurobipy.GRB.MAXIMIZE - else: - raise ValueError('Objective sense is not recognized: {0}'.format(obj.sense)) - - (gurobi_expr, - referenced_vars, - repn_constant, - mutable_linear_coefficients, - mutable_quadratic_coefficients) = self._get_expr_from_pyomo_expr(obj.expr) - mutable_constant = _MutableConstant() - mutable_constant.expr = repn_constant - mutable_objective = _MutableObjective(self._solver_model, - mutable_constant, - mutable_linear_coefficients, - mutable_quadratic_coefficients) - self._mutable_objective = mutable_objective - - for var in referenced_vars: - self._referenced_variables[id(var)] += 1 - - self._solver_model.setObjective(gurobi_expr + value(repn_constant), sense=sense) - self._objective = obj - self._objective_expr = obj.expr - self._vars_referenced_by_obj = referenced_vars - - 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. - suffixes = list(self._pyomo_model.component_objects(Suffix, active=True, descend_into=False, sort=True)) - extract_duals = False - extract_slacks = False - extract_reduced_costs = False - for suffix in 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 = self._gurobipy.GRB - status = gprob.Status - - if gprob.getAttr(self._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 - - results = SolverResults() - results.solver.wallclock_time = gprob.Runtime - - if status == grb.LOADED: # problem is loaded, but no solution - results.solver.status = SolverStatus.aborted - results.solver.termination_message = "Model is loaded, but no solution information is available." - results.solver.termination_condition = TerminationCondition.error - elif status == grb.OPTIMAL: # optimal - results.solver.status = SolverStatus.ok - results.solver.termination_message = "Model was solved to optimality (subject to tolerances), " \ - "and an optimal solution is available." - results.solver.termination_condition = TerminationCondition.optimal - elif status == grb.INFEASIBLE: - results.solver.status = SolverStatus.warning - results.solver.termination_message = "Model was proven to be infeasible" - results.solver.termination_condition = TerminationCondition.infeasible - elif status == grb.INF_OR_UNBD: - results.solver.status = SolverStatus.warning - results.solver.termination_message = "Problem proven to be infeasible or unbounded." - results.solver.termination_condition = TerminationCondition.infeasibleOrUnbounded - elif status == grb.UNBOUNDED: - results.solver.status = SolverStatus.warning - results.solver.termination_message = "Model was proven to be unbounded." - results.solver.termination_condition = TerminationCondition.unbounded - elif status == grb.CUTOFF: - results.solver.status = SolverStatus.aborted - 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." - results.solver.termination_condition = TerminationCondition.minFunctionValue - elif status == grb.ITERATION_LIMIT: - results.solver.status = SolverStatus.aborted - results.solver.termination_message = "Optimization terminated because the total number of simplex " \ - "iterations performed exceeded the value specified in the " \ - "IterationLimit parameter." - results.solver.termination_condition = TerminationCondition.maxIterations - elif status == grb.NODE_LIMIT: - results.solver.status = SolverStatus.aborted - results.solver.termination_message = "Optimization terminated because the total number of " \ - "branch-and-cut nodes explored exceeded the value specified " \ - "in the NodeLimit parameter" - results.solver.termination_condition = TerminationCondition.maxEvaluations - elif status == grb.TIME_LIMIT: - results.solver.status = SolverStatus.aborted - results.solver.termination_message = "Optimization terminated because the time expended exceeded " \ - "the value specified in the TimeLimit parameter." - results.solver.termination_condition = TerminationCondition.maxTimeLimit - elif status == grb.SOLUTION_LIMIT: - results.solver.status = SolverStatus.aborted - results.solver.termination_message = "Optimization terminated because the number of solutions found " \ - "reached the value specified in the SolutionLimit parameter." - results.solver.termination_condition = TerminationCondition.unknown - elif status == grb.INTERRUPTED: - results.solver.status = SolverStatus.aborted - results.solver.termination_message = "Optimization was terminated by the user." - results.solver.termination_condition = TerminationCondition.error - elif status == grb.NUMERIC: - results.solver.status = SolverStatus.error - results.solver.termination_message = "Optimization was terminated due to unrecoverable numerical " \ - "difficulties." - results.solver.termination_condition = TerminationCondition.error - elif status == grb.SUBOPTIMAL: - results.solver.status = SolverStatus.warning - results.solver.termination_message = "Unable to satisfy optimality tolerances; a sub-optimal " \ - "solution is available." - results.solver.termination_condition = TerminationCondition.other - # 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)): - results.solver.status = SolverStatus.aborted - 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." - results.solver.termination_condition = TerminationCondition.other - else: - results.solver.status = SolverStatus.error - results.solver.termination_message = \ - ("Unhandled Gurobi solve status " - "("+str(status)+")") - results.solver.termination_condition = TerminationCondition.error - - results.problem.name = gprob.ModelName - - if gprob.ModelSense == 1: - results.problem.sense = minimize - elif gprob.ModelSense == -1: - results.problem.sense = maximize - else: - raise RuntimeError('Unrecognized gurobi objective sense: {0}'.format(gprob.ModelSense)) - - results.problem.upper_bound = None - results.problem.lower_bound = None - if (gprob.NumBinVars + gprob.NumIntVars) == 0: - try: - results.problem.upper_bound = gprob.ObjVal - results.problem.lower_bound = gprob.ObjVal - except (self._gurobipy.GurobiError, AttributeError): - pass - elif gprob.ModelSense == 1: # minimizing - try: - results.problem.upper_bound = gprob.ObjVal - except (self._gurobipy.GurobiError, AttributeError): - pass - try: - results.problem.lower_bound = gprob.ObjBound - except (self._gurobipy.GurobiError, AttributeError): - pass - elif gprob.ModelSense == -1: # maximizing - try: - results.problem.upper_bound = gprob.ObjBound - except (self._gurobipy.GurobiError, AttributeError): - pass - try: - results.problem.lower_bound = gprob.ObjVal - except (self._gurobipy.GurobiError, AttributeError): - pass - else: - raise RuntimeError('Unrecognized gurobi objective sense: {0}'.format(gprob.ModelSense)) - - results.problem.number_of_constraints = gprob.NumConstrs + gprob.NumQConstrs + gprob.NumSOS - results.problem.number_of_nonzeros = gprob.NumNZs - results.problem.number_of_variables = gprob.NumVars - results.problem.number_of_binary_variables = gprob.NumBinVars - results.problem.number_of_integer_variables = gprob.NumIntVars - results.problem.number_of_continuous_variables = gprob.NumVars - gprob.NumIntVars - gprob.NumBinVars - results.problem.number_of_objectives = 1 - results.problem.number_of_solutions = gprob.SolCount - - if self._tmp_config.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() - - return results - - 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 = self._solver_var_to_pyomo_var_map.values() - - gurobi_vars_to_load = [var_map[id(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[id(var)] > 0: - var.value = val - - 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 = self._solver_var_to_pyomo_var_map.values() - - gurobi_vars_to_load = [var_map[id(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[id(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() - quadratic_cons_to_load = self._solver_model.getQConstrs() - else: - gurobi_cons_to_load = set([con_map[id(pyomo_con)] for pyomo_con in cons_to_load]) - linear_cons_to_load = list(gurobi_cons_to_load.intersection(set(self._solver_model.getConstrs()))) - quadratic_cons_to_load = list(gurobi_cons_to_load.intersection(set(self._solver_model.getQConstrs()))) - linear_vals = self._solver_model.getAttr("Pi", linear_cons_to_load) - 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[id(gurobi_con)] - dual[pyomo_con] = val - for gurobi_con, val in zip(quadratic_cons_to_load, quadratic_vals): - pyomo_con = reverse_con_map[id(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() - 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 = list(gurobi_cons_to_load.intersection(set(self._solver_model.getConstrs()))) - quadratic_cons_to_load = list(gurobi_cons_to_load.intersection(set(self._solver_model.getQConstrs()))) - linear_vals = self._solver_model.getAttr("Slack", linear_cons_to_load) - 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[id(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 - for gurobi_con, val in zip(quadratic_cons_to_load, quadratic_vals): - pyomo_con = reverse_con_map[id(gurobi_con)] - slack[pyomo_con] = val - - def _update(self): - last_solve_vars = ComponentSet(self._solver_var_to_pyomo_var_map.values()) - current_vars = ComponentSet(v for v in self._pyomo_model.component_data_objects(Var, descend_into=True, sort=True)) - last_solve_cons = ComponentSet(self._solver_con_to_pyomo_con_map.values()) - current_cons = ComponentSet(c for c in self._pyomo_model.component_data_objects(Constraint, active=True, descend_into=True, sort=True)) - new_cons = current_cons - last_solve_cons - old_cons = last_solve_cons - current_cons - for c in old_cons: - self._remove_constraint(c) - new_vars = current_vars - last_solve_vars - old_vars = last_solve_vars - current_vars - for v in old_vars: - self._remove_var(v) - for v in new_vars: - self._add_var(v) - for c in new_cons: - self._add_constraint(c) - for helper in self._mutable_helpers: - helper.update() - for helper in self._mutable_quadratic_helpers: - gurobi_con = helper.con - new_gurobi_expr = helper.get_updated_expression() - new_rhs = helper.get_updated_rhs() - new_sense = gurobi_con.sense - pyomo_con = self._solver_con_to_pyomo_con_map[id(gurobi_con)] - name = self._symbol_map.getSymbol(pyomo_con, self._labeler) - self._solver_model.remove(gurobi_con) - new_con = self._solver_model.addQConstr(new_gurobi_expr, new_sense, new_rhs, name=name) - self._pyomo_con_to_solver_con_map[id(pyomo_con)] = new_con - del self._solver_con_to_pyomo_con_map[id(gurobi_con)] - self._solver_con_to_pyomo_con_map[id(new_con)] = pyomo_con - helper.con = new_con - pyomo_obj = _get_objective(self._pyomo_model) - if pyomo_obj is self._objective and pyomo_obj.expr is self._objective_expr: - helper = self._mutable_objective - new_gurobi_expr = helper.get_updated_expression() - if pyomo_obj.sense == minimize: - sense = self._gurobipy.GRB.MINIMIZE - else: - sense = self._gurobipy.GRB.MAXIMIZE - self._solver_model.setObjective(new_gurobi_expr, sense=sense) - else: - self._set_objective(pyomo_obj) - - -GurobiAuto.solve.__doc__ = add_docstring_list(GurobiAuto.solve.__doc__, GurobiAuto.CONFIG) diff --git a/pyomo/solver/gurobi_direct.py b/pyomo/solver/gurobi_direct.py deleted file mode 100644 index 6f9f1f47d5d..00000000000 --- a/pyomo/solver/gurobi_direct.py +++ /dev/null @@ -1,664 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright 2017 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 -import pyomo.common -from pyutilib.misc import Bunch -from pyutilib.services import TempfileManager -from pyomo.core.expr.numvalue import is_fixed -from pyomo.core.expr.numvalue import value -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.core.kernel.component_set import ComponentSet -from pyomo.core.kernel.component_map import ComponentMap -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 -from pyomo.core.base.var import Var -from pyomo.core.base.param import Param -from pyomo.core.base.constraint import Constraint -from pyomo.core.base.sos import SOSConstraint -from pyomo.core.base.objective import Objective -from pyomo.common.config import ConfigBlock, ConfigValue, add_docstring_list -from pyomo.solver.base import MIPSolver, Solver -from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler -from pyomo.core.expr.visitor import StreamBasedExpressionVisitor -from pyomo.core.expr.numvalue import native_numeric_types, native_types -import pyomo.core.expr.numeric_expr as numeric_expr - - -logger = logging.getLogger('pyomo.solvers') - - -class DegreeError(ValueError): - pass - - -def _is_numeric(x): - try: - float(x) - except ValueError: - return False - return True - - -def _get_objective(block): - obj = None - for o in block.component_data_objects(Objective, descend_into=True, active=True, sort=True): - if obj is not None: - raise ValueError('Multiple active objectives found') - obj = o - return obj - - -class _MutableHelper(object): - def __init__(self): - self.mutable_constant = 0 - self.mutable_linear_coefficients = list() - self.mutable_quadratic_coefficients = list() - - -class _GurobiWalker(StreamBasedExpressionVisitor): - def __init__(self, var_map): - """ - Parameters - ---------- - var_map: dict - maps ids of pyomo vars to gurobi vars - """ - import gurobipy - self._gurobipy = gurobipy - super(_GurobiWalker, self).__init__() - self.var_map = var_map - self.referenced_vars = ComponentSet() - - def initializeWalker(self, expr): - self.referenced_vars = ComponentSet() - walk, result = self.beforeChild(None, expr) - if not walk: - return False, result - return True, None - - # before child - skip leaf nodes - def beforeChild(self, node, child): - child_type = child.__class__ - if child_type in native_types: - return False, value(child) - if child_type is numeric_expr.LinearExpression: - return (False, (self._gurobipy.LinExpr(child.linear_coefs, - [self.var_map[id(i)] for i in child.linear_vars]) + - child.constant)) - if child.is_expression_type(): - return True, None - if child.is_constant(): - return False, value(child) - if child.is_variable_type(): - self.referenced_vars.add(child) - return False, self.var_map[id(child)] - if child.is_parameter_type(): - return False, value(child) - return True, None - - def exitNode(self, node, data): - if node.__class__ is numeric_expr.PowExpression: - arg1, arg2 = data - if arg2 != 2: - raise ValueError('Cannot handle exponent {0}'.format(str(arg2))) - return arg1 * arg1 - return node._apply_operation(data) - - -@SolverFactory.register('new_gurobi_direct', doc='Direct python interface to Gurobi') -class GurobiDirect(MIPSolver): - """ - Direct interface to Gurobi - """ - CONFIG = Solver.CONFIG() - MAPPED_OPTIONS = MIPSolver.MAPPED_OPTIONS() - - CONFIG.declare('symbolic_solver_labels', ConfigValue(default=False, domain=bool, - doc='If True, the gurobi variable and constraint names ' - 'will match those of the pyomo variables and constrains')) - CONFIG.declare('stream_solver', ConfigValue(default=False, domain=bool, - doc='If True, show the Gurobi output')) - CONFIG.declare('load_solutions', ConfigValue(default=True, domain=bool, - doc='If True, load the solution back into the Pyomo model')) - - __doc__ = add_docstring_list(__doc__, CONFIG) - - def __init__(self): - super(GurobiDirect, self).__init__() - - self._pyomo_model = None - self._solver_model = None - self._symbol_map = SymbolMap() - self._labeler = None - self._pyomo_var_to_solver_var_map = dict() - self._solver_var_to_pyomo_var_map = dict() - self._pyomo_con_to_solver_con_map = dict() - self._solver_con_to_pyomo_con_map = dict() - self._vars_referenced_by_con = dict() - self._vars_referenced_by_obj = dict() - self._objective = None - self._referenced_variables = dict() - self._referenced_params = dict() - self._mutable_coefficients_map = dict() - self._range_constraints = set() - self._tmp_config = None - self._tmp_mapped_options = None - self._tmp_options = None - self._walker = _GurobiWalker(self._pyomo_var_to_solver_var_map) - - try: - import gurobipy - self._gurobipy = gurobipy - self._python_api_exists = True - except Exception as e: - logger.warning("Import of gurobipy failed - gurobi message=" + str(e) + "\n") - self._python_api_exists = False - - def available(self): - return self._python_api_exists - - def license_status(self): - try: - tmp = self._gurobipy.Model() - return True - except self._gurobipy.GurobiError: - return False - - def solve(self, model, options=None, mapped_options=None, **config_options): - """ - solve a model - """ - self._tmp_options = self.options(options) - self._tmp_mapped_options = self.mapped_options(mapped_options) - self._tmp_config = self.config(config_options) - if model is self._pyomo_model: - self._update() - else: - self._set_instance(model) - self._apply_solver() - return self._postsolve() - - def _apply_solver(self): - if self._tmp_config.stream_solver: - self._solver_model.setParam('OutputFlag', 1) - else: - self._solver_model.setParam('OutputFlag', 0) - - for key, option in self._tmp_options.items(): - self._solver_model.setParam(key, option) - - self._solver_model.optimize() - - def _add_var(self, var): - varname = self._symbol_map.getSymbol(var, self._labeler) - vtype = self._gurobi_vtype_from_var(var) - if var.has_lb(): - lb = value(var.lb) - else: - lb = -self._gurobipy.GRB.INFINITY - if var.has_ub(): - ub = value(var.ub) - else: - ub = self._gurobipy.GRB.INFINITY - if var.is_fixed(): - lb = value(var.value) - ub = value(var.value) - - gurobipy_var = self._solver_model.addVar(lb=lb, ub=ub, vtype=vtype, name=varname) - - self._pyomo_var_to_solver_var_map[id(var)] = gurobipy_var - self._solver_var_to_pyomo_var_map[id(gurobipy_var)] = var - self._referenced_variables[id(var)] = 0 - - def _set_instance(self, model): - self._pyomo_model = model - self._symbol_map = SymbolMap() - self._labeler = None - self._pyomo_var_to_solver_var_map = dict() - self._solver_var_to_pyomo_var_map = dict() - self._pyomo_con_to_solver_con_map = dict() - self._solver_con_to_pyomo_con_map = dict() - self._vars_referenced_by_con = dict() - self._vars_referenced_by_obj = dict() - self._objective = None - self._referenced_variables = dict() - self._referenced_params = dict() - self._param_to_con_map = dict() - self._range_constraints = set() - self._walker = _GurobiWalker(self._pyomo_var_to_solver_var_map) - - if self._tmp_config.symbolic_solver_labels: - self._labeler = TextLabeler() - else: - self._labeler = NumericLabeler('x') - - if model.name is not None: - self._solver_model = self._gurobipy.Model(model.name) - else: - self._solver_model = self._gurobipy.Model() - - for var in model.component_data_objects(Var, descend_into=True, sort=True): - self._add_var(var) - - for param in model.component_data_objects(Param, descend_into=True, sort=True): - if not param.is_constant(): - self._add_param(param) - - for con in model.component_data_objects(Constraint, descend_into=True, active=True, sort=True): - self._add_constraint(con) - - for con in model.component_data_objects(SOSConstraint, descend_into=True, active=True, sort=True): - self._add_sos_constraint(con) - - self._set_objective(_get_objective(model)) - - def _add_constraint(self, con): - assert con.active - - conname = self._symbol_map.getSymbol(con, self._labeler) - - gurobi_expr = self._walker.walk_expression(con.body) - referenced_vars = self._walker.referenced_vars - - if con.equality: - gurobipy_con = self._solver_model.addConstr(lhs=gurobi_expr, - sense=self._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=self._gurobipy.GRB.GREATER_EQUAL, - rhs=value(con.lower), - name=conname) - elif con.has_ub(): - gurobipy_con = self._solver_model.addConstr(lhs=gurobi_expr, - sense=self._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[id(var)] += 1 - self._vars_referenced_by_con[id(con)] = referenced_vars - self._pyomo_con_to_solver_con_map[id(con)] = gurobipy_con - self._solver_con_to_pyomo_con_map[id(gurobipy_con)] = con - - def _add_sos_constraint(self, con): - assert con.active() - - conname = self._symbol_map.getSymbol(con, self._labeler) - level = con.level - if level == 1: - sos_type = self._gurobipy.GRB.SOS_TYPE1 - elif level == 2: - sos_type = self._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[id(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[id(con)].add(v) - gurobi_vars.append(self._pyomo_var_to_solver_var_map[id(v)]) - self._referenced_variables[id(v)] += 1 - weights.append(w) - - gurobipy_con = self._solver_model.addSOS(sos_type, gurobi_vars, weights) - self._pyomo_con_to_solver_con_map[id(con)] = gurobipy_con - self._solver_con_to_pyomo_con_map[id(gurobipy_con)] = con - - 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 = self._gurobipy.GRB.BINARY - elif var.is_integer(): - vtype = self._gurobipy.GRB.INTEGER - elif var.is_continuous(): - vtype = self._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[id(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 = self._gurobipy.GRB.MINIMIZE - elif obj.sense == maximize: - sense = self._gurobipy.GRB.MAXIMIZE - else: - raise ValueError('Objective sense is not recognized: {0}'.format(obj.sense)) - - gurobi_expr = self._walker.walk_expression(obj.expr) - referenced_vars = self._walker.referenced_vars - - for var in referenced_vars: - self._referenced_variables[id(var)] += 1 - - self._solver_model.setObjective(gurobi_expr, sense=sense) - self._objective = obj - self._vars_referenced_by_obj = referenced_vars - - 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. - suffixes = list(self._pyomo_model.component_objects(Suffix, active=True, descend_into=False, sort=True)) - extract_duals = False - extract_slacks = False - extract_reduced_costs = False - for suffix in 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 = self._gurobipy.GRB - status = gprob.Status - - if gprob.getAttr(self._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 - - results = SolverResults() - results.solver.wallclock_time = gprob.Runtime - - if status == grb.LOADED: # problem is loaded, but no solution - results.solver.status = SolverStatus.aborted - results.solver.termination_message = "Model is loaded, but no solution information is available." - results.solver.termination_condition = TerminationCondition.error - elif status == grb.OPTIMAL: # optimal - results.solver.status = SolverStatus.ok - results.solver.termination_message = "Model was solved to optimality (subject to tolerances), " \ - "and an optimal solution is available." - results.solver.termination_condition = TerminationCondition.optimal - elif status == grb.INFEASIBLE: - results.solver.status = SolverStatus.warning - results.solver.termination_message = "Model was proven to be infeasible" - results.solver.termination_condition = TerminationCondition.infeasible - elif status == grb.INF_OR_UNBD: - results.solver.status = SolverStatus.warning - results.solver.termination_message = "Problem proven to be infeasible or unbounded." - results.solver.termination_condition = TerminationCondition.infeasibleOrUnbounded - elif status == grb.UNBOUNDED: - results.solver.status = SolverStatus.warning - results.solver.termination_message = "Model was proven to be unbounded." - results.solver.termination_condition = TerminationCondition.unbounded - elif status == grb.CUTOFF: - results.solver.status = SolverStatus.aborted - 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." - results.solver.termination_condition = TerminationCondition.minFunctionValue - elif status == grb.ITERATION_LIMIT: - results.solver.status = SolverStatus.aborted - results.solver.termination_message = "Optimization terminated because the total number of simplex " \ - "iterations performed exceeded the value specified in the " \ - "IterationLimit parameter." - results.solver.termination_condition = TerminationCondition.maxIterations - elif status == grb.NODE_LIMIT: - results.solver.status = SolverStatus.aborted - results.solver.termination_message = "Optimization terminated because the total number of " \ - "branch-and-cut nodes explored exceeded the value specified " \ - "in the NodeLimit parameter" - results.solver.termination_condition = TerminationCondition.maxEvaluations - elif status == grb.TIME_LIMIT: - results.solver.status = SolverStatus.aborted - results.solver.termination_message = "Optimization terminated because the time expended exceeded " \ - "the value specified in the TimeLimit parameter." - results.solver.termination_condition = TerminationCondition.maxTimeLimit - elif status == grb.SOLUTION_LIMIT: - results.solver.status = SolverStatus.aborted - results.solver.termination_message = "Optimization terminated because the number of solutions found " \ - "reached the value specified in the SolutionLimit parameter." - results.solver.termination_condition = TerminationCondition.unknown - elif status == grb.INTERRUPTED: - results.solver.status = SolverStatus.aborted - results.solver.termination_message = "Optimization was terminated by the user." - results.solver.termination_condition = TerminationCondition.error - elif status == grb.NUMERIC: - results.solver.status = SolverStatus.error - results.solver.termination_message = "Optimization was terminated due to unrecoverable numerical " \ - "difficulties." - results.solver.termination_condition = TerminationCondition.error - elif status == grb.SUBOPTIMAL: - results.solver.status = SolverStatus.warning - results.solver.termination_message = "Unable to satisfy optimality tolerances; a sub-optimal " \ - "solution is available." - results.solver.termination_condition = TerminationCondition.other - # 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)): - results.solver.status = SolverStatus.aborted - 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." - results.solver.termination_condition = TerminationCondition.other - else: - results.solver.status = SolverStatus.error - results.solver.termination_message = \ - ("Unhandled Gurobi solve status " - "("+str(status)+")") - results.solver.termination_condition = TerminationCondition.error - - results.problem.name = gprob.ModelName - - if gprob.ModelSense == 1: - results.problem.sense = minimize - elif gprob.ModelSense == -1: - results.problem.sense = maximize - else: - raise RuntimeError('Unrecognized gurobi objective sense: {0}'.format(gprob.ModelSense)) - - results.problem.upper_bound = None - results.problem.lower_bound = None - if (gprob.NumBinVars + gprob.NumIntVars) == 0: - try: - results.problem.upper_bound = gprob.ObjVal - results.problem.lower_bound = gprob.ObjVal - except (self._gurobipy.GurobiError, AttributeError): - pass - elif gprob.ModelSense == 1: # minimizing - try: - results.problem.upper_bound = gprob.ObjVal - except (self._gurobipy.GurobiError, AttributeError): - pass - try: - results.problem.lower_bound = gprob.ObjBound - except (self._gurobipy.GurobiError, AttributeError): - pass - elif gprob.ModelSense == -1: # maximizing - try: - results.problem.upper_bound = gprob.ObjBound - except (self._gurobipy.GurobiError, AttributeError): - pass - try: - results.problem.lower_bound = gprob.ObjVal - except (self._gurobipy.GurobiError, AttributeError): - pass - else: - raise RuntimeError('Unrecognized gurobi objective sense: {0}'.format(gprob.ModelSense)) - - results.problem.number_of_constraints = gprob.NumConstrs + gprob.NumQConstrs + gprob.NumSOS - results.problem.number_of_nonzeros = gprob.NumNZs - results.problem.number_of_variables = gprob.NumVars - results.problem.number_of_binary_variables = gprob.NumBinVars - results.problem.number_of_integer_variables = gprob.NumIntVars - results.problem.number_of_continuous_variables = gprob.NumVars - gprob.NumIntVars - gprob.NumBinVars - results.problem.number_of_objectives = 1 - results.problem.number_of_solutions = gprob.SolCount - - if self._tmp_config.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() - - return results - - 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 = self._solver_var_to_pyomo_var_map.values() - - gurobi_vars_to_load = [var_map[id(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[id(var)] > 0: - var.value = val - - 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 = self._solver_var_to_pyomo_var_map.values() - - gurobi_vars_to_load = [var_map[id(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[id(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() - quadratic_cons_to_load = self._solver_model.getQConstrs() - else: - gurobi_cons_to_load = set([con_map[id(pyomo_con)] for pyomo_con in cons_to_load]) - linear_cons_to_load = list(gurobi_cons_to_load.intersection(set(self._solver_model.getConstrs()))) - quadratic_cons_to_load = list(gurobi_cons_to_load.intersection(set(self._solver_model.getQConstrs()))) - linear_vals = self._solver_model.getAttr("Pi", linear_cons_to_load) - 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[id(gurobi_con)] - dual[pyomo_con] = val - for gurobi_con, val in zip(quadratic_cons_to_load, quadratic_vals): - pyomo_con = reverse_con_map[id(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() - 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 = list(gurobi_cons_to_load.intersection(set(self._solver_model.getConstrs()))) - quadratic_cons_to_load = list(gurobi_cons_to_load.intersection(set(self._solver_model.getQConstrs()))) - linear_vals = self._solver_model.getAttr("Slack", linear_cons_to_load) - 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[id(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 - for gurobi_con, val in zip(quadratic_cons_to_load, quadratic_vals): - pyomo_con = reverse_con_map[id(gurobi_con)] - slack[pyomo_con] = val - - def _update(self): - pass - - -GurobiDirect.solve.__doc__ = add_docstring_list(GurobiDirect.solve.__doc__, GurobiDirect.CONFIG) diff --git a/pyomo/solver/gurobi_persistent.py b/pyomo/solver/gurobi_persistent.py new file mode 100644 index 00000000000..3fc03da23e4 --- /dev/null +++ b/pyomo/solver/gurobi_persistent.py @@ -0,0 +1,1503 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright 2017 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 +from pyomo.core.expr.numvalue import value, is_constant, native_types, is_fixed +from pyomo.repn import generate_standard_repn +from pyomo.core.kernel.objective import minimize, maximize +from pyomo.core.kernel.component_set import ComponentSet +from pyomo.opt.base import SolverFactory +from pyomo.core.base.suffix import Suffix +from pyomo.core.base.var import Var +from pyomo.core.base.constraint import Constraint +from pyomo.core.base.sos import SOSConstraint +from pyomo.core.base.objective import Objective +from pyomo.common.config import ConfigValue, add_docstring_list, NonNegativeFloat +from pyomo.solver.base import MIPSolver, ResultsBase, SolutionLoaderBase, TerminationCondition +from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler +from pyomo.core.expr.visitor import StreamBasedExpressionVisitor, identify_components +import pyomo.core.expr.numeric_expr as numeric_expr +from pyomo.core.base.expression import SimpleExpression, _GeneralExpressionData +import collections + + +logger = logging.getLogger('pyomo.solvers') + + +class DegreeError(ValueError): + pass + + +class GurobiPersistentSolutionLoader(SolutionLoaderBase): + def __init__(self, model, solver): + self._solver = solver + self._model = model + self._valid = True + + def _assert_solution_still_valid(self): + if not self._valid: + raise RuntimeError('The results in the solver are no longer valid.') + + def load_solution(self): + self._assert_solution_still_valid() + + self._solver.load_vars() + + if hasattr(self._model, 'dual'): + self._solver.load_duals() + + if hasattr(self._model, 'slack'): + self._solver.load_slacks() + + if hasattr(self._model, 'rc'): + self._solver.load_rc() + + def load_vars(self, vars_to_load=None, solution_number=0): + self._assert_solution_still_valid() + self._solver.load_vars(vars_to_load=vars_to_load, solution_number=solution_number) + + def load_reduced_costs(self, vars_to_load=None): + self._assert_solution_still_valid() + self._solver.load_rc(vars_to_load=vars_to_load) + + def load_duals(self, cons_to_load=None): + self._assert_solution_still_valid() + self._solver.load_duals(cons_to_load=cons_to_load) + + def load_slacks(self, cons_to_load=None): + self._assert_solution_still_valid() + self._solver.load_slacks(cons_to_load=cons_to_load) + + +class GurobiPersistentResults(ResultsBase): + def __init__(self, model, solver): + super(GurobiPersistentResults, self).__init__() + self.solution_loader = GurobiPersistentSolutionLoader(model=model, solver=solver) + if solver.get_model_attr('SolCount') > 0: + self._found_feasible_solution = True + else: + self._found_feasible_solution = False + self.solver.declare('wallclock_time', + ConfigValue(default=None, + domain=NonNegativeFloat, + doc="The wallclock time reported by Gurobi")) + + def found_feasible_solution(self): + return self._found_feasible_solution + + +def _get_objective(block): + obj = None + for o in block.component_data_objects(Objective, descend_into=True, active=True, sort=True): + if obj is not None: + raise ValueError('Multiple active objectives found') + obj = o + return obj + + +class _MutableLinearCoefficient(object): + def __init__(self): + self.expr = None + self.var = None + self.con = None + self.gurobi_model = None + + def update(self): + self.gurobi_model.chgCoeff(self.con, self.var, value(self.expr)) + + def __str__(self): + s = str(self.var) + ': ' + str(self.expr) + return s + + +class _MutableRangeConstant(object): + def __init__(self): + self.lhs_expr = None + self.rhs_expr = None + self.con = None + self.slack = None + + def update(self): + rhs_val = value(self.rhs_expr) + lhs_val = value(self.lhs_expr) + self.con.rhs = rhs_val + self.slack.ub = rhs_val - lhs_val + + +class _MutableConstant(object): + def __init__(self): + self.expr = None + self.con = None + + def update(self): + self.con.rhs = value(self.expr) + + +class _MutableQuadraticConstraint(object): + def __init__(self, gurobi_model, gurobi_con, constant, linear_coefs, quadratic_coefs): + self.con = gurobi_con + self.gurobi_model = gurobi_model + self.constant = constant + self.last_constant_value = value(self.constant) + self.linear_coefs = linear_coefs + self.last_linear_coef_values = [value(i.expr) for i in self.linear_coefs] + self.quadratic_coefs = quadratic_coefs + self.last_quadratic_coef_values = [value(i.expr) for i in self.quadratic_coefs] + + def get_updated_expression(self): + gurobi_expr = self.gurobi_model.getQCRow(self.con) + for ndx, coef in enumerate(self.linear_coefs): + new_coef_value = value(coef.expr) - self.last_linear_coef_values[ndx] + gurobi_expr += new_coef_value * coef.var + self.last_linear_coef_values[ndx] = new_coef_value + for ndx, coef in enumerate(self.quadratic_coefs): + new_coef_value = value(coef.expr) - self.last_quadratic_coef_values[ndx] + gurobi_expr += new_coef_value * coef.var1 * coef.var2 + self.last_quadratic_coef_values[ndx] = new_coef_value + return gurobi_expr + + def get_updated_rhs(self): + return value(self.constant.expr) + + +class _MutableObjective(object): + def __init__(self, gurobi_model, constant, linear_coefs, quadratic_coefs): + self.gurobi_model = gurobi_model + self.constant = constant + self.linear_coefs = linear_coefs + self.quadratic_coefs = quadratic_coefs + self.last_quadratic_coef_values = [value(i.expr) for i in self.quadratic_coefs] + + def get_updated_expression(self): + for ndx, coef in enumerate(self.linear_coefs): + coef.var.obj = value(coef.expr) + self.gurobi_model.ObjCon = value(self.constant.expr) + + gurobi_expr = None + for ndx, coef in enumerate(self.quadratic_coefs): + if value(coef.expr) != self.last_quadratic_coef_values[ndx]: + if gurobi_expr is None: + self.gurobi_model.update() + gurobi_expr = self.gurobi_model.getObjective() + new_coef_value = value(coef.expr) - self.last_quadratic_coef_values[ndx] + gurobi_expr += new_coef_value * coef.var1 * coef.var2 + self.last_quadratic_coef_values[ndx] = new_coef_value + return gurobi_expr + + +class _MutableQuadraticCoefficient(object): + def __init__(self): + self.expr = None + self.var1 = None + self.var2 = None + + +class _GurobiWalker(StreamBasedExpressionVisitor): + def __init__(self, var_map): + """ + Parameters + ---------- + var_map: dict + maps ids of pyomo vars to gurobi vars + """ + import gurobipy + self._gurobipy = gurobipy + super(_GurobiWalker, self).__init__() + self.var_map = var_map + self.referenced_vars = ComponentSet() + + def initializeWalker(self, expr): + self.referenced_vars = ComponentSet() + walk, result = self.beforeChild(None, expr) + if not walk: + return False, result + return True, None + + # before child - skip leaf nodes + def beforeChild(self, node, child): + child_type = child.__class__ + if child_type in native_types: + return False, value(child) + if child_type is numeric_expr.LinearExpression: + return (False, (self._gurobipy.LinExpr(child.linear_coefs, + [self.var_map[id(i)] for i in child.linear_vars]) + + child.constant)) + if child.is_expression_type(): + return True, None + if child.is_constant(): + return False, value(child) + if child.is_variable_type(): + self.referenced_vars.add(child) + return False, self.var_map[id(child)] + if child.is_parameter_type(): + return False, value(child) + return True, None + + def exitNode(self, node, data): + if node.__class__ is numeric_expr.PowExpression: + arg1, arg2 = data + if arg2 != 2: + raise ValueError('Cannot handle exponent {0}'.format(str(arg2))) + return arg1 * arg1 + return node._apply_operation(data) + + +@SolverFactory.register('gurobi_persistent_new', doc='Direct python interface to Gurobi with automatic updates') +class GurobiPersistentNew(MIPSolver): + """ + Direct interface to Gurobi + """ + CONFIG = MIPSolver.CONFIG() + + CONFIG.declare('symbolic_solver_labels', ConfigValue(default=False, domain=bool, + doc='If True, the gurobi variable and constraint names ' + 'will match those of the pyomo variables and constrains')) + CONFIG.declare('stream_solver', ConfigValue(default=False, domain=bool, + doc='If True, show the Gurobi output')) + CONFIG.declare('load_solutions', ConfigValue(default=True, domain=bool, + doc='If True, load the solution back into the Pyomo model after ' + 'solving')) + CONFIG.declare('check_for_updated_mutable_params_in_constraints', + ConfigValue(default=True, domain=bool, + doc='If True, the solver interface will look for constraint coefficients that depend on ' + 'mutable parameters, and automatically update the coefficients for each solve.')) + CONFIG.declare('check_for_updated_mutable_params_in_objective', + ConfigValue(default=True, domain=bool, + doc='If True, the solver interface will look for objective coefficients that depend on ' + 'mutable parameters, and automatically update the coefficients for each solve.')) + CONFIG.declare('check_for_new_or_removed_constraints', + ConfigValue(default=True, domain=bool, + doc='If True, the solver interface will check for new or removed constraints when ' + 'solve is called.')) + CONFIG.declare('update_constraints', + ConfigValue(default=False, domain=bool, + doc='If True, the solver interface will update constraint bounds each ' + 'time solve is called.')) + CONFIG.declare('check_for_new_or_removed_vars', + ConfigValue(default=True, domain=bool, + doc='If True, the solver interface will check for new or removed vars each time solve ' + 'is called.')) + CONFIG.declare('update_vars', + ConfigValue(default=True, domain=bool, + doc='If True, the solver interface will update variable bounds each time solve ' + 'is called')) + CONFIG.declare('update_named_expressions', + ConfigValue(default=True, domain=bool, + doc='If True, the solver interface will update Expressions each time solve is called.')) + + __doc__ = add_docstring_list(__doc__, CONFIG) + + def __init__(self): + super(GurobiPersistentNew, self).__init__() + + self._pyomo_model = None + self._solver_model = None + self._symbol_map = SymbolMap() + self._labeler = None + self._pyomo_var_to_solver_var_map = dict() + self._solver_var_to_pyomo_var_map = dict() + self._pyomo_con_to_solver_con_map = dict() + self._solver_con_to_pyomo_con_map = dict() + self._pyomo_sos_to_solver_sos_map = dict() + self._solver_sos_to_pyomo_sos_map = dict() + self._vars_referenced_by_con = dict() + self._vars_referenced_by_obj = dict() + self._objective = None + self._objective_expr = None + self._referenced_variables = dict() + self._referenced_params = dict() + self._range_constraints = set() + self._tmp_config = None + self._tmp_options = None + self._mutable_helpers = dict() + self._mutable_quadratic_helpers = dict() + self._mutable_objective = None + self._last_results_object = None + self._walker = _GurobiWalker(self._pyomo_var_to_solver_var_map) + self._constraint_bodies = dict() + self._constraint_lowers = dict() + self._constraint_uppers = dict() + self._named_expressions = dict() + self._obj_named_expressions = list() + self._needs_updated = True + self._callback = None + self._callback_func = None + self._constraints_added_since_update = set() + self._vars_added_since_update = ComponentSet() + + try: + import gurobipy + self._gurobipy = gurobipy + self._python_api_exists = True + except Exception as e: + logger.warning("Import of gurobipy failed - gurobi message=" + str(e) + "\n") + self._python_api_exists = False + + if self._gurobipy.GRB.VERSION_MAJOR < 7: + # The reason for this is that it is too difficult to manage the gurobi lazy updates both for + # versions >= 7 and < 7. + logger.warning('The persistent interface to Gurobi requires at least Gurobi version 7. ') + self._python_api_exists = False + + def available(self): + return self._python_api_exists + + def license_status(self): + try: + tmp = self._gurobipy.Model() + return True + except self._gurobipy.GurobiError: + return False + + def solve(self, model, options=None, **config_options): + """ + solve a model + """ + if not self.available(): + raise RuntimeError('The persistent interface to Gurobi is not available either because gurobipy could ' + 'not be imported or because the version of Gurobi being used is less than 7.') + if self._last_results_object is not None: + self._last_results_object.solution_loader._valid = False + if options is None: + options = dict() + self._tmp_options = self.options(options, preserve_implicit=True) + self._tmp_config = self.config(config_options) + if model is self._pyomo_model: + self.update() + else: + self.set_instance(model) + self._apply_solver() + return self._postsolve() + + def _apply_solver(self): + if self._tmp_config.stream_solver: + self._solver_model.setParam('OutputFlag', 1) + else: + self._solver_model.setParam('OutputFlag', 0) + + for key, option in self._tmp_options.items(): + self._solver_model.setParam(key, option) + self._solver_model.optimize(self._callback) + self._needs_updated = False + + def add_var(self, var): + varname = self._symbol_map.getSymbol(var, self._labeler) + vtype = self._gurobi_vtype_from_var(var) + if var.has_lb(): + lb = value(var.lb) + else: + lb = -self._gurobipy.GRB.INFINITY + if var.has_ub(): + ub = value(var.ub) + else: + ub = self._gurobipy.GRB.INFINITY + if var.is_fixed(): + lb = value(var.value) + ub = value(var.value) + + gurobipy_var = self._solver_model.addVar(lb=lb, ub=ub, vtype=vtype, name=varname) + + self._pyomo_var_to_solver_var_map[id(var)] = gurobipy_var + self._solver_var_to_pyomo_var_map[id(gurobipy_var)] = var + self._referenced_variables[id(var)] = 0 + self._vars_added_since_update.add(var) + + self._needs_updated = True + + def set_instance(self, model): + self._pyomo_model = model + self._symbol_map = SymbolMap() + self._labeler = None + self._pyomo_var_to_solver_var_map = dict() + self._solver_var_to_pyomo_var_map = dict() + self._pyomo_con_to_solver_con_map = dict() + self._solver_con_to_pyomo_con_map = dict() + self._pyomo_sos_to_solver_sos_map = dict() + self._solver_sos_to_pyomo_sos_map = dict() + self._vars_referenced_by_con = dict() + self._vars_referenced_by_obj = dict() + self._objective = None + self._referenced_variables = dict() + self._range_constraints = set() + self._mutable_helpers = dict() + self._mutable_quadratic_helpers = dict() + self._last_results_object = None + self._walker = _GurobiWalker(self._pyomo_var_to_solver_var_map) + self._constraint_bodies = dict() + self._constraint_lowers = dict() + self._constraint_uppers = dict() + self._named_expressions = dict() + self._obj_named_expressions = list() + self._constraints_added_since_update = set() + self._vars_added_since_update = ComponentSet() + + if self._tmp_config.symbolic_solver_labels: + self._labeler = TextLabeler() + else: + self._labeler = NumericLabeler('x') + + if model.name is not None: + self._solver_model = self._gurobipy.Model(model.name) + else: + self._solver_model = self._gurobipy.Model() + + self.add_block(model) + + def add_block(self, block): + for var in block.component_data_objects(Var, descend_into=True, sort=True): + self.add_var(var) + + for con in block.component_data_objects(Constraint, descend_into=True, active=True, sort=True): + self.add_constraint(con) + + for con in block.component_data_objects(SOSConstraint, descend_into=True, active=True, sort=True): + self.add_sos_constraint(con) + + self.set_objective(_get_objective(block)) + + def remove_block(self, block): + for con in block.component_data_objects(ctype=Constraint, descend_into=True, active=True, sort=True): + self.remove_constraint(con) + + for con in block.component_data_objects(ctype=SOSConstraint, descend_into=True, active=True, sort=True): + self.remove_sos_constraint(con) + + for var in block.component_data_objects(ctype=Var, descend_into=True, sort=True): + self.remove_var(var) + + def _get_expr_from_pyomo_expr(self, expr): + mutable_linear_coefficients = list() + mutable_quadratic_coefficients = list() + repn = generate_standard_repn(expr, quadratic=True, compute_values=False) + referenced_vars = ComponentSet() + + degree = repn.polynomial_degree() + if (degree is None) or (degree > 2): + raise DegreeError('GurobiAuto does not support expressions of degree {0}.'.format(degree)) + + if len(repn.linear_vars) > 0: + referenced_vars.update(repn.linear_vars) + linear_coef_vals = list() + for ndx, coef in enumerate(repn.linear_coefs): + if not is_constant(coef): + mutable_linear_coefficient = _MutableLinearCoefficient() + mutable_linear_coefficient.expr = coef + mutable_linear_coefficient.var = self._pyomo_var_to_solver_var_map[id(repn.linear_vars[ndx])] + mutable_linear_coefficients.append(mutable_linear_coefficient) + linear_coef_vals.append(value(coef)) + new_expr = self._gurobipy.LinExpr(linear_coef_vals, [self._pyomo_var_to_solver_var_map[id(i)] for i in repn.linear_vars]) + else: + new_expr = 0.0 + + for ndx, v in enumerate(repn.quadratic_vars): + x, y = v + gurobi_x = self._pyomo_var_to_solver_var_map[id(x)] + gurobi_y = self._pyomo_var_to_solver_var_map[id(y)] + coef = repn.quadratic_coefs[ndx] + if not is_constant(coef): + mutable_quadratic_coefficient = _MutableQuadraticCoefficient() + mutable_quadratic_coefficient.expr = coef + mutable_quadratic_coefficient.var1 = gurobi_x + mutable_quadratic_coefficient.var2 = gurobi_y + mutable_quadratic_coefficients.append(mutable_quadratic_coefficient) + coef_val = value(coef) + new_expr += coef_val * gurobi_x * gurobi_y + referenced_vars.add(x) + referenced_vars.add(y) + + return new_expr, referenced_vars, repn.constant, mutable_linear_coefficients, mutable_quadratic_coefficients + + def add_constraint(self, con): + assert con.active + + conname = self._symbol_map.getSymbol(con, self._labeler) + + if self.config.check_for_updated_mutable_params_in_constraints: + (gurobi_expr, + referenced_vars, + repn_constant, + mutable_linear_coefficients, + mutable_quadratic_coefficients) = self._get_expr_from_pyomo_expr(con.body) + else: + gurobi_expr = self._walker.walk_expression(con.body) + referenced_vars = self._walker.referenced_vars + repn_constant = 0 + mutable_linear_coefficients = list() + mutable_quadratic_coefficients = list() + + if gurobi_expr.__class__ is self._gurobipy.LinExpr: + if con.equality: + rhs_expr = con.lower - repn_constant + rhs_val = value(rhs_expr) + gurobipy_con = self._solver_model.addLConstr(gurobi_expr, + self._gurobipy.GRB.EQUAL, + rhs_val, + name=conname) + if not is_constant(rhs_expr): + mutable_constant = _MutableConstant() + mutable_constant.expr = rhs_expr + mutable_constant.con = gurobipy_con + self._mutable_helpers[con] = [mutable_constant] + else: + self._mutable_helpers[con] = list() + elif con.has_lb() and con.has_ub(): + lhs_expr = con.lower - repn_constant + rhs_expr = con.upper - repn_constant + lhs_val = value(lhs_expr) + rhs_val = value(rhs_expr) + gurobipy_con = self._solver_model.addRange(gurobi_expr, lhs_val, rhs_val, name=conname) + self._range_constraints.add(con) + if not is_constant(lhs_expr) or not is_constant(rhs_expr): + mutable_range_constant = _MutableRangeConstant() + mutable_range_constant.lhs_expr = lhs_expr + mutable_range_constant.rhs_expr = rhs_expr + mutable_range_constant.con = gurobipy_con + mutable_range_constant.slack = self._solver_model.getVarByName('Rg'+conname) + self._mutable_helpers[con] = [mutable_range_constant] + else: + self._mutable_helpers[con] = list() + elif con.has_lb(): + rhs_expr = con.lower - repn_constant + rhs_val = value(rhs_expr) + gurobipy_con = self._solver_model.addLConstr(gurobi_expr, self._gurobipy.GRB.GREATER_EQUAL, rhs_val, name=conname) + if not is_constant(rhs_expr): + mutable_constant = _MutableConstant() + mutable_constant.expr = rhs_expr + mutable_constant.con = gurobipy_con + self._mutable_helpers[con] = [mutable_constant] + else: + self._mutable_helpers[con] = list() + elif con.has_ub(): + rhs_expr = con.upper - repn_constant + rhs_val = value(rhs_expr) + gurobipy_con = self._solver_model.addLConstr(gurobi_expr, self._gurobipy.GRB.LESS_EQUAL, rhs_val, name=conname) + if not is_constant(rhs_expr): + mutable_constant = _MutableConstant() + mutable_constant.expr = rhs_expr + mutable_constant.con = gurobipy_con + self._mutable_helpers[con] = [mutable_constant] + else: + self._mutable_helpers[con] = list() + else: + raise ValueError("Constraint does not have a lower " + "or an upper bound: {0} \n".format(con)) + for tmp in mutable_linear_coefficients: + tmp.con = gurobipy_con + tmp.gurobi_model = self._solver_model + self._mutable_helpers[con].extend(mutable_linear_coefficients) + elif gurobi_expr.__class__ is self._gurobipy.QuadExpr: + if con.equality: + raise NotImplementedError('Quadratic equality constraints are not supported') + elif con.has_lb() and con.has_ub(): + raise NotImplementedError('Quadratic range constraints are not supported') + elif con.has_lb(): + rhs_expr = con.lower - repn_constant + rhs_val = value(rhs_expr) + gurobipy_con = self._solver_model.addQConstr(gurobi_expr, self._gurobipy.GRB.GREATER_EQUAL, rhs_val, name=conname) + elif con.has_ub(): + rhs_expr = con.upper - repn_constant + rhs_val = value(rhs_expr) + gurobipy_con = self._solver_model.addQConstr(gurobi_expr, self._gurobipy.GRB.LESS_EQUAL, rhs_val, name=conname) + else: + raise ValueError("Constraint does not have a lower " + "or an upper bound: {0} \n".format(con)) + if len(mutable_linear_coefficients) > 0 or len(mutable_quadratic_coefficients) > 0 or not is_constant(repn_constant): + mutable_constant = _MutableConstant() + mutable_constant.expr = repn_constant + mutable_quadratic_constraint = _MutableQuadraticConstraint(self._solver_model, gurobipy_con, + mutable_constant, + mutable_linear_coefficients, + mutable_quadratic_coefficients) + self._mutable_quadratic_helpers[con] = mutable_quadratic_constraint + + if self.config.update_named_expressions: + self._named_expressions[con] = list() + for e in identify_components(con.body, {SimpleExpression, _GeneralExpressionData}): + self._named_expressions[con].append((e, e.expr)) + for var in referenced_vars: + self._referenced_variables[id(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[id(gurobipy_con)] = con + self._constraint_bodies[con] = con.body + self._constraint_lowers[con] = value(con.lower) + self._constraint_uppers[con] = value(con.upper) + self._constraints_added_since_update.add(con) + + self._needs_updated = True + + def add_sos_constraint(self, con): + assert con.active() + + conname = self._symbol_map.getSymbol(con, self._labeler) + level = con.level + if level == 1: + sos_type = self._gurobipy.GRB.SOS_TYPE1 + elif level == 2: + sos_type = self._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[id(v)]) + self._referenced_variables[id(v)] += 1 + weights.append(w) + + gurobipy_con = self._solver_model.addSOS(sos_type, gurobi_vars, weights) + self._pyomo_sos_to_solver_sos_map[con] = gurobipy_con + self._solver_sos_to_pyomo_sos_map[id(gurobipy_con)] = con + self._constraints_added_since_update.add(con) + + self._needs_updated = True + + def remove_constraint(self, con): + if con in self._constraints_added_since_update: + self._update_gurobi_model() + con_id = id(con) + solver_con = self._pyomo_con_to_solver_con_map[con] + self._solver_model.remove(solver_con) + self._symbol_map.removeSymbol(con) + self._labeler.remove_obj(con) + for var in self._vars_referenced_by_con[con]: + self._referenced_variables[id(var)] -= 1 + del self._vars_referenced_by_con[con] + del self._pyomo_con_to_solver_con_map[con] + del self._solver_con_to_pyomo_con_map[id(solver_con)] + self._range_constraints.discard(con) + del self._constraint_bodies[con] + del self._constraint_lowers[con] + del self._constraint_uppers[con] + self._mutable_helpers.pop(con, None) + self._mutable_quadratic_helpers.pop(con, None) + self._named_expressions.pop(con, None) + self._needs_updated = True + + def remove_sos_constraint(self, con): + if con in self._constraints_added_since_update: + self._update_gurobi_model() + solver_sos_con = self._pyomo_sos_to_solver_sos_map[con] + self._solver_model.remove(solver_sos_con) + self._symbol_map.removeSymbol(con) + self._labeler.remove_obj(con) + for var in self._vars_referenced_by_con[con]: + self._referenced_variables[id(var)] -= 1 + del self._vars_referenced_by_con[con] + del self._pyomo_sos_to_solver_sos_map[con] + del self._solver_sos_to_pyomo_sos_map[id(solver_sos_con)] + + def remove_var(self, var): + if self._referenced_variables[id(var)] != 0: + raise ValueError('Cannot remove Var {0} because it is still referenced by the objective or one or more constraints'.format(var)) + if var in self._vars_added_since_update: + self._update_gurobi_model() + solver_var = self._pyomo_var_to_solver_var_map[id(var)] + self._solver_model.remove(solver_var) + self._symbol_map.removeSymbol(var) + self._labeler.remove_obj(var) + del self._referenced_variables[id(var)] + del self._pyomo_var_to_solver_var_map[id(var)] + del self._solver_var_to_pyomo_var_map[id(solver_var)] + + def update_var(self, var): + var_id = id(var) + if var_id not in self._pyomo_var_to_solver_var_map: + raise ValueError('The Var provided to update_var needs to be added first: {0}'.format(var)) + gurobipy_var = self._pyomo_var_to_solver_var_map[var_id] + vtype = self._gurobi_vtype_from_var(var) + if var.is_fixed(): + lb = var.value + ub = var.value + else: + lb = -self._gurobipy.GRB.INFINITY + ub = self._gurobipy.GRB.INFINITY + if var.has_lb(): + lb = value(var.lb) + if var.has_ub(): + ub = value(var.ub) + gurobipy_var.setAttr('lb', lb) + gurobipy_var.setAttr('ub', ub) + gurobipy_var.setAttr('vtype', vtype) + 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 = self._gurobipy.GRB.BINARY + elif var.is_integer(): + vtype = self._gurobipy.GRB.INTEGER + elif var.is_continuous(): + vtype = self._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[id(var)] -= 1 + self._vars_referenced_by_obj = ComponentSet() + self._objective = None + self._objective_expr = None + self._obj_named_expressions = list() + + if obj.active is False: + raise ValueError('Cannot add inactive objective to solver.') + + if obj.sense == minimize: + sense = self._gurobipy.GRB.MINIMIZE + elif obj.sense == maximize: + sense = self._gurobipy.GRB.MAXIMIZE + else: + raise ValueError('Objective sense is not recognized: {0}'.format(obj.sense)) + + if self.config.check_for_updated_mutable_params_in_objective: + (gurobi_expr, + referenced_vars, + repn_constant, + mutable_linear_coefficients, + mutable_quadratic_coefficients) = self._get_expr_from_pyomo_expr(obj.expr) + else: + gurobi_expr = self._walker.walk_expression(obj.expr) + referenced_vars = self._walker.referenced_vars + repn_constant = 0 + mutable_linear_coefficients = list() + mutable_quadratic_coefficients = list() + + if self.config.update_named_expressions: + assert len(self._obj_named_expressions) == 0 + for e in identify_components(obj.expr, {SimpleExpression, _GeneralExpressionData}): + self._obj_named_expressions.append((e, e.expr)) + + mutable_constant = _MutableConstant() + mutable_constant.expr = repn_constant + mutable_objective = _MutableObjective(self._solver_model, + mutable_constant, + mutable_linear_coefficients, + mutable_quadratic_coefficients) + self._mutable_objective = mutable_objective + + for var in referenced_vars: + self._referenced_variables[id(var)] += 1 + + self._solver_model.setObjective(gurobi_expr + value(repn_constant), sense=sense) + self._objective = obj + self._objective_expr = obj.expr + 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. + suffixes = list(self._pyomo_model.component_objects(Suffix, active=True, descend_into=False, sort=True)) + extract_duals = False + extract_slacks = False + extract_reduced_costs = False + for suffix in 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 = self._gurobipy.GRB + status = gprob.Status + + if gprob.getAttr(self._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 + + results = GurobiPersistentResults(self._pyomo_model, self) + self._last_results_object = results + results.solver.wallclock_time = gprob.Runtime + + if status == grb.LOADED: # problem is loaded, but no solution + results.solver.termination_condition = TerminationCondition.unknown + elif status == grb.OPTIMAL: # optimal + results.solver.termination_condition = TerminationCondition.optimal + elif status == grb.INFEASIBLE: + results.solver.termination_condition = TerminationCondition.infeasible + elif status == grb.INF_OR_UNBD: + results.solver.termination_condition = TerminationCondition.infeasibleOrUnbounded + elif status == grb.UNBOUNDED: + results.solver.termination_condition = TerminationCondition.unbounded + elif status == grb.CUTOFF: + results.solver.termination_condition = TerminationCondition.objectiveLimit + elif status == grb.ITERATION_LIMIT: + results.solver.termination_condition = TerminationCondition.maxIterations + elif status == grb.NODE_LIMIT: + results.solver.termination_condition = TerminationCondition.maxIterations + elif status == grb.TIME_LIMIT: + results.solver.termination_condition = TerminationCondition.maxTimeLimit + elif status == grb.SOLUTION_LIMIT: + results.solver.termination_condition = TerminationCondition.unknown + elif status == grb.INTERRUPTED: + results.solver.termination_condition = TerminationCondition.interrupted + elif status == grb.NUMERIC: + results.solver.termination_condition = TerminationCondition.unknown + elif status == grb.SUBOPTIMAL: + results.solver.termination_condition = TerminationCondition.unknown + elif status == grb.USER_OBJ_LIMIT: + results.solver.termination_condition = TerminationCondition.objectiveLimit + else: + results.solver.termination_condition = TerminationCondition.unknown + + try: + results.solver.best_feasible_objective = gprob.ObjVal + except (self._gurobipy.GurobiError, AttributeError): + pass + try: + results.solver.best_objective_bound = gprob.ObjBound + except (self._gurobipy.GurobiError, AttributeError): + pass + + if self._tmp_config.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() + + return results + + def _load_suboptimal_mip_solution(self, vars_to_load, solution_number): + if self.get_model_attr('NumIntVars') == 0 and self.get_model_attr('NumBinVars') == 0: + raise ValueError('Cannot obtain suboptimal solutions for a continuous model') + var_map = self._pyomo_var_to_solver_var_map + ref_vars = self._referenced_variables + original_solution_number = self._solver.get_gurobi_param('SolutionNumber') + self._solver.set_gurobi_param('SolutionNumber', solution_number) + gurobi_vars_to_load = [var_map[id(pyomo_var)] for pyomo_var in vars_to_load] + vals = self._solver_model.getAttr("Xn", gurobi_vars_to_load) + for var, val in zip(vars_to_load, vals): + if ref_vars[id(var)] > 0: + var.value = val + self._solver.set_gurobi_param('SolutionNumber', original_solution_number) + + def load_vars(self, vars_to_load=None, solution_number=0): + if self._needs_updated: + self._update_gurobi_model() # this is needed to ensure that solutions cannot be loaded after the model has been changed + var_map = self._pyomo_var_to_solver_var_map + ref_vars = self._referenced_variables + if vars_to_load is None: + vars_to_load = self._solver_var_to_pyomo_var_map.values() + + if solution_number != 0: + self._load_suboptimal_mip_solution(vars_to_load=vars_to_load, solution_number=solution_number) + else: + gurobi_vars_to_load = [var_map[id(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[id(var)] > 0: + var.value = val + + def load_rc(self, vars_to_load=None): + if self._needs_updated: + self._update_gurobi_model() + 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 = self._solver_var_to_pyomo_var_map.values() + + gurobi_vars_to_load = [var_map[id(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[id(var)] > 0: + rc[var] = val + + def load_duals(self, cons_to_load=None): + if self._needs_updated: + self._update_gurobi_model() + 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() + 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 = list(gurobi_cons_to_load.intersection(set(self._solver_model.getConstrs()))) + quadratic_cons_to_load = list(gurobi_cons_to_load.intersection(set(self._solver_model.getQConstrs()))) + linear_vals = self._solver_model.getAttr("Pi", linear_cons_to_load) + 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[id(gurobi_con)] + dual[pyomo_con] = val + for gurobi_con, val in zip(quadratic_cons_to_load, quadratic_vals): + pyomo_con = reverse_con_map[id(gurobi_con)] + dual[pyomo_con] = val + + def load_slacks(self, cons_to_load=None): + if self._needs_updated: + self._update_gurobi_model() + 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() + 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 = list(gurobi_cons_to_load.intersection(set(self._solver_model.getConstrs()))) + quadratic_cons_to_load = list(gurobi_cons_to_load.intersection(set(self._solver_model.getQConstrs()))) + linear_vals = self._solver_model.getAttr("Slack", linear_cons_to_load) + 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[id(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 + for gurobi_con, val in zip(quadratic_cons_to_load, quadratic_vals): + pyomo_con = reverse_con_map[id(gurobi_con)] + slack[pyomo_con] = val + + def update(self): + if self.config.check_for_new_or_removed_vars or self.config.update_vars: + last_solve_vars = ComponentSet(self._solver_var_to_pyomo_var_map.values()) + current_vars = ComponentSet(v for v in self._pyomo_model.component_data_objects(Var, descend_into=True, sort=True)) + new_vars = current_vars - last_solve_vars + old_vars = last_solve_vars - current_vars + if self.config.check_for_new_or_removed_constraints or self.config.update_cons: + last_solve_cons = ComponentSet(self._solver_con_to_pyomo_con_map.values()) + current_cons = ComponentSet(c for c in self._pyomo_model.component_data_objects(Constraint, active=True, descend_into=True, sort=True)) + new_cons = current_cons - last_solve_cons + old_cons = last_solve_cons - current_cons + if self.config.check_for_new_or_removed_constraints: + for c in old_cons: + self.remove_constraint(c) + for c in self._pyomo_sos_to_solver_sos_map.keys(): + self.remove_sos_constraint(c) + if self.config.check_for_new_or_removed_vars: + for v in old_vars: + self.remove_var(v) + for v in new_vars: + self.add_var(v) + if self.config.check_for_new_or_removed_constraints: + for c in new_cons: + self.add_constraint(c) + for c in self._pyomo_model.component_data_objects(SOSConstraint, descend_into=True, active=True, sort=True): + self.add_sos_constraint(c) + if self.config.update_constraints: + cons_to_update = current_cons - new_cons + for c in cons_to_update: + if ((c.body is not self._constraint_bodies[id(c)]) or + (value(c.lower) != self._constraint_lowers[id(c)]) or + (value(c.upper) != self._constraint_uppers[id(c)])): + self.remove_constraint(c) + self.add_constraint(c) + if self.config.update_vars: + vars_to_update = current_vars - new_vars + gurobi_vars = list() + lbs = list() + ubs = list() + vtypes = list() + for v in vars_to_update: + vtypes.append(self._gurobi_vtype_from_var(v)) + if v.is_fixed(): + lbs.append(v.value) + ubs.append(v.value) + else: + lb = -self._gurobipy.GRB.INFINITY + ub = self._gurobipy.GRB.INFINITY + if v.has_lb(): + lb = value(v.lb) + if v.has_ub(): + ub = value(v.ub) + lbs.append(lb) + ubs.append(ub) + gurobi_vars.append(self._pyomo_var_to_solver_var_map[id(v)]) + self._solver_model.setAttr('lb', gurobi_vars, lbs) + self._solver_model.setAttr('ub', gurobi_vars, ubs) + self._solver_model.setAttr('vtype', gurobi_vars, vtypes) + if self.config.update_named_expressions: + for c, expr_list in self._named_expressions.items(): + for named_expr, old_expr in expr_list: + if not (named_expr.expr is old_expr): + self._remove_constraint(c) + self.add_constraint(c) + break + if self.config.check_for_updated_mutable_params_in_constraints: + for con, helpers in self._mutable_helpers.items(): + for helper in helpers: + helper.update() + for con, helper in self._mutable_quadratic_helpers.items(): + if con in self._constraints_added_since_update: + self._update_gurobi_model() + gurobi_con = helper.con + new_gurobi_expr = helper.get_updated_expression() + new_rhs = helper.get_updated_rhs() + new_sense = gurobi_con.sense + pyomo_con = self._solver_con_to_pyomo_con_map[id(gurobi_con)] + name = self._symbol_map.getSymbol(pyomo_con, self._labeler) + self._solver_model.remove(gurobi_con) + new_con = self._solver_model.addQConstr(new_gurobi_expr, new_sense, new_rhs, name=name) + self._pyomo_con_to_solver_con_map[id(pyomo_con)] = new_con + del self._solver_con_to_pyomo_con_map[id(gurobi_con)] + self._solver_con_to_pyomo_con_map[id(new_con)] = pyomo_con + helper.con = new_con + self._constraints_added_since_update.add(con) + pyomo_obj = _get_objective(self._pyomo_model) + already_called_set_objective = False + if not (pyomo_obj is self._objective): + self._set_objective(pyomo_obj) + already_called_set_objective = True + if (not already_called_set_objective) and (not (pyomo_obj.expr is self._objective_expr)): + self._set_objective(pyomo_obj) + already_called_set_objective = True + if (not already_called_set_objective) and self.config.update_named_expressions: + for named_expr, old_expr in self._obj_named_expressions: + if not (named_expr.expr is old_expr): + self._set_objective(pyomo_obj) + already_called_set_objective = True + break + if (not already_called_set_objective) and self.config.check_for_updated_mutable_params_in_objective: + helper = self._mutable_objective + new_gurobi_expr = helper.get_updated_expression() + if new_gurobi_expr is not None: + if pyomo_obj.sense == minimize: + sense = self._gurobipy.GRB.MINIMIZE + else: + sense = self._gurobipy.GRB.MAXIMIZE + self._solver_model.setObjective(new_gurobi_expr, sense=sense) + self._needs_updated = True + + def _update_gurobi_model(self): + self._solver_model.update() + self._constraints_added_since_update = set() + self._vars_added_since_update = ComponentSet() + self._needs_updated = False + + def get_model_attr(self, attr): + """ + Get the value of an attribute on the Gurobi model. + + Parameters + ---------- + attr: str + The attribute to get. See Gurobi documentation for descriptions of the attributes. + """ + if self._needs_updated: + self._update_gurobi_model() + return self._solver_model.getAttr(attr) + + def write(self, filename): + """ + Write the model to a file (e.g., and lp file). + + Parameters + ---------- + filename: str + Name of the file to which the model should be written. + """ + self._solver_model.write(filename) + self._constraints_added_since_update = set() + self._vars_added_since_update = ComponentSet() + self._needs_updated = False + + def set_linear_constraint_attr(self, con, attr, val): + """ + Set the value of an attribute on a gurobi linear constraint. + + Parameters + ---------- + con: pyomo.core.base.constraint._GeneralConstraintData + The pyomo constraint for which the corresponding gurobi constraint attribute + should be modified. + attr: str + The attribute to be modified. Options are: + CBasis + DStart + Lazy + val: any + See gurobi documentation for acceptable values. + """ + if attr in {'Sense', 'RHS', 'ConstrName'}: + raise ValueError('Linear constraint attr {0} cannot be set with' + + ' the set_linear_constraint_attr method. Please use' + + ' the remove_constraint and add_constraint methods.'.format(attr)) + self._pyomo_con_to_solver_con_map[con].setAttr(attr, val) + self._needs_updated = True + + def set_var_attr(self, var, attr, val): + """ + Set the value of an attribute on a gurobi variable. + + Parameters + ---------- + var: pyomo.core.base.var._GeneralVarData + The pyomo var for which the corresponding gurobi var attribute + should be modified. + attr: str + The attribute to be modified. Options are: + Start + VarHintVal + VarHintPri + BranchPriority + VBasis + PStart + val: any + See gurobi documentation for acceptable values. + """ + if attr in {'LB', 'UB', 'VType', 'VarName'}: + raise ValueError('Var attr {0} cannot be set with' + + ' the set_var_attr method. Please use' + + ' the update_var method.'.format(attr)) + if attr == 'Obj': + raise ValueError('Var attr Obj cannot be set with' + + ' the set_var_attr method. Please use' + + ' the set_objective method.') + self._pyomo_var_to_solver_var_map[id(var)].setAttr(attr, val) + self._needs_updated = True + + def get_var_attr(self, var, attr): + """ + Get the value of an attribute on a gurobi var. + + Parameters + ---------- + var: pyomo.core.base.var._GeneralVarData + The pyomo var for which the corresponding gurobi var attribute + should be retrieved. + attr: str + The attribute to get. See gurobi documentation + """ + if self._needs_updated: + self._update() + return self._pyomo_var_to_solver_var_map[id(var)].getAttr(attr) + + def get_linear_constraint_attr(self, con, attr): + """ + Get the value of an attribute on a gurobi linear constraint. + + Parameters + ---------- + con: pyomo.core.base.constraint._GeneralConstraintData + The pyomo constraint for which the corresponding gurobi constraint attribute + should be retrieved. + attr: str + The attribute to get. See the Gurobi documentation + """ + if self._needs_updated: + self._update() + return self._pyomo_con_to_solver_con_map[con].getAttr(attr) + + def get_sos_attr(self, con, attr): + """ + Get the value of an attribute on a gurobi sos constraint. + + Parameters + ---------- + con: pyomo.core.base.sos._SOSConstraintData + The pyomo SOS constraint for which the corresponding gurobi SOS constraint attribute + should be retrieved. + attr: str + The attribute to get. See the Gurobi documentation + """ + if self._needs_updated: + self._update() + return self._pyomo_sos_to_solver_sos_map[con].getAttr(attr) + + def get_quadratic_constraint_attr(self, con, attr): + """ + Get the value of an attribute on a gurobi quadratic constraint. + + Parameters + ---------- + con: pyomo.core.base.constraint._GeneralConstraintData + The pyomo constraint for which the corresponding gurobi constraint attribute + should be retrieved. + attr: str + The attribute to get. See the Gurobi documentation + """ + if self._needs_updated: + self._update() + return self._pyomo_con_to_solver_con_map[con].getAttr(attr) + + def set_gurobi_param(self, param, val): + """ + Set a gurobi parameter. + + Parameters + ---------- + param: str + The gurobi parameter to set. Options include any gurobi parameter. + Please see the Gurobi documentation for options. + val: any + The value to set the parameter to. See Gurobi documentation for possible values. + """ + self._solver_model.setParam(param, val) + + def get_gurobi_param_info(self, param): + """ + Get information about a gurobi parameter. + + Parameters + ---------- + param: str + The gurobi parameter to get info for. See Gurobi documenation for possible options. + + Returns + ------- + six-tuple containing the parameter name, type, value, minimum value, maximum value, and default value. + """ + return self._solver_model.getParamInfo(param) + + def _intermediate_callback(self): + def f(gurobi_model, where): + self._callback_func(self._pyomo_model, self, where) + return f + + def set_callback(self, func=None): + """ + Specify a callback for gurobi to use. + + Parameters + ---------- + func: function + The function to call. The function should have three arguments. The first will be the pyomo model being + solved. The second will be the GurobiPersistent instance. The third will be an enum member of + gurobipy.GRB.Callback. This will indicate where in the branch and bound algorithm gurobi is at. For + example, suppose we want to solve + + min 2*x + y + s.t. + y >= (x-2)**2 + 0 <= x <= 4 + y >= 0 + y integer + + as an MILP using exteneded cutting planes in callbacks. + + >>> + >> from gurobipy import GRB + >> import pyomo.environ as pe + >> from pyomo.core.expr.taylor_series import taylor_series_expansion + >> + >> m = pe.ConcreteModel() + >> m.x = pe.Var(bounds=(0, 4)) + >> m.y = pe.Var(within=pe.Integers, bounds=(0, None)) + >> m.obj = pe.Objective(expr=2*m.x + m.y) + >> m.cons = pe.ConstraintList() # for the cutting planes + >> + >> def _add_cut(xval): + >> # a function to generate the cut + >> m.x.value = xval + >> return m.cons.add(m.y >= taylor_series_expansion((m.x - 2)**2)) + >> + >> _add_cut(0) # start with 2 cuts at the bounds of x + >> _add_cut(4) # this is an arbitrary choice + >> + >> opt = pe.SolverFactory('gurobi_persistent') + >> opt.set_instance(m) + >> opt.set_gurobi_param('PreCrush', 1) + >> opt.set_gurobi_param('LazyConstraints', 1) + >> + >> def my_callback(cb_m, cb_opt, cb_where): + >> if cb_where == GRB.Callback.MIPSOL: + >> cb_opt.cbGetSolution(vars=[m.x, m.y]) + >> if m.y.value < (m.x.value - 2)**2 - 1e-6: + >> cb_opt.cbLazy(_add_cut(m.x.value)) + >> + >> opt.set_callback(my_callback) + >> opt.solve() + >> assert abs(m.x.value - 1) <= 1e-6 + >> assert abs(m.y.value - 1) <= 1e-6 + + """ + if func is not None: + self._callback_func = func + self._callback = self._intermediate_callback() + else: + self._callback = None + self._callback_func = None + + def cbCut(self, con): + """ + Add a cut within a callback. + + Parameters + ---------- + con: pyomo.core.base.constraint._GeneralConstraintData + The cut to add + """ + if not con.active: + raise ValueError('cbCut expected an active constraint.') + + if is_fixed(con.body): + raise ValueError('cbCut expected a non-trival constraint') + + gurobi_expr = self._walker.walk_expression(con.body) + + if con.has_lb(): + if con.has_ub(): + raise ValueError('Range constraints are not supported in cbCut.') + 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: + self._solver_model.cbCut(lhs=gurobi_expr, sense=self._gurobipy.GRB.EQUAL, + rhs=value(con.lower)) + elif con.has_lb() and (value(con.lower) > -float('inf')): + self._solver_model.cbCut(lhs=gurobi_expr, sense=self._gurobipy.GRB.GREATER_EQUAL, + rhs=value(con.lower)) + elif con.has_ub() and (value(con.upper) < float('inf')): + self._solver_model.cbCut(lhs=gurobi_expr, sense=self._gurobipy.GRB.LESS_EQUAL, + rhs=value(con.upper)) + else: + raise ValueError('Constraint does not have a lower or an upper bound {0} \n'.format(con)) + + def cbGet(self, what): + return self._solver_model.cbGet(what) + + def cbGetNodeRel(self, vars): + """ + Parameters + ---------- + vars: Var or iterable of Var + """ + if not isinstance(vars, collections.Iterable): + vars = [vars] + gurobi_vars = [self._pyomo_var_to_solver_var_map[id(i)] for i in vars] + var_values = self._solver_model.cbGetNodeRel(gurobi_vars) + for i, v in enumerate(vars): + v.value = var_values[i] + + def cbGetSolution(self, vars): + """ + Parameters + ---------- + vars: iterable of vars + """ + if not isinstance(vars, collections.Iterable): + vars = [vars] + gurobi_vars = [self._pyomo_var_to_solver_var_map[id(i)] for i in vars] + var_values = self._solver_model.cbGetSolution(gurobi_vars) + for i, v in enumerate(vars): + v.value = var_values[i] + + def cbLazy(self, con): + """ + Parameters + ---------- + con: pyomo.core.base.constraint._GeneralConstraintData + The lazy constraint to add + """ + if not con.active: + raise ValueError('cbLazy expected an active constraint.') + + if is_fixed(con.body): + raise ValueError('cbLazy expected a non-trival constraint') + + gurobi_expr = self._walker.walk_expression(con.body) + + if con.has_lb(): + if con.has_ub(): + raise ValueError('Range constraints are not supported in cbLazy.') + 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: + self._solver_model.cbLazy(lhs=gurobi_expr, sense=self._gurobipy.GRB.EQUAL, + rhs=value(con.lower)) + elif con.has_lb() and (value(con.lower) > -float('inf')): + self._solver_model.cbLazy(lhs=gurobi_expr, sense=self._gurobipy.GRB.GREATER_EQUAL, + rhs=value(con.lower)) + elif con.has_ub() and (value(con.upper) < float('inf')): + self._solver_model.cbLazy(lhs=gurobi_expr, sense=self._gurobipy.GRB.LESS_EQUAL, + rhs=value(con.upper)) + else: + raise ValueError('Constraint does not have a lower or an upper bound {0} \n'.format(con)) + + def cbSetSolution(self, vars, solution): + if not isinstance(vars, collections.Iterable): + vars = [vars] + gurobi_vars = [self._pyomo_var_to_solver_var_map[id(i)] for i in vars] + self._solver_model.cbSetSolution(gurobi_vars, solution) + + def cbUseSolution(self): + return self._solver_model.cbUseSolution() + + def reset(self): + self._solver_model.reset() + + +GurobiPersistentNew.solve.__doc__ = add_docstring_list(GurobiPersistentNew.solve.__doc__, GurobiPersistentNew.CONFIG)