1from collections.abc import Iterable
2import enum
3import logging
4import math
5from typing import List, Dict, Optional
6
7from pyomo.common.collections import ComponentSet, ComponentMap, OrderedSet
8from pyomo.common.dependencies import attempt_import
9from pyomo.common.errors import PyomoException
10from pyomo.common.tee import capture_output
11from pyomo.common.timing import HierarchicalTimer
12from pyomo.common.config import ConfigValue
13from pyomo.core.kernel.objective import minimize, maximize
14from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler
15from pyomo.core.base.var import Var, _GeneralVarData
16from pyomo.core.base.constraint import _GeneralConstraintData
17from pyomo.core.base.sos import _SOSConstraintData
18from pyomo.core.base.param import _ParamData
19from pyomo.core.expr.numvalue import (
20    value, is_constant, is_fixed, native_numeric_types,
21)
22from pyomo.repn import generate_standard_repn
23
24from pyomo.contrib.appsi.base import (
25    PersistentSolver, Results, TerminationCondition, MIPSolverConfig,
26    PersistentBase, PersistentSolutionLoader
27)
28
29logger = logging.getLogger(__name__)
30
31
32def _import_gurobipy():
33    try:
34        import gurobipy
35    except ImportError:
36        Gurobi._available = Gurobi.Availability.NotFound
37        raise
38    if gurobipy.GRB.VERSION_MAJOR < 7:
39        Gurobi._available = Gurobi.Availability.BadVersion
40        raise ImportError('The APPSI Gurobi interface requires gurobipy>=7.0.0')
41    return gurobipy
42
43
44gurobipy, gurobipy_available = attempt_import('gurobipy',
45                                              importer=_import_gurobipy)
46
47
48class DegreeError(PyomoException):
49    pass
50
51
52class GurobiConfig(MIPSolverConfig):
53    def __init__(self,
54                 description=None,
55                 doc=None,
56                 implicit=False,
57                 implicit_domain=None,
58                 visibility=0):
59        super(GurobiConfig, self).__init__(description=description,
60                                           doc=doc,
61                                           implicit=implicit,
62                                           implicit_domain=implicit_domain,
63                                           visibility=visibility)
64
65        self.declare('logfile', ConfigValue(domain=str))
66        self.logfile = ''
67
68
69class GurobiSolutionLoader(PersistentSolutionLoader):
70    def load_vars(self, vars_to_load=None, solution_number=0):
71        self._assert_solution_still_valid()
72        self._solver.load_vars(vars_to_load=vars_to_load, solution_number=solution_number)
73
74    def get_primals(self, vars_to_load=None, solution_number=0):
75        self._assert_solution_still_valid()
76        return self._solver.get_primals(vars_to_load=vars_to_load, solution_number=solution_number)
77
78
79class GurobiResults(Results):
80    def __init__(self, solver):
81        super(GurobiResults, self).__init__()
82        self.wallclock_time = None
83        self.solution_loader = GurobiSolutionLoader(solver=solver)
84
85
86class _MutableLinearCoefficient(object):
87    def __init__(self):
88        self.expr = None
89        self.var = None
90        self.con = None
91        self.gurobi_model = None
92
93    def update(self):
94        self.gurobi_model.chgCoeff(self.con, self.var, value(self.expr))
95
96
97class _MutableRangeConstant(object):
98    def __init__(self):
99        self.lhs_expr = None
100        self.rhs_expr = None
101        self.con = None
102        self.slack_name = None
103        self.gurobi_model = None
104
105    def update(self):
106        rhs_val = value(self.rhs_expr)
107        lhs_val = value(self.lhs_expr)
108        self.con.rhs = rhs_val
109        slack = self.gurobi_model.getVarByName(self.slack_name)
110        slack.ub = rhs_val - lhs_val
111
112
113class _MutableConstant(object):
114    def __init__(self):
115        self.expr = None
116        self.con = None
117
118    def update(self):
119        self.con.rhs = value(self.expr)
120
121
122class _MutableQuadraticConstraint(object):
123    def __init__(self, gurobi_model, gurobi_con, constant, linear_coefs, quadratic_coefs):
124        self.con = gurobi_con
125        self.gurobi_model = gurobi_model
126        self.constant = constant
127        self.last_constant_value = value(self.constant.expr)
128        self.linear_coefs = linear_coefs
129        self.last_linear_coef_values = [value(i.expr) for i in self.linear_coefs]
130        self.quadratic_coefs = quadratic_coefs
131        self.last_quadratic_coef_values = [value(i.expr) for i in self.quadratic_coefs]
132
133    def get_updated_expression(self):
134        gurobi_expr = self.gurobi_model.getQCRow(self.con)
135        for ndx, coef in enumerate(self.linear_coefs):
136            current_coef_value = value(coef.expr)
137            incremental_coef_value = current_coef_value - self.last_linear_coef_values[ndx]
138            gurobi_expr += incremental_coef_value * coef.var
139            self.last_linear_coef_values[ndx] = current_coef_value
140        for ndx, coef in enumerate(self.quadratic_coefs):
141            current_coef_value = value(coef.expr)
142            incremental_coef_value = current_coef_value - self.last_quadratic_coef_values[ndx]
143            gurobi_expr += incremental_coef_value * coef.var1 * coef.var2
144            self.last_quadratic_coef_values[ndx] = current_coef_value
145        return gurobi_expr
146
147    def get_updated_rhs(self):
148        return value(self.constant.expr)
149
150
151class _MutableObjective(object):
152    def __init__(self, gurobi_model, constant, linear_coefs, quadratic_coefs):
153        self.gurobi_model = gurobi_model
154        self.constant = constant
155        self.linear_coefs = linear_coefs
156        self.quadratic_coefs = quadratic_coefs
157        self.last_quadratic_coef_values = [value(i.expr) for i in self.quadratic_coefs]
158
159    def get_updated_expression(self):
160        for ndx, coef in enumerate(self.linear_coefs):
161            coef.var.obj = value(coef.expr)
162        self.gurobi_model.ObjCon = value(self.constant.expr)
163
164        gurobi_expr = None
165        for ndx, coef in enumerate(self.quadratic_coefs):
166            if value(coef.expr) != self.last_quadratic_coef_values[ndx]:
167                if gurobi_expr is None:
168                    self.gurobi_model.update()
169                    gurobi_expr = self.gurobi_model.getObjective()
170                current_coef_value = value(coef.expr)
171                incremental_coef_value = current_coef_value - self.last_quadratic_coef_values[ndx]
172                gurobi_expr += incremental_coef_value * coef.var1 * coef.var2
173                self.last_quadratic_coef_values[ndx] = current_coef_value
174        return gurobi_expr
175
176
177class _MutableQuadraticCoefficient(object):
178    def __init__(self):
179        self.expr = None
180        self.var1 = None
181        self.var2 = None
182
183
184class Gurobi(PersistentBase, PersistentSolver):
185    """
186    Interface to Gurobi
187    """
188    _available = None
189
190    def __init__(self):
191        super(Gurobi, self).__init__()
192        self._config = GurobiConfig()
193        self._solver_options = dict()
194        self._solver_model = None
195        self._symbol_map = SymbolMap()
196        self._labeler = None
197        self._pyomo_var_to_solver_var_map = dict()
198        self._pyomo_con_to_solver_con_map = dict()
199        self._solver_con_to_pyomo_con_map = dict()
200        self._pyomo_sos_to_solver_sos_map = dict()
201        self._range_constraints = OrderedSet()
202        self._mutable_helpers = dict()
203        self._mutable_quadratic_helpers = dict()
204        self._mutable_objective = None
205        self._needs_updated = True
206        self._callback = None
207        self._callback_func = None
208        self._constraints_added_since_update = OrderedSet()
209        self._vars_added_since_update = ComponentSet()
210        self._last_results_object: Optional[GurobiResults] = None
211
212    def available(self):
213        if self._available is None:
214            self._check_license()
215        return self._available
216
217    @classmethod
218    def _check_license(cls):
219        try:
220            # Gurobipy writes out license file information when creating
221            # the environment
222            with capture_output(capture_fd=True):
223                m = gurobipy.Model()
224        except ImportError:
225            # Triggered if this is the first time the deferred import of
226            # gurobipy is resolved. _import_gurobipy will have already
227            # set _available appropriately.
228            return
229        except gurobipy.GurobiError:
230            cls._available = Gurobi.Availability.BadLicense
231            return
232        m.setParam('OutputFlag', 0)
233        try:
234            # As of 3/2021, the limited-size Gurobi license was limited
235            # to 2000 variables.
236            m.addVars(range(2001))
237            m.setParam('OutputFlag', 0)
238            m.optimize()
239            cls._available = Gurobi.Availability.FullLicense
240        except gurobipy.GurobiError:
241            cls._available = Gurobi.Availability.LimitedLicense
242        finally:
243            m.dispose()
244
245    def version(self):
246        version = (gurobipy.GRB.VERSION_MAJOR,
247                   gurobipy.GRB.VERSION_MINOR,
248                   gurobipy.GRB.VERSION_TECHNICAL)
249        return version
250
251    @property
252    def config(self) -> GurobiConfig:
253        return self._config
254
255    @config.setter
256    def config(self, val: GurobiConfig):
257        self._config = val
258
259    @property
260    def gurobi_options(self):
261        """
262        Returns
263        -------
264        gurobi_options: dict
265            A dictionary mapping solver options to values for those options. These
266            are solver specific.
267        """
268        return self._solver_options
269
270    @gurobi_options.setter
271    def gurobi_options(self, val: Dict):
272        self._solver_options = val
273
274    @property
275    def symbol_map(self):
276        return self._symbol_map
277
278    def _solve(self, timer: HierarchicalTimer):
279        config = self.config
280        options = self.gurobi_options
281        if config.stream_solver:
282            self._solver_model.setParam('LogToConsole', 1)
283        else:
284            self._solver_model.setParam('LogToConsole', 0)
285        self._solver_model.setParam('LogFile', config.logfile)
286
287        if config.time_limit is not None:
288            self._solver_model.setParam('TimeLimit', config.time_limit)
289        if config.mip_gap is not None:
290            self._solver_model.setParam('MIPGap', config.mip_gap)
291
292        for key, option in options.items():
293            self._solver_model.setParam(key, option)
294        timer.start('optimize')
295        self._solver_model.optimize(self._callback)
296        timer.stop('optimize')
297        self._needs_updated = False
298        return self._postsolve(timer)
299
300    def solve(self, model, timer: HierarchicalTimer = None) -> Results:
301        avail = self.available()
302        if not avail:
303            raise PyomoException(f'Solver {self.__class__} is not available ({avail}).')
304        if self._last_results_object is not None:
305            self._last_results_object.solution_loader.invalidate()
306        if timer is None:
307            timer = HierarchicalTimer()
308        if model is not self._model:
309            timer.start('set_instance')
310            self.set_instance(model)
311            timer.stop('set_instance')
312        else:
313            timer.start('update')
314            self.update(timer=timer)
315            timer.stop('update')
316        res = self._solve(timer)
317        self._last_results_object = res
318        if self.config.report_timing:
319            logger.info('\n' + str(timer))
320        return res
321
322    def _add_variables(self, variables: List[_GeneralVarData]):
323        var_names = list()
324        vtypes = list()
325        lbs = list()
326        ubs = list()
327        for var in variables:
328            varname = self._symbol_map.getSymbol(var, self._labeler)
329            vtype = self._gurobi_vtype_from_var(var)
330            lb = value(var.lb)
331            ub = value(var.ub)
332            if lb is None:
333                lb = -gurobipy.GRB.INFINITY
334            if ub is None:
335                ub = gurobipy.GRB.INFINITY
336            if var.is_fixed():
337                lb = value(var.value)
338                ub = value(var.value)
339            var_names.append(varname)
340            vtypes.append(vtype)
341            lbs.append(lb)
342            ubs.append(ub)
343
344        gurobi_vars = self._solver_model.addVars(len(variables), lb=lbs, ub=ubs, vtype=vtypes, name=var_names)
345
346        for ndx, pyomo_var in enumerate(variables):
347            gurobi_var = gurobi_vars[ndx]
348            self._pyomo_var_to_solver_var_map[id(pyomo_var)] = gurobi_var
349        self._vars_added_since_update.update(variables)
350        self._needs_updated = True
351
352    def _add_params(self, params: List[_ParamData]):
353        pass
354
355    def set_instance(self, model):
356        if not self.available():
357            raise ImportError('Could not import gurobipy')
358        saved_config = self.config
359        saved_options = self.gurobi_options
360        saved_update_config = self.update_config
361        self.__init__()
362        self.config = saved_config
363        self.gurobi_options = saved_options
364        self.update_config = saved_update_config
365        self._model = model
366
367        if self.config.symbolic_solver_labels:
368            self._labeler = TextLabeler()
369        else:
370            self._labeler = NumericLabeler('x')
371
372        if model.name is not None:
373            self._solver_model = gurobipy.Model(model.name)
374        else:
375            self._solver_model = gurobipy.Model()
376
377        self.add_block(model)
378        if self._objective is None:
379            self.set_objective(None)
380
381    def _get_expr_from_pyomo_expr(self, expr):
382        mutable_linear_coefficients = list()
383        mutable_quadratic_coefficients = list()
384        repn = generate_standard_repn(expr, quadratic=True, compute_values=False)
385
386        degree = repn.polynomial_degree()
387        if (degree is None) or (degree > 2):
388            raise DegreeError('GurobiAuto does not support expressions of degree {0}.'.format(degree))
389
390        if len(repn.linear_vars) > 0:
391            linear_coef_vals = list()
392            for ndx, coef in enumerate(repn.linear_coefs):
393                if not is_constant(coef):
394                    mutable_linear_coefficient = _MutableLinearCoefficient()
395                    mutable_linear_coefficient.expr = coef
396                    mutable_linear_coefficient.var = self._pyomo_var_to_solver_var_map[id(repn.linear_vars[ndx])]
397                    mutable_linear_coefficients.append(mutable_linear_coefficient)
398                linear_coef_vals.append(value(coef))
399            new_expr = gurobipy.LinExpr(linear_coef_vals, [self._pyomo_var_to_solver_var_map[id(i)] for i in repn.linear_vars])
400        else:
401            new_expr = 0.0
402
403        for ndx, v in enumerate(repn.quadratic_vars):
404            x, y = v
405            gurobi_x = self._pyomo_var_to_solver_var_map[id(x)]
406            gurobi_y = self._pyomo_var_to_solver_var_map[id(y)]
407            coef = repn.quadratic_coefs[ndx]
408            if not is_constant(coef):
409                mutable_quadratic_coefficient = _MutableQuadraticCoefficient()
410                mutable_quadratic_coefficient.expr = coef
411                mutable_quadratic_coefficient.var1 = gurobi_x
412                mutable_quadratic_coefficient.var2 = gurobi_y
413                mutable_quadratic_coefficients.append(mutable_quadratic_coefficient)
414            coef_val = value(coef)
415            new_expr += coef_val * gurobi_x * gurobi_y
416
417        return new_expr, repn.constant, mutable_linear_coefficients, mutable_quadratic_coefficients
418
419    def _add_constraints(self, cons: List[_GeneralConstraintData]):
420        for con in cons:
421            conname = self._symbol_map.getSymbol(con, self._labeler)
422            (gurobi_expr,
423             repn_constant,
424             mutable_linear_coefficients,
425             mutable_quadratic_coefficients) = self._get_expr_from_pyomo_expr(con.body)
426
427            if (gurobi_expr.__class__ in {gurobipy.LinExpr, gurobipy.Var} or
428                    gurobi_expr.__class__ in native_numeric_types):
429                if con.equality:
430                    rhs_expr = con.lower - repn_constant
431                    rhs_val = value(rhs_expr)
432                    gurobipy_con = self._solver_model.addLConstr(gurobi_expr,
433                                                                 gurobipy.GRB.EQUAL,
434                                                                 rhs_val,
435                                                                 name=conname)
436                    if not is_constant(rhs_expr):
437                        mutable_constant = _MutableConstant()
438                        mutable_constant.expr = rhs_expr
439                        mutable_constant.con = gurobipy_con
440                        self._mutable_helpers[con] = [mutable_constant]
441                elif con.has_lb() and con.has_ub():
442                    lhs_expr = con.lower - repn_constant
443                    rhs_expr = con.upper - repn_constant
444                    lhs_val = value(lhs_expr)
445                    rhs_val = value(rhs_expr)
446                    gurobipy_con = self._solver_model.addRange(gurobi_expr, lhs_val, rhs_val, name=conname)
447                    self._range_constraints.add(con)
448                    if not is_constant(lhs_expr) or not is_constant(rhs_expr):
449                        mutable_range_constant = _MutableRangeConstant()
450                        mutable_range_constant.lhs_expr = lhs_expr
451                        mutable_range_constant.rhs_expr = rhs_expr
452                        mutable_range_constant.con = gurobipy_con
453                        mutable_range_constant.slack_name = 'Rg' + conname
454                        mutable_range_constant.gurobi_model = self._solver_model
455                        self._mutable_helpers[con] = [mutable_range_constant]
456                elif con.has_lb():
457                    rhs_expr = con.lower - repn_constant
458                    rhs_val = value(rhs_expr)
459                    gurobipy_con = self._solver_model.addLConstr(gurobi_expr, gurobipy.GRB.GREATER_EQUAL, rhs_val, name=conname)
460                    if not is_constant(rhs_expr):
461                        mutable_constant = _MutableConstant()
462                        mutable_constant.expr = rhs_expr
463                        mutable_constant.con = gurobipy_con
464                        self._mutable_helpers[con] = [mutable_constant]
465                elif con.has_ub():
466                    rhs_expr = con.upper - repn_constant
467                    rhs_val = value(rhs_expr)
468                    gurobipy_con = self._solver_model.addLConstr(gurobi_expr, gurobipy.GRB.LESS_EQUAL, rhs_val, name=conname)
469                    if not is_constant(rhs_expr):
470                        mutable_constant = _MutableConstant()
471                        mutable_constant.expr = rhs_expr
472                        mutable_constant.con = gurobipy_con
473                        self._mutable_helpers[con] = [mutable_constant]
474                else:
475                    raise ValueError("Constraint does not have a lower "
476                                     "or an upper bound: {0} \n".format(con))
477                for tmp in mutable_linear_coefficients:
478                    tmp.con = gurobipy_con
479                    tmp.gurobi_model = self._solver_model
480                if len(mutable_linear_coefficients) > 0:
481                    if con not in self._mutable_helpers:
482                        self._mutable_helpers[con] = mutable_linear_coefficients
483                    else:
484                        self._mutable_helpers[con].extend(mutable_linear_coefficients)
485            elif gurobi_expr.__class__ is gurobipy.QuadExpr:
486                if con.equality:
487                    rhs_expr = con.lower - repn_constant
488                    rhs_val = value(rhs_expr)
489                    gurobipy_con = self._solver_model.addQConstr(gurobi_expr, gurobipy.GRB.EQUAL, rhs_val, name=conname)
490                elif con.has_lb() and con.has_ub():
491                    raise NotImplementedError('Quadratic range constraints are not supported')
492                elif con.has_lb():
493                    rhs_expr = con.lower - repn_constant
494                    rhs_val = value(rhs_expr)
495                    gurobipy_con = self._solver_model.addQConstr(gurobi_expr, gurobipy.GRB.GREATER_EQUAL, rhs_val, name=conname)
496                elif con.has_ub():
497                    rhs_expr = con.upper - repn_constant
498                    rhs_val = value(rhs_expr)
499                    gurobipy_con = self._solver_model.addQConstr(gurobi_expr, gurobipy.GRB.LESS_EQUAL, rhs_val, name=conname)
500                else:
501                    raise ValueError("Constraint does not have a lower "
502                                     "or an upper bound: {0} \n".format(con))
503                if len(mutable_linear_coefficients) > 0 or len(mutable_quadratic_coefficients) > 0 or not is_constant(repn_constant):
504                    mutable_constant = _MutableConstant()
505                    mutable_constant.expr = rhs_expr
506                    mutable_quadratic_constraint = _MutableQuadraticConstraint(self._solver_model, gurobipy_con,
507                                                                               mutable_constant,
508                                                                               mutable_linear_coefficients,
509                                                                               mutable_quadratic_coefficients)
510                    self._mutable_quadratic_helpers[con] = mutable_quadratic_constraint
511            else:
512                raise ValueError('Unrecognized Gurobi expression type: ' + str(gurobi_expr.__class__))
513
514            self._pyomo_con_to_solver_con_map[con] = gurobipy_con
515            self._solver_con_to_pyomo_con_map[id(gurobipy_con)] = con
516        self._constraints_added_since_update.update(cons)
517        self._needs_updated = True
518
519    def _add_sos_constraints(self, cons: List[_SOSConstraintData]):
520        for con in cons:
521            conname = self._symbol_map.getSymbol(con, self._labeler)
522            level = con.level
523            if level == 1:
524                sos_type = gurobipy.GRB.SOS_TYPE1
525            elif level == 2:
526                sos_type = gurobipy.GRB.SOS_TYPE2
527            else:
528                raise ValueError("Solver does not support SOS "
529                                 "level {0} constraints".format(level))
530
531            gurobi_vars = []
532            weights = []
533
534            for v, w in con.get_items():
535                v_id = id(v)
536                gurobi_vars.append(self._pyomo_var_to_solver_var_map[v_id])
537                weights.append(w)
538
539            gurobipy_con = self._solver_model.addSOS(sos_type, gurobi_vars, weights)
540            self._pyomo_sos_to_solver_sos_map[con] = gurobipy_con
541        self._constraints_added_since_update.update(cons)
542        self._needs_updated = True
543
544    def _remove_constraints(self, cons: List[_GeneralConstraintData]):
545        for con in cons:
546            if con in self._constraints_added_since_update:
547                self._update_gurobi_model()
548            solver_con = self._pyomo_con_to_solver_con_map[con]
549            self._solver_model.remove(solver_con)
550            self._symbol_map.removeSymbol(con)
551            self._labeler.remove_obj(con)
552            del self._pyomo_con_to_solver_con_map[con]
553            del self._solver_con_to_pyomo_con_map[id(solver_con)]
554            self._range_constraints.discard(con)
555            self._mutable_helpers.pop(con, None)
556            self._mutable_quadratic_helpers.pop(con, None)
557        self._needs_updated = True
558
559    def _remove_sos_constraints(self, cons: List[_SOSConstraintData]):
560        for con in cons:
561            if con in self._constraints_added_since_update:
562                self._update_gurobi_model()
563            solver_sos_con = self._pyomo_sos_to_solver_sos_map[con]
564            self._solver_model.remove(solver_sos_con)
565            self._symbol_map.removeSymbol(con)
566            self._labeler.remove_obj(con)
567            del self._pyomo_sos_to_solver_sos_map[con]
568        self._needs_updated = True
569
570    def _remove_variables(self, variables: List[_GeneralVarData]):
571        for var in variables:
572            if var in self._vars_added_since_update:
573                self._update_gurobi_model()
574            solver_var = self._pyomo_var_to_solver_var_map[id(var)]
575            self._solver_model.remove(solver_var)
576            self._symbol_map.removeSymbol(var)
577            self._labeler.remove_obj(var)
578            del self._pyomo_var_to_solver_var_map[id(var)]
579        self._needs_updated = True
580
581    def _remove_params(self, params: List[_ParamData]):
582        pass
583
584    def _update_variables(self, variables: List[_GeneralVarData]):
585        for var in variables:
586            var_id = id(var)
587            if var_id not in self._pyomo_var_to_solver_var_map:
588                raise ValueError('The Var provided to update_var needs to be added first: {0}'.format(var))
589            gurobipy_var = self._pyomo_var_to_solver_var_map[var_id]
590            vtype = self._gurobi_vtype_from_var(var)
591            if var.is_fixed():
592                lb = var.value
593                ub = var.value
594            else:
595                lb = -gurobipy.GRB.INFINITY
596                ub = gurobipy.GRB.INFINITY
597                _lb = value(var.lb)
598                _ub = value(var.ub)
599                if _lb is not None:
600                    lb = _lb
601                if _ub is not None:
602                    ub = _ub
603            gurobipy_var.setAttr('lb', lb)
604            gurobipy_var.setAttr('ub', ub)
605            gurobipy_var.setAttr('vtype', vtype)
606        self._needs_updated = True
607
608    def update_params(self):
609        for con, helpers in self._mutable_helpers.items():
610            for helper in helpers:
611                helper.update()
612
613        for con, helper in self._mutable_quadratic_helpers.items():
614            if con in self._constraints_added_since_update:
615                self._update_gurobi_model()
616            gurobi_con = helper.con
617            new_gurobi_expr = helper.get_updated_expression()
618            new_rhs = helper.get_updated_rhs()
619            new_sense = gurobi_con.qcsense
620            pyomo_con = self._solver_con_to_pyomo_con_map[id(gurobi_con)]
621            name = self._symbol_map.getSymbol(pyomo_con, self._labeler)
622            self._solver_model.remove(gurobi_con)
623            new_con = self._solver_model.addQConstr(new_gurobi_expr, new_sense, new_rhs, name=name)
624            self._pyomo_con_to_solver_con_map[id(pyomo_con)] = new_con
625            del self._solver_con_to_pyomo_con_map[id(gurobi_con)]
626            self._solver_con_to_pyomo_con_map[id(new_con)] = pyomo_con
627            helper.con = new_con
628            self._constraints_added_since_update.add(con)
629
630        helper = self._mutable_objective
631        pyomo_obj = self._objective
632        new_gurobi_expr = helper.get_updated_expression()
633        if new_gurobi_expr is not None:
634            if pyomo_obj.sense == minimize:
635                sense = gurobipy.GRB.MINIMIZE
636            else:
637                sense = gurobipy.GRB.MAXIMIZE
638            self._solver_model.setObjective(new_gurobi_expr, sense=sense)
639
640    def _gurobi_vtype_from_var(self, var):
641        """
642        This function takes a pyomo variable and returns the appropriate gurobi variable type
643        :param var: pyomo.core.base.var.Var
644        :return: gurobipy.GRB.CONTINUOUS or gurobipy.GRB.BINARY or gurobipy.GRB.INTEGER
645        """
646        if var.is_binary():
647            vtype = gurobipy.GRB.BINARY
648        elif var.is_integer():
649            vtype = gurobipy.GRB.INTEGER
650        elif var.is_continuous():
651            vtype = gurobipy.GRB.CONTINUOUS
652        else:
653            raise ValueError('Variable domain type is not recognized for {0}'.format(var.domain))
654        return vtype
655
656    def _set_objective(self, obj):
657        if obj is None:
658            sense = gurobipy.GRB.MINIMIZE
659            gurobi_expr = 0
660            repn_constant = 0
661            mutable_linear_coefficients = list()
662            mutable_quadratic_coefficients = list()
663        else:
664            if obj.sense == minimize:
665                sense = gurobipy.GRB.MINIMIZE
666            elif obj.sense == maximize:
667                sense = gurobipy.GRB.MAXIMIZE
668            else:
669                raise ValueError('Objective sense is not recognized: {0}'.format(obj.sense))
670
671            (gurobi_expr,
672             repn_constant,
673             mutable_linear_coefficients,
674             mutable_quadratic_coefficients) = self._get_expr_from_pyomo_expr(obj.expr)
675
676        mutable_constant = _MutableConstant()
677        mutable_constant.expr = repn_constant
678        mutable_objective = _MutableObjective(self._solver_model,
679                                              mutable_constant,
680                                              mutable_linear_coefficients,
681                                              mutable_quadratic_coefficients)
682        self._mutable_objective = mutable_objective
683        self._solver_model.setObjective(gurobi_expr + value(repn_constant), sense=sense)
684        self._needs_updated = True
685
686    def _postsolve(self, timer: HierarchicalTimer):
687        config = self.config
688
689        gprob = self._solver_model
690        grb = gurobipy.GRB
691        status = gprob.Status
692
693        results = GurobiResults(self)
694        results.wallclock_time = gprob.Runtime
695
696        if status == grb.LOADED:  # problem is loaded, but no solution
697            results.termination_condition = TerminationCondition.unknown
698        elif status == grb.OPTIMAL:  # optimal
699            results.termination_condition = TerminationCondition.optimal
700        elif status == grb.INFEASIBLE:
701            results.termination_condition = TerminationCondition.infeasible
702        elif status == grb.INF_OR_UNBD:
703            results.termination_condition = TerminationCondition.infeasibleOrUnbounded
704        elif status == grb.UNBOUNDED:
705            results.termination_condition = TerminationCondition.unbounded
706        elif status == grb.CUTOFF:
707            results.termination_condition = TerminationCondition.objectiveLimit
708        elif status == grb.ITERATION_LIMIT:
709            results.termination_condition = TerminationCondition.maxIterations
710        elif status == grb.NODE_LIMIT:
711            results.termination_condition = TerminationCondition.maxIterations
712        elif status == grb.TIME_LIMIT:
713            results.termination_condition = TerminationCondition.maxTimeLimit
714        elif status == grb.SOLUTION_LIMIT:
715            results.termination_condition = TerminationCondition.unknown
716        elif status == grb.INTERRUPTED:
717            results.termination_condition = TerminationCondition.interrupted
718        elif status == grb.NUMERIC:
719            results.termination_condition = TerminationCondition.unknown
720        elif status == grb.SUBOPTIMAL:
721            results.termination_condition = TerminationCondition.unknown
722        elif status == grb.USER_OBJ_LIMIT:
723            results.termination_condition = TerminationCondition.objectiveLimit
724        else:
725            results.termination_condition = TerminationCondition.unknown
726
727        if self._objective is None:
728            results.best_feasible_objective = None
729            results.best_objective_bound = None
730        else:
731            try:
732                results.best_feasible_objective = gprob.ObjVal
733            except (gurobipy.GurobiError, AttributeError):
734                results.best_feasible_objective = None
735            try:
736                if gprob.NumBinVars + gprob.NumIntVars == 0:
737                    results.best_objective_bound = gprob.ObjVal
738                else:
739                    results.best_objective_bound = gprob.ObjBound
740            except (gurobipy.GurobiError, AttributeError):
741                if self._objective.sense == minimize:
742                    results.best_objective_bound = -math.inf
743                else:
744                    results.best_objective_bound = math.inf
745            if results.best_feasible_objective is not None and not math.isfinite(results.best_feasible_objective):
746                results.best_feasible_objective = None
747
748        timer.start('load solution')
749        if config.load_solution:
750            if gprob.SolCount > 0:
751                if results.termination_condition != TerminationCondition.optimal:
752                    logger.warning('Loading a feasible but suboptimal solution. '
753                                   'Please set load_solution=False and check '
754                                   'results.termination_condition and '
755                                   'resutls.found_feasible_solution() before loading a solution.')
756                self.load_vars()
757            else:
758                raise RuntimeError('A feasible solution was not found, so no solution can be loaded.'
759                                   'Please set opt.config.load_solution=False and check '
760                                   'results.termination_condition and '
761                                   'resutls.best_feasible_objective before loading a solution.')
762        timer.stop('load solution')
763
764        return results
765
766    def _load_suboptimal_mip_solution(self, vars_to_load, solution_number):
767        if self.get_model_attr('NumIntVars') == 0 and self.get_model_attr('NumBinVars') == 0:
768            raise ValueError('Cannot obtain suboptimal solutions for a continuous model')
769        var_map = self._pyomo_var_to_solver_var_map
770        ref_vars = self._referenced_variables
771        original_solution_number = self._solver_model.get_gurobi_param('SolutionNumber')
772        self._solver_model.set_gurobi_param('SolutionNumber', solution_number)
773        gurobi_vars_to_load = [var_map[id(pyomo_var)] for pyomo_var in vars_to_load]
774        vals = self._solver_model.getAttr("Xn", gurobi_vars_to_load)
775        res = ComponentMap()
776        for var, val in zip(vars_to_load, vals):
777            if ref_vars[id(var)] > 0:
778                res[var] = val
779        self._solver_model.set_gurobi_param('SolutionNumber', original_solution_number)
780        return res
781
782    def load_vars(self, vars_to_load=None, solution_number=0):
783        for v, val in self.get_primals(vars_to_load=vars_to_load, solution_number=solution_number).items():
784            v.value = val
785
786    def get_primals(self, vars_to_load=None, solution_number=0):
787        if self._needs_updated:
788            self._update_gurobi_model()  # this is needed to ensure that solutions cannot be loaded after the model has been changed
789        var_map = self._pyomo_var_to_solver_var_map
790        ref_vars = self._referenced_variables
791        if vars_to_load is None:
792            vars_to_load = self._pyomo_var_to_solver_var_map.keys()
793        else:
794            vars_to_load = [id(v) for v in vars_to_load]
795
796        if solution_number != 0:
797            return self._load_suboptimal_mip_solution(vars_to_load=vars_to_load, solution_number=solution_number)
798        else:
799            gurobi_vars_to_load = [var_map[pyomo_var_id] for pyomo_var_id in vars_to_load]
800            vals = self._solver_model.getAttr("X", gurobi_vars_to_load)
801
802            res = ComponentMap()
803            for var_id, val in zip(vars_to_load, vals):
804                if ref_vars[var_id] > 0:
805                    res[self._vars[var_id][0]] = val
806            return res
807
808    def get_reduced_costs(self, vars_to_load=None):
809        if self._needs_updated:
810            self._update_gurobi_model()
811
812        var_map = self._pyomo_var_to_solver_var_map
813        ref_vars = self._referenced_variables
814        res = ComponentMap()
815        if vars_to_load is None:
816            vars_to_load = self._pyomo_var_to_solver_var_map.keys()
817
818        gurobi_vars_to_load = [var_map[pyomo_var_id] for pyomo_var_id in vars_to_load]
819        vals = self._solver_model.getAttr("Rc", gurobi_vars_to_load)
820
821        for var_id, val in zip(vars_to_load, vals):
822            if ref_vars[var_id] > 0:
823                res[self._vars[var_id][0]] = val
824
825        return res
826
827    def get_duals(self, cons_to_load=None):
828        if self._needs_updated:
829            self._update_gurobi_model()
830
831        con_map = self._pyomo_con_to_solver_con_map
832        reverse_con_map = self._solver_con_to_pyomo_con_map
833        dual = dict()
834
835        if cons_to_load is None:
836            linear_cons_to_load = self._solver_model.getConstrs()
837            quadratic_cons_to_load = self._solver_model.getQConstrs()
838        else:
839            gurobi_cons_to_load = OrderedSet([con_map[pyomo_con] for pyomo_con in cons_to_load])
840            linear_cons_to_load = list(gurobi_cons_to_load.intersection(OrderedSet(self._solver_model.getConstrs())))
841            quadratic_cons_to_load = list(gurobi_cons_to_load.intersection(OrderedSet(self._solver_model.getQConstrs())))
842        linear_vals = self._solver_model.getAttr("Pi", linear_cons_to_load)
843        quadratic_vals = self._solver_model.getAttr("QCPi", quadratic_cons_to_load)
844
845        for gurobi_con, val in zip(linear_cons_to_load, linear_vals):
846            pyomo_con = reverse_con_map[id(gurobi_con)]
847            dual[pyomo_con] = val
848        for gurobi_con, val in zip(quadratic_cons_to_load, quadratic_vals):
849            pyomo_con = reverse_con_map[id(gurobi_con)]
850            dual[pyomo_con] = val
851
852        return dual
853
854    def get_slacks(self, cons_to_load=None):
855        if self._needs_updated:
856            self._update_gurobi_model()
857
858        con_map = self._pyomo_con_to_solver_con_map
859        reverse_con_map = self._solver_con_to_pyomo_con_map
860        slack = dict()
861
862        gurobi_range_con_vars = OrderedSet(self._solver_model.getVars()) - OrderedSet(self._pyomo_var_to_solver_var_map.values())
863
864        if cons_to_load is None:
865            linear_cons_to_load = self._solver_model.getConstrs()
866            quadratic_cons_to_load = self._solver_model.getQConstrs()
867        else:
868            gurobi_cons_to_load = OrderedSet([con_map[pyomo_con] for pyomo_con in cons_to_load])
869            linear_cons_to_load = list(gurobi_cons_to_load.intersection(OrderedSet(self._solver_model.getConstrs())))
870            quadratic_cons_to_load = list(gurobi_cons_to_load.intersection(OrderedSet(self._solver_model.getQConstrs())))
871        linear_vals = self._solver_model.getAttr("Slack", linear_cons_to_load)
872        quadratic_vals = self._solver_model.getAttr("QCSlack", quadratic_cons_to_load)
873
874        for gurobi_con, val in zip(linear_cons_to_load, linear_vals):
875            pyomo_con = reverse_con_map[id(gurobi_con)]
876            if pyomo_con in self._range_constraints:
877                lin_expr = self._solver_model.getRow(gurobi_con)
878                for i in reversed(range(lin_expr.size())):
879                    v = lin_expr.getVar(i)
880                    if v in gurobi_range_con_vars:
881                        Us_ = v.X
882                        Ls_ = v.UB - v.X
883                        if Us_ > Ls_:
884                            slack[pyomo_con] = Us_
885                        else:
886                            slack[pyomo_con] = -Ls_
887                        break
888            else:
889                slack[pyomo_con] = val
890        for gurobi_con, val in zip(quadratic_cons_to_load, quadratic_vals):
891            pyomo_con = reverse_con_map[id(gurobi_con)]
892            slack[pyomo_con] = val
893        return slack
894
895    def update(self, timer: HierarchicalTimer = None):
896        if self._needs_updated:
897            self._update_gurobi_model()
898        super(Gurobi, self).update(timer=timer)
899        self._update_gurobi_model()
900
901    def _update_gurobi_model(self):
902        self._solver_model.update()
903        self._constraints_added_since_update = OrderedSet()
904        self._vars_added_since_update = ComponentSet()
905        self._needs_updated = False
906
907    def get_model_attr(self, attr):
908        """
909        Get the value of an attribute on the Gurobi model.
910
911        Parameters
912        ----------
913        attr: str
914            The attribute to get. See Gurobi documentation for descriptions of the attributes.
915        """
916        if self._needs_updated:
917            self._update_gurobi_model()
918        return self._solver_model.getAttr(attr)
919
920    def write(self, filename):
921        """
922        Write the model to a file (e.g., and lp file).
923
924        Parameters
925        ----------
926        filename: str
927            Name of the file to which the model should be written.
928        """
929        self._solver_model.write(filename)
930        self._constraints_added_since_update = OrderedSet()
931        self._vars_added_since_update = ComponentSet()
932        self._needs_updated = False
933
934    def set_linear_constraint_attr(self, con, attr, val):
935        """
936        Set the value of an attribute on a gurobi linear constraint.
937
938        Parameters
939        ----------
940        con: pyomo.core.base.constraint._GeneralConstraintData
941            The pyomo constraint for which the corresponding gurobi constraint attribute
942            should be modified.
943        attr: str
944            The attribute to be modified. Options are:
945                CBasis
946                DStart
947                Lazy
948        val: any
949            See gurobi documentation for acceptable values.
950        """
951        if attr in {'Sense', 'RHS', 'ConstrName'}:
952            raise ValueError('Linear constraint attr {0} cannot be set with' +
953                             ' the set_linear_constraint_attr method. Please use' +
954                             ' the remove_constraint and add_constraint methods.'.format(attr))
955        self._pyomo_con_to_solver_con_map[con].setAttr(attr, val)
956        self._needs_updated = True
957
958    def set_var_attr(self, var, attr, val):
959        """
960        Set the value of an attribute on a gurobi variable.
961
962        Parameters
963        ----------
964        var: pyomo.core.base.var._GeneralVarData
965            The pyomo var for which the corresponding gurobi var attribute
966            should be modified.
967        attr: str
968            The attribute to be modified. Options are:
969                Start
970                VarHintVal
971                VarHintPri
972                BranchPriority
973                VBasis
974                PStart
975        val: any
976            See gurobi documentation for acceptable values.
977        """
978        if attr in {'LB', 'UB', 'VType', 'VarName'}:
979            raise ValueError('Var attr {0} cannot be set with' +
980                             ' the set_var_attr method. Please use' +
981                             ' the update_var method.'.format(attr))
982        if attr == 'Obj':
983            raise ValueError('Var attr Obj cannot be set with' +
984                             ' the set_var_attr method. Please use' +
985                             ' the set_objective method.')
986        self._pyomo_var_to_solver_var_map[id(var)].setAttr(attr, val)
987        self._needs_updated = True
988
989    def get_var_attr(self, var, attr):
990        """
991        Get the value of an attribute on a gurobi var.
992
993        Parameters
994        ----------
995        var: pyomo.core.base.var._GeneralVarData
996            The pyomo var for which the corresponding gurobi var attribute
997            should be retrieved.
998        attr: str
999            The attribute to get. See gurobi documentation
1000        """
1001        if self._needs_updated:
1002            self._update_gurobi_model()
1003        return self._pyomo_var_to_solver_var_map[id(var)].getAttr(attr)
1004
1005    def get_linear_constraint_attr(self, con, attr):
1006        """
1007        Get the value of an attribute on a gurobi linear constraint.
1008
1009        Parameters
1010        ----------
1011        con: pyomo.core.base.constraint._GeneralConstraintData
1012            The pyomo constraint for which the corresponding gurobi constraint attribute
1013            should be retrieved.
1014        attr: str
1015            The attribute to get. See the Gurobi documentation
1016        """
1017        if self._needs_updated:
1018            self._update_gurobi_model()
1019        return self._pyomo_con_to_solver_con_map[con].getAttr(attr)
1020
1021    def get_sos_attr(self, con, attr):
1022        """
1023        Get the value of an attribute on a gurobi sos constraint.
1024
1025        Parameters
1026        ----------
1027        con: pyomo.core.base.sos._SOSConstraintData
1028            The pyomo SOS constraint for which the corresponding gurobi SOS constraint attribute
1029            should be retrieved.
1030        attr: str
1031            The attribute to get. See the Gurobi documentation
1032        """
1033        if self._needs_updated:
1034            self._update_gurobi_model()
1035        return self._pyomo_sos_to_solver_sos_map[con].getAttr(attr)
1036
1037    def get_quadratic_constraint_attr(self, con, attr):
1038        """
1039        Get the value of an attribute on a gurobi quadratic constraint.
1040
1041        Parameters
1042        ----------
1043        con: pyomo.core.base.constraint._GeneralConstraintData
1044            The pyomo constraint for which the corresponding gurobi constraint attribute
1045            should be retrieved.
1046        attr: str
1047            The attribute to get. See the Gurobi documentation
1048        """
1049        if self._needs_updated:
1050            self._update_gurobi_model()
1051        return self._pyomo_con_to_solver_con_map[con].getAttr(attr)
1052
1053    def set_gurobi_param(self, param, val):
1054        """
1055        Set a gurobi parameter.
1056
1057        Parameters
1058        ----------
1059        param: str
1060            The gurobi parameter to set. Options include any gurobi parameter.
1061            Please see the Gurobi documentation for options.
1062        val: any
1063            The value to set the parameter to. See Gurobi documentation for possible values.
1064        """
1065        self._solver_model.setParam(param, val)
1066
1067    def get_gurobi_param_info(self, param):
1068        """
1069        Get information about a gurobi parameter.
1070
1071        Parameters
1072        ----------
1073        param: str
1074            The gurobi parameter to get info for. See Gurobi documenation for possible options.
1075
1076        Returns
1077        -------
1078        six-tuple containing the parameter name, type, value, minimum value, maximum value, and default value.
1079        """
1080        return self._solver_model.getParamInfo(param)
1081
1082    def _intermediate_callback(self):
1083        def f(gurobi_model, where):
1084            self._callback_func(self._model, self, where)
1085        return f
1086
1087    def set_callback(self, func=None):
1088        """
1089        Specify a callback for gurobi to use.
1090
1091        Parameters
1092        ----------
1093        func: function
1094            The function to call. The function should have three arguments. The first will be the pyomo model being
1095            solved. The second will be the GurobiPersistent instance. The third will be an enum member of
1096            gurobipy.GRB.Callback. This will indicate where in the branch and bound algorithm gurobi is at. For
1097            example, suppose we want to solve
1098
1099            min 2*x + y
1100            s.t.
1101                y >= (x-2)**2
1102                0 <= x <= 4
1103                y >= 0
1104                y integer
1105
1106            as an MILP using exteneded cutting planes in callbacks.
1107
1108                >>> from gurobipy import GRB # doctest:+SKIP
1109                >>> import pyomo.environ as pe
1110                >>> from pyomo.core.expr.taylor_series import taylor_series_expansion
1111                >>> from pyomo.contrib import appsi
1112                >>>
1113                >>> m = pe.ConcreteModel()
1114                >>> m.x = pe.Var(bounds=(0, 4))
1115                >>> m.y = pe.Var(within=pe.Integers, bounds=(0, None))
1116                >>> m.obj = pe.Objective(expr=2*m.x + m.y)
1117                >>> m.cons = pe.ConstraintList()  # for the cutting planes
1118                >>>
1119                >>> def _add_cut(xval):
1120                ...     # a function to generate the cut
1121                ...     m.x.value = xval
1122                ...     return m.cons.add(m.y >= taylor_series_expansion((m.x - 2)**2))
1123                >>>
1124                >>> _c = _add_cut(0)  # start with 2 cuts at the bounds of x
1125                >>> _c = _add_cut(4)  # this is an arbitrary choice
1126                >>>
1127                >>> opt = appsi.solvers.Gurobi()
1128                >>> opt.config.stream_solver = True
1129                >>> opt.set_instance(m) # doctest:+SKIP
1130                >>> opt.gurobi_options['PreCrush'] = 1
1131                >>> opt.gurobi_options['LazyConstraints'] = 1
1132                >>>
1133                >>> def my_callback(cb_m, cb_opt, cb_where):
1134                ...     if cb_where == GRB.Callback.MIPSOL:
1135                ...         cb_opt.cbGetSolution(vars=[m.x, m.y])
1136                ...         if m.y.value < (m.x.value - 2)**2 - 1e-6:
1137                ...             cb_opt.cbLazy(_add_cut(m.x.value))
1138                >>>
1139                >>> opt.set_callback(my_callback)
1140                >>> res = opt.solve(m) # doctest:+SKIP
1141
1142        """
1143        if func is not None:
1144            self._callback_func = func
1145            self._callback = self._intermediate_callback()
1146        else:
1147            self._callback = None
1148            self._callback_func = None
1149
1150    def cbCut(self, con):
1151        """
1152        Add a cut within a callback.
1153
1154        Parameters
1155        ----------
1156        con: pyomo.core.base.constraint._GeneralConstraintData
1157            The cut to add
1158        """
1159        if not con.active:
1160            raise ValueError('cbCut expected an active constraint.')
1161
1162        if is_fixed(con.body):
1163            raise ValueError('cbCut expected a non-trival constraint')
1164
1165        (gurobi_expr,
1166         repn_constant,
1167         mutable_linear_coefficients,
1168         mutable_quadratic_coefficients) = self._get_expr_from_pyomo_expr(con.body)
1169
1170        if con.has_lb():
1171            if con.has_ub():
1172                raise ValueError('Range constraints are not supported in cbCut.')
1173            if not is_fixed(con.lower):
1174                raise ValueError('Lower bound of constraint {0} is not constant.'.format(con))
1175        if con.has_ub():
1176            if not is_fixed(con.upper):
1177                raise ValueError('Upper bound of constraint {0} is not constant.'.format(con))
1178
1179        if con.equality:
1180            self._solver_model.cbCut(lhs=gurobi_expr, sense=gurobipy.GRB.EQUAL,
1181                                     rhs=value(con.lower - repn_constant))
1182        elif con.has_lb() and (value(con.lower) > -float('inf')):
1183            self._solver_model.cbCut(lhs=gurobi_expr, sense=gurobipy.GRB.GREATER_EQUAL,
1184                                     rhs=value(con.lower - repn_constant))
1185        elif con.has_ub() and (value(con.upper) < float('inf')):
1186            self._solver_model.cbCut(lhs=gurobi_expr, sense=gurobipy.GRB.LESS_EQUAL,
1187                                     rhs=value(con.upper - repn_constant))
1188        else:
1189            raise ValueError('Constraint does not have a lower or an upper bound {0} \n'.format(con))
1190
1191    def cbGet(self, what):
1192        return self._solver_model.cbGet(what)
1193
1194    def cbGetNodeRel(self, vars):
1195        """
1196        Parameters
1197        ----------
1198        vars: Var or iterable of Var
1199        """
1200        if not isinstance(vars, Iterable):
1201            vars = [vars]
1202        gurobi_vars = [self._pyomo_var_to_solver_var_map[id(i)] for i in vars]
1203        var_values = self._solver_model.cbGetNodeRel(gurobi_vars)
1204        for i, v in enumerate(vars):
1205            v.value = var_values[i]
1206
1207    def cbGetSolution(self, vars):
1208        """
1209        Parameters
1210        ----------
1211        vars: iterable of vars
1212        """
1213        if not isinstance(vars, Iterable):
1214            vars = [vars]
1215        gurobi_vars = [self._pyomo_var_to_solver_var_map[id(i)] for i in vars]
1216        var_values = self._solver_model.cbGetSolution(gurobi_vars)
1217        for i, v in enumerate(vars):
1218            v.value = var_values[i]
1219
1220    def cbLazy(self, con):
1221        """
1222        Parameters
1223        ----------
1224        con: pyomo.core.base.constraint._GeneralConstraintData
1225            The lazy constraint to add
1226        """
1227        if not con.active:
1228            raise ValueError('cbLazy expected an active constraint.')
1229
1230        if is_fixed(con.body):
1231            raise ValueError('cbLazy expected a non-trival constraint')
1232
1233        (gurobi_expr,
1234         repn_constant,
1235         mutable_linear_coefficients,
1236         mutable_quadratic_coefficients) = self._get_expr_from_pyomo_expr(con.body)
1237
1238        if con.has_lb():
1239            if con.has_ub():
1240                raise ValueError('Range constraints are not supported in cbLazy.')
1241            if not is_fixed(con.lower):
1242                raise ValueError('Lower bound of constraint {0} is not constant.'.format(con))
1243        if con.has_ub():
1244            if not is_fixed(con.upper):
1245                raise ValueError('Upper bound of constraint {0} is not constant.'.format(con))
1246
1247        if con.equality:
1248            self._solver_model.cbLazy(lhs=gurobi_expr, sense=gurobipy.GRB.EQUAL,
1249                                      rhs=value(con.lower - repn_constant))
1250        elif con.has_lb() and (value(con.lower) > -float('inf')):
1251            self._solver_model.cbLazy(lhs=gurobi_expr, sense=gurobipy.GRB.GREATER_EQUAL,
1252                                      rhs=value(con.lower - repn_constant))
1253        elif con.has_ub() and (value(con.upper) < float('inf')):
1254            self._solver_model.cbLazy(lhs=gurobi_expr, sense=gurobipy.GRB.LESS_EQUAL,
1255                                      rhs=value(con.upper - repn_constant))
1256        else:
1257            raise ValueError('Constraint does not have a lower or an upper bound {0} \n'.format(con))
1258
1259    def cbSetSolution(self, vars, solution):
1260        if not isinstance(vars, Iterable):
1261            vars = [vars]
1262        gurobi_vars = [self._pyomo_var_to_solver_var_map[id(i)] for i in vars]
1263        self._solver_model.cbSetSolution(gurobi_vars, solution)
1264
1265    def cbUseSolution(self):
1266        return self._solver_model.cbUseSolution()
1267
1268    def reset(self):
1269        self._solver_model.reset()
1270
1271