Source code for pyomo.contrib.solver.solvers.gurobi.gurobi_persistent

# ____________________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2026 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.
# ____________________________________________________________________________________

from __future__ import annotations
import logging
from typing import Dict, List, Optional, Sequence, Mapping
from collections.abc import Iterable

from pyomo.common.collections import ComponentSet, OrderedSet, ComponentMap
from pyomo.common.errors import PyomoException
from pyomo.common.shutdown import python_is_shutting_down
from pyomo.common.timing import HierarchicalTimer
from pyomo.core.base.objective import ObjectiveData
from pyomo.core.kernel.objective import minimize, maximize
from pyomo.core.base.var import VarData
from pyomo.core.base.constraint import ConstraintData, Constraint
from pyomo.core.base.sos import SOSConstraintData, SOSConstraint
from pyomo.core.base.param import ParamData
from pyomo.core.expr.numvalue import value, is_constant, is_fixed, native_numeric_types
from pyomo.repn import generate_standard_repn
from pyomo.contrib.solver.common.results import Results
from pyomo.contrib.solver.common.util import IncompatibleModelError
from pyomo.contrib.solver.common.base import PersistentSolverBase
from pyomo.core.staleflag import StaleFlagManager
from .gurobi_direct_base import (
    GurobiDirectBase,
    gurobipy,
    GurobiConfig,
    GurobiDirectSolutionLoaderBase,
)
from .gurobi_direct import GurobiDirectSolutionLoader
from pyomo.contrib.solver.common.util import get_objective
from pyomo.contrib.observer.model_observer import (
    Observer,
    ModelChangeDetector,
    AutoUpdateConfig,
    Reason,
)

logger = logging.getLogger(__name__)


[docs] class GurobiPersistentSolutionLoader(GurobiDirectSolutionLoaderBase):
[docs] def __init__(self, solver_model, var_map, con_map) -> None: super().__init__(solver_model) self._var_map = var_map self._con_map = con_map self._valid = True
def _get_var_lists(self): return list(self._var_map.keys()), list(self._var_map.values()) def _get_var_map(self): return self._var_map def _get_con_map(self): return self._con_map def invalidate(self): self._valid = False def _assert_solution_still_valid(self): if not self._valid: raise RuntimeError('The results in the solver are no longer valid.')
[docs] def load_vars( self, vars_to_load: Sequence[VarData] | None = None, solution_id=0 ) -> None: self._assert_solution_still_valid() return super().load_vars(vars_to_load, solution_id)
[docs] def get_primals( self, vars_to_load: Sequence[VarData] | None = None, solution_id=0 ) -> Mapping[VarData, float]: self._assert_solution_still_valid() return super().get_primals(vars_to_load, solution_id)
[docs] def get_duals( self, cons_to_load: Sequence[ConstraintData] | None = None ) -> Dict[ConstraintData, float]: self._assert_solution_still_valid() return super().get_duals(cons_to_load)
[docs] def get_reduced_costs( self, vars_to_load: Sequence[VarData] | None = None ) -> Mapping[VarData, float]: self._assert_solution_still_valid() return super().get_reduced_costs(vars_to_load)
class _MutableLowerBound: __slots__ = ("var", "expr", "var_map") def __init__(self, var, expr, var_map): self.var = var self.expr = expr self.var_map = var_map def update(self): self.var_map[self.var].setAttr('lb', value(self.expr)) class _MutableUpperBound: __slots__ = ("var", "expr", "var_map") def __init__(self, var, expr, var_map): self.var = var self.expr = expr self.var_map = var_map def update(self): self.var_map[self.var].setAttr('ub', value(self.expr)) class _MutableLinearCoefficient: __slots__ = ("expr", "pyomo_con", "pyomo_var", "con_map", "var_map", "gurobi_model") def __init__(self, expr, pyomo_con, con_map, pyomo_var, var_map, gurobi_model): self.expr = expr self.pyomo_con = pyomo_con self.pyomo_var = pyomo_var self.con_map = con_map self.var_map = var_map self.gurobi_model = gurobi_model @property def gurobi_var(self): return self.var_map[self.pyomo_var] @property def gurobi_con(self): return self.con_map[self.pyomo_con] def update(self): self.gurobi_model.chgCoeff(self.gurobi_con, self.gurobi_var, value(self.expr)) class _MutableRangeConstant: __slots__ = ( "lhs_expr", "rhs_expr", "pyomo_con", "con_map", "slack_name", "gurobi_model", ) def __init__( self, lhs_expr, rhs_expr, pyomo_con, con_map, slack_name, gurobi_model ): self.lhs_expr = lhs_expr self.rhs_expr = rhs_expr self.pyomo_con = pyomo_con self.con_map = con_map self.slack_name = slack_name self.gurobi_model = gurobi_model def update(self): rhs_val = value(self.rhs_expr) lhs_val = value(self.lhs_expr) con = self.con_map[self.pyomo_con] con.rhs = rhs_val slack = self.gurobi_model.getVarByName(self.slack_name) slack.ub = rhs_val - lhs_val class _MutableConstant: __slots__ = ("expr", "pyomo_con", "con_map") def __init__(self, expr, pyomo_con, con_map): self.expr = expr self.pyomo_con = pyomo_con self.con_map = con_map def update(self): con = self.con_map[self.pyomo_con] con.rhs = value(self.expr) class _MutableQuadraticConstraint: __slots__ = ( "pyomo_con", "con_map", "gurobi_model", "constant", "last_constant_value", "linear_coefs", "last_linear_coef_values", "quadratic_coefs", "last_quadratic_coef_values", ) def __init__( self, gurobi_model, pyomo_con, con_map, constant, linear_coefs, quadratic_coefs ): self.pyomo_con = pyomo_con self.con_map = con_map 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] @property def gurobi_con(self): return self.con_map[self.pyomo_con] def get_updated_expression(self): gurobi_expr = self.gurobi_model.getQCRow(self.gurobi_con) for ndx, coef in enumerate(self.linear_coefs): current_coef_value = value(coef.expr) incremental_coef_value = ( current_coef_value - self.last_linear_coef_values[ndx] ) gurobi_expr += incremental_coef_value * coef.gurobi_var self.last_linear_coef_values[ndx] = current_coef_value for ndx, coef in enumerate(self.quadratic_coefs): current_coef_value = value(coef.expr) incremental_coef_value = ( current_coef_value - self.last_quadratic_coef_values[ndx] ) gurobi_expr += incremental_coef_value * coef.var1 * coef.var2 self.last_quadratic_coef_values[ndx] = current_coef_value return gurobi_expr def get_updated_rhs(self): return value(self.constant.expr) class _MutableObjective: __slots__ = ( "gurobi_model", "constant", "linear_coefs", "quadratic_coefs", "last_quadratic_coef_values", ) def __init__(self, gurobi_model, constant, linear_coefs, quadratic_coefs): self.gurobi_model = gurobi_model self.constant: _MutableConstant = constant self.linear_coefs: List[_MutableLinearCoefficient] = linear_coefs self.quadratic_coefs: List[_MutableQuadraticCoefficient] = quadratic_coefs self.last_quadratic_coef_values: List[float] = [ value(i.expr) for i in self.quadratic_coefs ] def get_updated_expression(self): for ndx, coef in enumerate(self.linear_coefs): coef.gurobi_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() current_coef_value = value(coef.expr) incremental_coef_value = ( current_coef_value - self.last_quadratic_coef_values[ndx] ) gurobi_expr += incremental_coef_value * coef.var1 * coef.var2 self.last_quadratic_coef_values[ndx] = current_coef_value return gurobi_expr class _MutableQuadraticCoefficient: __slots__ = ("expr", "var_map", "v1", "v2") def __init__(self, expr, v1, v2, var_map): self.expr = expr self.var_map = var_map self.v1 = v1 self.v2 = v2 @property def var1(self): return self.var_map[self.v1] @property def var2(self): return self.var_map[self.v2]
[docs] class GurobiPersistentConfig(GurobiConfig):
[docs] def __init__( self, description=None, doc=None, implicit=False, implicit_domain=None, visibility=0, ): GurobiConfig.__init__( self, description=description, doc=doc, implicit=implicit, implicit_domain=implicit_domain, visibility=visibility, ) self.auto_updates: bool = self.declare('auto_updates', AutoUpdateConfig())
[docs] class GurobiPersistent(GurobiDirectBase, PersistentSolverBase, Observer): _minimum_version = (7, 0, 0) CONFIG = GurobiPersistentConfig()
[docs] def __init__(self, **kwds): super().__init__(**kwds) # we actually want to only grab the license when # set_instance is called self._release_env_client() self._solver_model = None self._pyomo_var_to_solver_var_map = ComponentMap() self._pyomo_con_to_solver_con_map = {} self._pyomo_sos_to_solver_sos_map = {} self._pyomo_model = None self._objective = None self._mutable_helpers = {} self._mutable_bounds = {} self._mutable_quadratic_helpers = {} self._mutable_objective = None self._needs_updated = True self._callback_func = None self._constraints_added_since_update = OrderedSet() self._vars_added_since_update = ComponentSet() self._last_results_object: Optional[Results] = None self._change_detector = None self._constraint_ndx = 0 self._disallow_set_var_attr = {'lb', 'ub', 'vtype', 'varname'} self._disallow_linear_constraint_attr = {'sense', 'rhs', 'constrname'}
def _clear(self): release = False if self._solver_model is not None: release = True self._solver_model = None if release: self._release_env_client() self._pyomo_var_to_solver_var_map = ComponentMap() self._pyomo_con_to_solver_con_map = {} self._pyomo_sos_to_solver_sos_map = {} self._pyomo_model = None self._objective = None self._mutable_helpers = {} self._mutable_bounds = {} self._mutable_quadratic_helpers = {} self._mutable_objective = None self._needs_updated = True self._constraints_added_since_update = OrderedSet() self._vars_added_since_update = ComponentSet() self._last_results_object = None self._change_detector = None self._constraint_ndx = 0 def _create_solver_model(self, pyomo_model, config): if pyomo_model is self._pyomo_model: self.update(**config) else: self.set_instance(pyomo_model, **config) solution_loader = GurobiPersistentSolutionLoader( solver_model=self._solver_model, var_map=self._pyomo_var_to_solver_var_map, con_map=self._pyomo_con_to_solver_con_map, ) has_obj = self._objective is not None return self._solver_model, solution_loader, has_obj def _pyomo_gurobi_var_iter(self): return self._pyomo_var_to_solver_var_map.items() def release_license(self): self._clear() super().release_license()
[docs] def solve(self, model, **kwds) -> Results: res = super().solve(model, **kwds) self._needs_updated = False return res
def _process_domain_and_bounds(self, var): lb, ub, step = var.domain.get_interval() if lb is None: lb = -gurobipy.GRB.INFINITY if ub is None: ub = gurobipy.GRB.INFINITY if step == 0: vtype = gurobipy.GRB.CONTINUOUS elif step == 1: if lb == 0 and ub == 1: vtype = gurobipy.GRB.BINARY else: vtype = gurobipy.GRB.INTEGER else: raise ValueError(f'Unrecognized domain: {var.domain}') if var.fixed: lb = var.value ub = lb else: if var._lb is not None: lb = max(lb, value(var._lb)) if var._ub is not None: ub = min(ub, value(var._ub)) if not is_constant(var._lb): mutable_lb = _MutableLowerBound( var, var.lower, self._pyomo_var_to_solver_var_map ) self._mutable_bounds[id(var), 'lb'] = (var, mutable_lb) if not is_constant(var._ub): mutable_ub = _MutableUpperBound( var, var.upper, self._pyomo_var_to_solver_var_map ) self._mutable_bounds[id(var), 'ub'] = (var, mutable_ub) return lb, ub, vtype def _add_variables(self, variables: List[VarData]): self._invalidate_last_results() vtypes = [] lbs = [] ubs = [] for ndx, var in enumerate(variables): lb, ub, vtype = self._process_domain_and_bounds(var) vtypes.append(vtype) lbs.append(lb) ubs.append(ub) gurobi_vars = self._solver_model.addVars( len(variables), lb=lbs, ub=ubs, vtype=vtypes ).values() for pyomo_var, gurobi_var in zip(variables, gurobi_vars): self._pyomo_var_to_solver_var_map[pyomo_var] = gurobi_var self._vars_added_since_update.update(variables) self._needs_updated = True
[docs] def set_instance(self, pyomo_model, **kwds): config = self.config(value=kwds, preserve_implicit=True) if config.timer is None: timer = HierarchicalTimer() else: timer = config.timer self._clear() self._register_env_client() self._pyomo_model = pyomo_model self._solver_model = gurobipy.Model(env=self.env()) timer.start('set_instance') self._change_detector = ModelChangeDetector( model=self._pyomo_model, observers=[self], **config.auto_updates ) timer.stop('set_instance')
def update(self, **kwds): config = self.config(value=kwds, preserve_implicit=True) if config.timer is None: timer = HierarchicalTimer() else: timer = config.timer if self._pyomo_model is None: raise RuntimeError('must call set_instance or solve before update') timer.start('update') if self._needs_updated: self._update_gurobi_model() self._change_detector.update(timer=timer, **config.auto_updates) timer.stop('update') def _get_expr_from_pyomo_repn(self, repn): if repn.nonlinear_expr is not None: raise NotImplementedError( f'GurobiPersistent only supports linear and quadratic expressions for now: {repn}.' ) if len(repn.linear_vars) > 0: coef_list = [value(i) for i in repn.linear_coefs] vlist = [self._pyomo_var_to_solver_var_map[v] for v in repn.linear_vars] new_expr = gurobipy.LinExpr(coef_list, vlist) else: new_expr = 0.0 if len(repn.quadratic_vars) > 0: for coef, (x, y) in zip(repn.quadratic_coefs, repn.quadratic_vars): gurobi_x = self._pyomo_var_to_solver_var_map[x] gurobi_y = self._pyomo_var_to_solver_var_map[y] new_expr += value(coef) * gurobi_x * gurobi_y return new_expr def _add_constraints(self, cons: List[ConstraintData]): self._invalidate_last_results() gurobi_expr_list = [] for ndx, con in enumerate(cons): lb, body, ub = con.to_bounded_expression(evaluate_bounds=False) repn = generate_standard_repn(body, quadratic=True, compute_values=False) gurobi_expr = self._get_expr_from_pyomo_repn(repn) mutable_constant = None if lb is None: if ub is None: raise ValueError( f"Constraint does not have a lower or an upper bound: {con}" ) rhs_expr = ub - repn.constant gurobi_expr_list.append(gurobi_expr <= float(value(rhs_expr))) if not is_constant(rhs_expr): mutable_constant = _MutableConstant( rhs_expr, con, self._pyomo_con_to_solver_con_map ) elif ub is None: rhs_expr = lb - repn.constant gurobi_expr_list.append(float(value(rhs_expr)) <= gurobi_expr) if not is_constant(rhs_expr): mutable_constant = _MutableConstant( rhs_expr, con, self._pyomo_con_to_solver_con_map ) elif con.equality: rhs_expr = lb - repn.constant gurobi_expr_list.append(gurobi_expr == float(value(rhs_expr))) if not is_constant(rhs_expr): mutable_constant = _MutableConstant( rhs_expr, con, self._pyomo_con_to_solver_con_map ) else: assert ( len(repn.quadratic_vars) == 0 ), "Quadratic range constraints are not supported" lhs_expr = lb - repn.constant rhs_expr = ub - repn.constant gurobi_expr_list.append( gurobi_expr == [float(value(lhs_expr)), float(value(rhs_expr))] ) if not is_constant(lhs_expr) or not is_constant(rhs_expr): conname = f'c{self._constraint_ndx}[{ndx}]' mutable_constant = _MutableRangeConstant( lhs_expr, rhs_expr, con, self._pyomo_con_to_solver_con_map, 'Rg' + conname, self._solver_model, ) mlc_list = [] for c, v in zip(repn.linear_coefs, repn.linear_vars): if not is_constant(c): mlc = _MutableLinearCoefficient( c, con, self._pyomo_con_to_solver_con_map, v, self._pyomo_var_to_solver_var_map, self._solver_model, ) mlc_list.append(mlc) if len(repn.quadratic_vars) == 0: if len(mlc_list) > 0: self._mutable_helpers[con] = mlc_list if mutable_constant is not None: if con not in self._mutable_helpers: self._mutable_helpers[con] = [] self._mutable_helpers[con].append(mutable_constant) else: if mutable_constant is None: mutable_constant = _MutableConstant( rhs_expr, con, self._pyomo_con_to_solver_con_map ) mqc_list = [] for coef, (x, y) in zip(repn.quadratic_coefs, repn.quadratic_vars): if not is_constant(coef): mqc = _MutableQuadraticCoefficient( coef, x, y, self._pyomo_var_to_solver_var_map ) mqc_list.append(mqc) mqc = _MutableQuadraticConstraint( self._solver_model, con, self._pyomo_con_to_solver_con_map, mutable_constant, mlc_list, mqc_list, ) self._mutable_quadratic_helpers[con] = mqc gurobi_cons = list( self._solver_model.addConstrs( (gurobi_expr_list[i] for i in range(len(gurobi_expr_list))), name=f'c{self._constraint_ndx}', ).values() ) self._constraint_ndx += 1 self._pyomo_con_to_solver_con_map.update(zip(cons, gurobi_cons)) self._constraints_added_since_update.update(cons) self._needs_updated = True def _add_sos_constraints(self, cons: List[SOSConstraintData]): self._invalidate_last_results() for con in cons: level = con.level if level == 1: sos_type = gurobipy.GRB.SOS_TYPE1 elif level == 2: sos_type = gurobipy.GRB.SOS_TYPE2 else: raise ValueError( f"Solver does not support SOS level {level} constraints" ) gurobi_vars = [] weights = [] for v, w in con.get_items(): gurobi_vars.append(self._pyomo_var_to_solver_var_map[v]) 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._constraints_added_since_update.update(cons) self._needs_updated = True def _remove_objectives(self, objs: List[ObjectiveData]): for obj in objs: if obj is not self._objective: raise RuntimeError( 'tried to remove an objective that has not been added: ' f'{str(obj)}' ) else: self._invalidate_last_results() self._solver_model.setObjective(0, sense=gurobipy.GRB.MINIMIZE) # see PR #2454 self._solver_model.update() self._objective = None self._needs_updated = False def _add_objectives(self, objs: List[ObjectiveData]): if len(objs) > 1: raise NotImplementedError( 'the persistent interface to gurobi currently ' f'only supports single-objective problems; got {len(objs)}: ' f'{[str(i) for i in objs]}' ) if len(objs) == 0: return obj = objs[0] if self._objective is not None: raise NotImplementedError( 'the persistent interface to gurobi currently ' 'only supports single-objective problems; tried to add ' f'an objective ({str(obj)}), but there is already an ' f'active objective ({str(self._objective)})' ) self._invalidate_last_results() if obj.sense == minimize: sense = gurobipy.GRB.MINIMIZE elif obj.sense == maximize: sense = gurobipy.GRB.MAXIMIZE else: raise ValueError(f'Objective sense is not recognized: {obj.sense}') repn = generate_standard_repn(obj.expr, quadratic=True, compute_values=False) repn_constant = value(repn.constant) gurobi_expr = self._get_expr_from_pyomo_repn(repn) mutable_constant = _MutableConstant(repn.constant, None, None) mlc_list = [] for c, v in zip(repn.linear_coefs, repn.linear_vars): if not is_constant(c): mlc = _MutableLinearCoefficient( c, None, None, v, self._pyomo_var_to_solver_var_map, self._solver_model, ) mlc_list.append(mlc) mqc_list = [] for coef, (x, y) in zip(repn.quadratic_coefs, repn.quadratic_vars): if not is_constant(coef): mqc = _MutableQuadraticCoefficient( coef, x, y, self._pyomo_var_to_solver_var_map ) mqc_list.append(mqc) self._mutable_objective = _MutableObjective( self._solver_model, mutable_constant, mlc_list, mqc_list ) self._solver_model.setObjective(gurobi_expr + repn_constant, sense=sense) self._objective = obj self._needs_updated = True def _update_gurobi_model(self): self._solver_model.update() self._constraints_added_since_update = OrderedSet() self._vars_added_since_update = ComponentSet() self._needs_updated = False def _remove_constraints(self, cons: List[ConstraintData]): self._invalidate_last_results() for con in cons: if con in self._constraints_added_since_update: self._update_gurobi_model() solver_con = self._pyomo_con_to_solver_con_map[con] self._solver_model.remove(solver_con) del self._pyomo_con_to_solver_con_map[con] self._mutable_helpers.pop(con, None) self._mutable_quadratic_helpers.pop(con, None) self._needs_updated = True def _remove_sos_constraints(self, cons: List[SOSConstraintData]): self._invalidate_last_results() for con in cons: 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) del self._pyomo_sos_to_solver_sos_map[con] self._needs_updated = True def _remove_variables(self, variables: List[VarData]): self._invalidate_last_results() for var in variables: v_id = id(var) if var in self._vars_added_since_update: self._update_gurobi_model() solver_var = self._pyomo_var_to_solver_var_map.pop(var) self._solver_model.remove(solver_var) self._mutable_bounds.pop((v_id, 'lb'), None) self._mutable_bounds.pop((v_id, 'ub'), None) self._needs_updated = True def _update_variables(self, variables: Mapping[VarData, Reason]): self._invalidate_last_results() new_vars = [] old_vars = [] mod_vars = [] for v, reason in variables.items(): if reason & Reason.added: new_vars.append(v) elif reason & Reason.removed: old_vars.append(v) else: mod_vars.append(v) if new_vars: self._add_variables(new_vars) if old_vars: self._remove_variables(old_vars) cons_to_reprocess = OrderedSet() cons_to_update = OrderedSet() reprocess_obj = False update_obj = False for v in mod_vars: reason = variables[v] if reason & (Reason.bounds | Reason.domain | Reason.fixed | Reason.value): var_id = id(v) self._mutable_bounds.pop((var_id, 'lb'), None) self._mutable_bounds.pop((var_id, 'ub'), None) gurobipy_var = self._pyomo_var_to_solver_var_map[v] lb, ub, vtype = self._process_domain_and_bounds(v) gurobipy_var.setAttr('lb', lb) gurobipy_var.setAttr('ub', ub) gurobipy_var.setAttr('vtype', vtype) if reason & Reason.fixed: cons_to_reprocess.update( self._change_detector.get_constraints_impacted_by_var(v) ) objs = self._change_detector.get_objectives_impacted_by_var(v) if objs: assert len(objs) == 1 assert objs[0] is self._objective reprocess_obj = True elif (reason & Reason.value) and v.fixed: cons_to_update.update( self._change_detector.get_constraints_impacted_by_var(v) ) objs = self._change_detector.get_objectives_impacted_by_var(v) if objs: assert len(objs) == 1 assert objs[0] is self._objective update_obj = True self._remove_constraints(cons_to_reprocess) self._add_constraints(cons_to_reprocess) cons_to_update -= cons_to_reprocess for c in cons_to_update: if c in self._mutable_helpers: for i in self._mutable_helpers[c]: i.update() self._update_quadratic_constraint(c) if reprocess_obj: obj = self._objective self._remove_objectives([obj]) self._add_objectives([obj]) elif update_obj: self._mutable_objective_update() self._needs_updated = True def _update_constraints(self, cons: Mapping[ConstraintData, Reason]): self._invalidate_last_results() new_cons = [] old_cons = [] for c, reason in cons.items(): if reason & Reason.added: new_cons.append(c) elif reason & Reason.removed: old_cons.append(c) elif reason & Reason.expr: old_cons.append(c) new_cons.append(c) if old_cons: self._remove_constraints(old_cons) if new_cons: self._add_constraints(new_cons) self._needs_updated = True def _update_sos_constraints(self, cons: Mapping[SOSConstraintData, Reason]): self._invalidate_last_results() new_cons = [] old_cons = [] for c, reason in cons.items(): if reason & Reason.added: new_cons.append(c) elif reason & Reason.removed: old_cons.append(c) elif reason & Reason.sos_items: old_cons.append(c) new_cons.append(c) if old_cons: self._remove_sos_constraints(old_cons) if new_cons: self._add_sos_constraints(new_cons) self._needs_updated = True def _update_objectives(self, objs: Mapping[ObjectiveData, Reason]): self._invalidate_last_results() new_objs = [] old_objs = [] new_sense = [] for obj, reason in objs.items(): if reason & Reason.added: new_objs.append(obj) elif reason & Reason.removed: old_objs.append(obj) elif reason & Reason.expr: old_objs.append(obj) new_objs.append(obj) elif reason & Reason.sense: new_sense.append(obj) if old_objs: self._remove_objectives(old_objs) if new_objs: self._add_objectives(new_objs) if new_sense: assert len(new_sense) == 1 obj = new_sense[0] assert obj is self._objective if obj.sense == minimize: sense = gurobipy.GRB.MINIMIZE elif obj.sense == maximize: sense = gurobipy.GRB.MAXIMIZE else: raise ValueError(f'Objective sense is not recognized: {obj.sense}') self._solver_model.ModelSense = sense def _update_quadratic_constraint(self, c: ConstraintData): if c in self._mutable_quadratic_helpers: if c in self._constraints_added_since_update: self._update_gurobi_model() helper = self._mutable_quadratic_helpers[c] gurobi_con = helper.gurobi_con new_gurobi_expr = helper.get_updated_expression() new_rhs = helper.get_updated_rhs() new_sense = gurobi_con.qcsense self._solver_model.remove(gurobi_con) new_con = self._solver_model.addQConstr(new_gurobi_expr, new_sense, new_rhs) self._pyomo_con_to_solver_con_map[c] = new_con assert helper.pyomo_con is c self._constraints_added_since_update.add(c) def _mutable_objective_update(self): if self._mutable_objective is not None: new_gurobi_expr = self._mutable_objective.get_updated_expression() if new_gurobi_expr is not None: if self._objective.sense == minimize: sense = gurobipy.GRB.MINIMIZE else: sense = gurobipy.GRB.MAXIMIZE # TODO: need a test for when part of the object is linear # and part of the objective is quadratic, but both # parts have mutable coefficients self._solver_model.setObjective(new_gurobi_expr, sense=sense) def _update_parameters(self, params: Mapping[ParamData, Reason]): self._invalidate_last_results() cons_to_update = OrderedSet() update_obj = False vars_to_update = ComponentSet() for p, reason in params.items(): if reason & Reason.added: continue if reason & Reason.removed: continue if reason & Reason.value: cons_to_update.update( self._change_detector.get_constraints_impacted_by_param(p) ) objs = self._change_detector.get_objectives_impacted_by_param(p) if objs: assert len(objs) == 1 assert objs[0] is self._objective update_obj = True vars_to_update.update( self._change_detector.get_variables_impacted_by_param(p) ) for c in cons_to_update: if c in self._mutable_helpers: for i in self._mutable_helpers[c]: i.update() self._update_quadratic_constraint(c) if update_obj: self._mutable_objective_update() for v in vars_to_update: vid = id(v) if (vid, 'lb') in self._mutable_bounds: self._mutable_bounds[(vid, 'lb')][1].update() if (vid, 'ub') in self._mutable_bounds: self._mutable_bounds[(vid, 'ub')][1].update() self._needs_updated = True def _invalidate_last_results(self): if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate()
[docs] 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)
[docs] 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 = OrderedSet() self._vars_added_since_update = ComponentSet() self._needs_updated = False
[docs] 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.ConstraintData 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. """ attr = attr.lower() if attr in self._disallow_linear_constraint_attr: raise ValueError( f'Linear constraint attr {attr} cannot be set with' ' the set_linear_constraint_attr method. Please use' ' the remove_constraint and add_constraint methods.' ) self._pyomo_con_to_solver_con_map[con].setAttr(attr, val) self._needs_updated = True
[docs] def set_var_attr(self, var, attr, val): """ Set the value of an attribute on a gurobi variable. Parameters ---------- var: pyomo.core.base.var.VarData 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. """ attr = attr.lower() if attr in self._disallow_set_var_attr: raise ValueError( f'Var attr {attr} cannot be set with' ' the set_var_attr method. Please use' ' the update_var method.' ) 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[var].setAttr(attr, val) self._needs_updated = True
[docs] def get_var_attr(self, var, attr): """ Get the value of an attribute on a gurobi var. Parameters ---------- var: pyomo.core.base.var.VarData 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_gurobi_model() return self._pyomo_var_to_solver_var_map[var].getAttr(attr)
[docs] 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.ConstraintData 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_gurobi_model() return self._pyomo_con_to_solver_con_map[con].getAttr(attr)
[docs] 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_gurobi_model() return self._pyomo_sos_to_solver_sos_map[con].getAttr(attr)
[docs] 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.ConstraintData 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_gurobi_model() return self._pyomo_con_to_solver_con_map[con].getAttr(attr)
[docs] 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)
[docs] 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 documentation 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
[docs] 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 .. math:: min 2*x + y s.t. y >= (x-2)**2 0 <= x <= 4 y >= 0 y integer as an MILP using extended cutting planes in callbacks. >>> from gurobipy import GRB # doctest:+SKIP >>> import pyomo.environ as pyo >>> from pyomo.core.expr.taylor_series import taylor_series_expansion >>> from pyomo.contrib import appsi >>> >>> m = pyo.ConcreteModel() >>> m.x = pyo.Var(bounds=(0, 4)) >>> m.y = pyo.Var(within=pyo.Integers, bounds=(0, None)) >>> m.obj = pyo.Objective(expr=2*m.x + m.y) >>> m.cons = pyo.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)) ... >>> _c = _add_cut(0) # start with 2 cuts at the bounds of x >>> _c = _add_cut(4) # this is an arbitrary choice >>> >>> opt = appsi.solvers.Gurobi() >>> opt.config.stream_solver = True >>> opt.set_instance(m) # doctest:+SKIP >>> opt.gurobi_options['PreCrush'] = 1 >>> opt.gurobi_options['LazyConstraints'] = 1 >>> >>> def my_callback(cb_m, cb_opt, cb_where): ... if cb_where == GRB.Callback.MIPSOL: ... cb_opt.cbGetSolution(variables=[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) >>> res = opt.solve(m) # doctest:+SKIP """ if func is not None: self._callback_func = func self._callback = self._intermediate_callback() else: self._callback = None self._callback_func = None
[docs] def cbCut(self, con): """ Add a cut within a callback. Parameters ---------- con: pyomo.core.base.constraint.ConstraintData 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-trivial constraint') repn = generate_standard_repn(con.body, quadratic=True, compute_values=True) gurobi_expr = self._get_expr_from_pyomo_repn(repn) 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(f'Lower bound of constraint {con} is not constant.') if con.has_ub(): if not is_fixed(con.upper): raise ValueError(f'Upper bound of constraint {con} is not constant.') if con.equality: self._solver_model.cbCut( lhs=gurobi_expr, sense=gurobipy.GRB.EQUAL, rhs=value(con.lower - repn.constant), ) elif con.has_lb() and (value(con.lower) > -float('inf')): self._solver_model.cbCut( lhs=gurobi_expr, sense=gurobipy.GRB.GREATER_EQUAL, rhs=value(con.lower - repn.constant), ) elif con.has_ub() and (value(con.upper) < float('inf')): self._solver_model.cbCut( lhs=gurobi_expr, sense=gurobipy.GRB.LESS_EQUAL, rhs=value(con.upper - repn.constant), ) else: raise ValueError( f'Constraint does not have a lower or an upper bound {con} \n' )
def cbGet(self, what): return self._solver_model.cbGet(what)
[docs] def cbGetNodeRel(self, variables): """ Parameters ---------- variables: Var or iterable of Var """ if not isinstance(variables, Iterable): variables = [variables] gurobi_vars = [self._pyomo_var_to_solver_var_map[i] for i in variables] var_values = self._solver_model.cbGetNodeRel(gurobi_vars) for i, v in enumerate(variables): v.set_value(var_values[i], skip_validation=True)
[docs] def cbGetSolution(self, variables): """ Parameters ---------- variables: iterable of vars """ if not isinstance(variables, Iterable): variables = [variables] gurobi_vars = [self._pyomo_var_to_solver_var_map[i] for i in variables] var_values = self._solver_model.cbGetSolution(gurobi_vars) for i, v in enumerate(variables): v.set_value(var_values[i], skip_validation=True)
[docs] def cbLazy(self, con): """ Parameters ---------- con: pyomo.core.base.constraint.ConstraintData 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-trivial constraint') repn = generate_standard_repn(con.body, quadratic=True, compute_values=True) gurobi_expr = self._get_expr_from_pyomo_repn(repn) 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(f'Lower bound of constraint {con} is not constant.') if con.has_ub(): if not is_fixed(con.upper): raise ValueError(f'Upper bound of constraint {con} is not constant.') if con.equality: self._solver_model.cbLazy( lhs=gurobi_expr, sense=gurobipy.GRB.EQUAL, rhs=value(con.lower - repn.constant), ) elif con.has_lb() and (value(con.lower) > -float('inf')): self._solver_model.cbLazy( lhs=gurobi_expr, sense=gurobipy.GRB.GREATER_EQUAL, rhs=value(con.lower - repn.constant), ) elif con.has_ub() and (value(con.upper) < float('inf')): self._solver_model.cbLazy( lhs=gurobi_expr, sense=gurobipy.GRB.LESS_EQUAL, rhs=value(con.upper - repn.constant), ) else: raise ValueError( f'Constraint does not have a lower or an upper bound {con} \n' )
def cbSetSolution(self, variables, solution): if not isinstance(variables, Iterable): variables = [variables] gurobi_vars = [self._pyomo_var_to_solver_var_map[i] for i in variables] self._solver_model.cbSetSolution(gurobi_vars, solution) def cbUseSolution(self): return self._solver_model.cbUseSolution() def reset(self): self._solver_model.reset()
[docs] def add_constraints(self, cons): self._change_detector.add_constraints(cons)
def add_sos_constraints(self, cons): self._change_detector.add_sos_constraints(cons)
[docs] def set_objective(self, obj: ObjectiveData): self._change_detector.add_objectives([obj])
[docs] def remove_constraints(self, cons): self._change_detector.remove_constraints(cons)
def remove_sos_constraints(self, cons): self._change_detector.remove_sos_constraints(cons)
[docs] def update_variables(self, variables): self._change_detector.update_variables(variables)
[docs] def update_parameters(self, params): self._change_detector.update_parameters(params)