1#  ___________________________________________________________________________
2#
3#  Pyomo: Python Optimization Modeling Objects
4#  Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC
5#  Under the terms of Contract DE-NA0003525 with National Technology and
6#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
7#  rights in this software.
8#  This software is distributed under the 3-clause BSD License.
9#  ___________________________________________________________________________
10
11from collections.abc import Iterable
12
13from pyomo.solvers.plugins.solvers.gurobi_direct import GurobiDirect, gurobipy
14from pyomo.solvers.plugins.solvers.persistent_solver import PersistentSolver
15from pyomo.core.expr.numvalue import value, is_fixed
16from pyomo.opt.base import SolverFactory
17
18
19@SolverFactory.register('gurobi_persistent', doc='Persistent python interface to Gurobi')
20class GurobiPersistent(PersistentSolver, GurobiDirect):
21    """
22    A class that provides a persistent interface to Gurobi. Direct solver interfaces do not use any file io.
23    Rather, they interface directly with the python bindings for the specific solver. Persistent solver interfaces
24    are similar except that they "remember" their model. Thus, persistent solver interfaces allow incremental changes
25    to the solver model (e.g., the gurobi python model or the cplex python model). Note that users are responsible
26    for notifying the persistent solver interfaces when changes are made to the corresponding pyomo model.
27
28    Keyword Arguments
29    -----------------
30    model: ConcreteModel
31        Passing a model to the constructor is equivalent to calling the set_instance mehtod.
32    type: str
33        String indicating the class type of the solver instance.
34    name: str
35        String representing either the class type of the solver instance or an assigned name.
36    doc: str
37        Documentation for the solver
38    options: dict
39        Dictionary of solver options
40    """
41
42    def __init__(self, **kwds):
43        kwds['type'] = 'gurobi_persistent'
44        GurobiDirect.__init__(self, **kwds)
45
46        self._pyomo_model = kwds.pop('model', None)
47        if self._pyomo_model is not None:
48            self.set_instance(self._pyomo_model, **kwds)
49
50    def _remove_constraint(self, solver_con):
51        if isinstance(solver_con, gurobipy.Constr):
52            if self._solver_model.getAttr('NumConstrs') == 0:
53                self._update()
54            else:
55                name = self._symbol_map.getSymbol(self._solver_con_to_pyomo_con_map[solver_con])
56                if self._solver_model.getConstrByName(name) is None:
57                    self._update()
58        elif isinstance(solver_con, gurobipy.QConstr):
59            if self._solver_model.getAttr('NumQConstrs') == 0:
60                self._update()
61            else:
62                try:
63                    qc_row = self._solver_model.getQCRow(solver_con)
64                except gurobipy.GurobiError:
65                    self._update()
66        elif isinstance(solver_con, gurobipy.SOS):
67            if self._solver_model.getAttr('NumSOS') == 0:
68                self._update()
69            else:
70                try:
71                    sos = self._solver_model.getSOS(solver_con)
72                except gurobipy.GurobiError:
73                    self._update()
74        else:
75            raise ValueError('Unrecognized type for gurobi constraint: {0}'.format(type(solver_con)))
76        self._solver_model.remove(solver_con)
77        self._needs_updated = True
78
79    def _remove_sos_constraint(self, solver_sos_con):
80        self._remove_constraint(solver_sos_con)
81        self._needs_updated = True
82
83    def _remove_var(self, solver_var):
84        if self._solver_model.getAttr('NumVars') == 0:
85            self._update()
86        else:
87            name = self._symbol_map.getSymbol(self._solver_var_to_pyomo_var_map[solver_var])
88            if self._solver_model.getVarByName(name) is None:
89                self._update()
90        self._solver_model.remove(solver_var)
91        self._needs_updated = True
92
93    def _warm_start(self):
94        GurobiDirect._warm_start(self)
95
96    def update_var(self, var):
97        """Update a single variable in the solver's model.
98
99        This will update bounds, fix/unfix the variable as needed, and
100        update the variable type.
101
102        Parameters
103        ----------
104        var: Var (scalar Var or single _VarData)
105
106        """
107        # see PR #366 for discussion about handling indexed
108        # objects and keeping compatibility with the
109        # pyomo.kernel objects
110        #if var.is_indexed():
111        #    for child_var in var.values():
112        #        self.update_var(child_var)
113        #    return
114        if var not in self._pyomo_var_to_solver_var_map:
115            raise ValueError('The Var provided to update_var needs to be added first: {0}'.format(var))
116        gurobipy_var = self._pyomo_var_to_solver_var_map[var]
117        vtype = self._gurobi_vtype_from_var(var)
118        lb, ub = self._gurobi_lb_ub_from_var(var)
119
120        gurobipy_var.setAttr('lb', lb)
121        gurobipy_var.setAttr('ub', ub)
122        gurobipy_var.setAttr('vtype', vtype)
123        self._needs_updated = True
124
125    def write(self, filename):
126        """
127        Write the model to a file (e.g., and lp file).
128
129        Parameters
130        ----------
131        filename: str
132            Name of the file to which the model should be written.
133        """
134        self._solver_model.write(filename)
135        self._needs_updated = False
136
137    def update(self):
138        self._update()
139
140    def set_linear_constraint_attr(self, con, attr, val):
141        """
142        Set the value of an attribute on a gurobi linear constraint.
143
144        Parameters
145        ----------
146        con: pyomo.core.base.constraint._GeneralConstraintData
147            The pyomo constraint for which the corresponding gurobi constraint attribute
148            should be modified.
149        attr: str
150            The attribute to be modified. Options are:
151
152                CBasis
153                DStart
154                Lazy
155
156        val: any
157            See gurobi documentation for acceptable values.
158        """
159        if attr in {'Sense', 'RHS', 'ConstrName'}:
160            raise ValueError('Linear constraint attr {0} cannot be set with' +
161                             ' the set_linear_constraint_attr method. Please use' +
162                             ' the remove_constraint and add_constraint methods.'.format(attr))
163        if self._version_major < 7:
164            if (self._solver_model.getAttr('NumConstrs') == 0 or
165                    self._solver_model.getConstrByName(self._symbol_map.getSymbol(con)) is None):
166                self._solver_model.update()
167        self._pyomo_con_to_solver_con_map[con].setAttr(attr, val)
168        self._needs_updated = True
169
170    def set_var_attr(self, var, attr, val):
171        """
172        Set the value of an attribute on a gurobi variable.
173
174        Parameters
175        ----------
176        con: pyomo.core.base.var._GeneralVarData
177            The pyomo var for which the corresponding gurobi var attribute
178            should be modified.
179        attr: str
180            The attribute to be modified. Options are:
181
182                Start
183                VarHintVal
184                VarHintPri
185                BranchPriority
186                VBasis
187                PStart
188
189        val: any
190            See gurobi documentation for acceptable values.
191        """
192        if attr in {'LB', 'UB', 'VType', 'VarName'}:
193            raise ValueError('Var attr {0} cannot be set with' +
194                             ' the set_var_attr method. Please use' +
195                             ' the update_var method.'.format(attr))
196        if attr == 'Obj':
197            raise ValueError('Var attr Obj cannot be set with' +
198                             ' the set_var_attr method. Please use' +
199                             ' the set_objective method.')
200        if self._version_major < 7:
201            if (self._solver_model.getAttr('NumVars') == 0 or
202                    self._solver_model.getVarByName(self._symbol_map.getSymbol(var)) is None):
203                self._solver_model.update()
204        self._pyomo_var_to_solver_var_map[var].setAttr(attr, val)
205        self._needs_updated = True
206
207    def get_model_attr(self, attr):
208        """Get the value of an attribute on the Gurobi model.
209
210        Parameters
211        ----------
212        attr: str
213            The attribute to get. See Gurobi documentation for
214            descriptions of the attributes.
215
216            Options are:
217
218                NumVars
219                NumConstrs
220                NumSOS
221                NumQConstrs
222                NumgGenConstrs
223                NumNZs
224                DNumNZs
225                NumQNZs
226                NumQCNZs
227                NumIntVars
228                NumBinVars
229                NumPWLObjVars
230                ModelName
231                ModelSense
232                ObjCon
233                ObjVal
234                ObjBound
235                ObjBoundC
236                PoolObjBound
237                PoolObjVal
238                MIPGap
239                Runtime
240                Status
241                SolCount
242                IterCount
243                BarIterCount
244                NodeCount
245                IsMIP
246                IsQP
247                IsQCP
248                IsMultiObj
249                IISMinimal
250                MaxCoeff
251                MinCoeff
252                MaxBound
253                MinBound
254                MaxObjCoeff
255                MinObjCoeff
256                MaxRHS
257                MinRHS
258                MaxQCCoeff
259                MinQCCoeff
260                MaxQCLCoeff
261                MinQCLCoeff
262                MaxQCRHS
263                MinQCRHS
264                MaxQObjCoeff
265                MinQObjCoeff
266                Kappa
267                KappaExact
268                FarkasProof
269                TuneResultCount
270                LicenseExpiration
271                BoundVio
272                BoundSVio
273                BoundVioIndex
274                BoundSVioIndex
275                BoundVioSum
276                BoundSVioSum
277                ConstrVio
278                ConstrSVio
279                ConstrVioIndex
280                ConstrSVioIndex
281                ConstrVioSum
282                ConstrSVioSum
283                ConstrResidual
284                ConstrSResidual
285                ConstrResidualIndex
286                ConstrSResidualIndex
287                ConstrResidualSum
288                ConstrSResidualSum
289                DualVio
290                DualSVio
291                DualVioIndex
292                DualSVioIndex
293                DualVioSum
294                DualSVioSum
295                DualResidual
296                DualSResidual
297                DualResidualIndex
298                DualSResidualIndex
299                DualResidualSum
300                DualSResidualSum
301                ComplVio
302                ComplVioIndex
303                ComplVioSum
304                IntVio
305                IntVioIndex
306                IntVioSum
307
308        """
309        if self._needs_updated:
310            self._update()
311        return self._solver_model.getAttr(attr)
312
313    def get_var_attr(self, var, attr):
314        """
315        Get the value of an attribute on a gurobi var.
316
317        Parameters
318        ----------
319        var: pyomo.core.base.var._GeneralVarData
320            The pyomo var for which the corresponding gurobi var attribute
321            should be retrieved.
322        attr: str
323            The attribute to get. Options are:
324
325                LB
326                UB
327                Obj
328                VType
329                VarName
330                X
331                Xn
332                RC
333                BarX
334                Start
335                VarHintVal
336                VarHintPri
337                BranchPriority
338                VBasis
339                PStart
340                IISLB
341                IISUB
342                PWLObjCvx
343                SAObjLow
344                SAObjUp
345                SALBLow
346                SALBUp
347                SAUBLow
348                SAUBUp
349                UnbdRay
350        """
351        if self._needs_updated:
352            self._update()
353        return self._pyomo_var_to_solver_var_map[var].getAttr(attr)
354
355    def get_linear_constraint_attr(self, con, attr):
356        """
357        Get the value of an attribute on a gurobi linear constraint.
358
359        Parameters
360        ----------
361        con: pyomo.core.base.constraint._GeneralConstraintData
362            The pyomo constraint for which the corresponding gurobi constraint attribute
363            should be retrieved.
364        attr: str
365            The attribute to get. Options are:
366
367                Sense
368                RHS
369                ConstrName
370                Pi
371                Slack
372                CBasis
373                DStart
374                Lazy
375                IISConstr
376                SARHSLow
377                SARHSUp
378                FarkasDual
379        """
380        if self._needs_updated:
381            self._update()
382        return self._pyomo_con_to_solver_con_map[con].getAttr(attr)
383
384    def get_sos_attr(self, con, attr):
385        """
386        Get the value of an attribute on a gurobi sos constraint.
387
388        Parameters
389        ----------
390        con: pyomo.core.base.sos._SOSConstraintData
391            The pyomo SOS constraint for which the corresponding gurobi SOS constraint attribute
392            should be retrieved.
393        attr: str
394            The attribute to get. Options are:
395
396                IISSOS
397        """
398        if self._needs_updated:
399            self._update()
400        return self._pyomo_con_to_solver_con_map[con].getAttr(attr)
401
402    def get_quadratic_constraint_attr(self, con, attr):
403        """
404        Get the value of an attribute on a gurobi quadratic constraint.
405
406        Parameters
407        ----------
408        con: pyomo.core.base.constraint._GeneralConstraintData
409            The pyomo constraint for which the corresponding gurobi constraint attribute
410            should be retrieved.
411        attr: str
412            The attribute to get. Options are:
413
414                QCSense
415                QCRHS
416                QCName
417                QCPi
418                QCSlack
419                IISQConstr
420        """
421        if self._needs_updated:
422            self._update()
423        return self._pyomo_con_to_solver_con_map[con].getAttr(attr)
424
425    def set_gurobi_param(self, param, val):
426        """
427        Set a gurobi parameter.
428
429        Parameters
430        ----------
431        param: str
432            The gurobi parameter to set. Options include any gurobi parameter.
433            Please see the Gurobi documentation for options.
434        val: any
435            The value to set the parameter to. See Gurobi documentation for possible values.
436        """
437        self._solver_model.setParam(param, val)
438
439    def get_gurobi_param_info(self, param):
440        """
441        Get information about a gurobi parameter.
442
443        Parameters
444        ----------
445        param: str
446            The gurobi parameter to get info for. See Gurobi documenation for possible options.
447
448        Returns
449        -------
450        six-tuple containing the parameter name, type, value, minimum value, maximum value, and default value.
451        """
452        return self._solver_model.getParamInfo(param)
453
454    def _intermediate_callback(self):
455        def f(gurobi_model, where):
456            self._callback_func(self._pyomo_model, self, where)
457        return f
458
459    def set_callback(self, func=None):
460        r"""Specify a callback for gurobi to use.
461
462        Parameters
463        ----------
464        func: function
465            The function to call. The function should have three
466            arguments. The first will be the pyomo model being
467            solved. The second will be the GurobiPersistent
468            instance. The third will be an enum member of
469            gurobipy.GRB.Callback. This will indicate where in the
470            branch and bound algorithm gurobi is at. For example,
471            suppose we want to solve
472
473            .. math::
474               :nowrap:
475
476               \begin{array}{ll}
477               \min          & 2x + y           \\
478               \mathrm{s.t.} & y \geq (x-2)^2   \\
479                             & 0 \leq x \leq 4  \\
480                             & y \geq 0         \\
481                             & y \in \mathbb{Z}
482               \end{array}
483
484            as an MILP using exteneded cutting planes in callbacks.
485
486            .. testcode::
487               :skipif: not gurobipy_available
488
489               from gurobipy import GRB
490               import pyomo.environ as pe
491               from pyomo.core.expr.taylor_series import taylor_series_expansion
492
493               m = pe.ConcreteModel()
494               m.x = pe.Var(bounds=(0, 4))
495               m.y = pe.Var(within=pe.Integers, bounds=(0, None))
496               m.obj = pe.Objective(expr=2*m.x + m.y)
497               m.cons = pe.ConstraintList()  # for the cutting planes
498
499               def _add_cut(xval):
500                   # a function to generate the cut
501                   m.x.value = xval
502                   return m.cons.add(m.y >= taylor_series_expansion((m.x - 2)**2))
503
504               _add_cut(0)  # start with 2 cuts at the bounds of x
505               _add_cut(4)  # this is an arbitrary choice
506
507               opt = pe.SolverFactory('gurobi_persistent')
508               opt.set_instance(m)
509               opt.set_gurobi_param('PreCrush', 1)
510               opt.set_gurobi_param('LazyConstraints', 1)
511
512               def my_callback(cb_m, cb_opt, cb_where):
513                   if cb_where == GRB.Callback.MIPSOL:
514                       cb_opt.cbGetSolution(vars=[m.x, m.y])
515                       if m.y.value < (m.x.value - 2)**2 - 1e-6:
516                           cb_opt.cbLazy(_add_cut(m.x.value))
517
518               opt.set_callback(my_callback)
519               opt.solve()
520
521            .. testoutput::
522               :hide:
523
524               ...
525
526            .. doctest::
527               :skipif: not gurobipy_available
528
529               >>> assert abs(m.x.value - 1) <= 1e-6
530               >>> assert abs(m.y.value - 1) <= 1e-6
531        """
532        if func is not None:
533            self._callback_func = func
534            self._callback = self._intermediate_callback()
535        else:
536            self._callback = None
537            self._callback_func = None
538
539    def cbCut(self, con):
540        """
541        Add a cut within a callback.
542
543        Parameters
544        ----------
545        con: pyomo.core.base.constraint._GeneralConstraintData
546            The cut to add
547        """
548        if not con.active:
549            raise ValueError('cbCut expected an active constraint.')
550
551        if is_fixed(con.body):
552            raise ValueError('cbCut expected a non-trival constraint')
553
554        gurobi_expr, referenced_vars = self._get_expr_from_pyomo_expr(con.body, self._max_constraint_degree)
555
556        if con.has_lb():
557            if con.has_ub():
558                raise ValueError('Range constraints are not supported in cbCut.')
559            if not is_fixed(con.lower):
560                raise ValueError('Lower bound of constraint {0} is not constant.'.format(con))
561        if con.has_ub():
562            if not is_fixed(con.upper):
563                raise ValueError('Upper bound of constraint {0} is not constant.'.format(con))
564
565        if con.equality:
566            self._solver_model.cbCut(lhs=gurobi_expr, sense=gurobipy.GRB.EQUAL,
567                                     rhs=value(con.lower))
568        elif con.has_lb() and (value(con.lower) > -float('inf')):
569            self._solver_model.cbCut(lhs=gurobi_expr, sense=gurobipy.GRB.GREATER_EQUAL,
570                                     rhs=value(con.lower))
571        elif con.has_ub() and (value(con.upper) < float('inf')):
572            self._solver_model.cbCut(lhs=gurobi_expr, sense=gurobipy.GRB.LESS_EQUAL,
573                                     rhs=value(con.upper))
574        else:
575            raise ValueError('Constraint does not have a lower or an upper bound {0} \n'.format(con))
576
577    def cbGet(self, what):
578        return self._solver_model.cbGet(what)
579
580    def cbGetNodeRel(self, vars):
581        """
582        Parameters
583        ----------
584        vars: Var or iterable of Var
585        """
586        if not isinstance(vars, Iterable):
587            vars = [vars]
588        gurobi_vars = [self._pyomo_var_to_solver_var_map[i] for i in vars]
589        var_values = self._solver_model.cbGetNodeRel(gurobi_vars)
590        for i, v in enumerate(vars):
591            v.value = var_values[i]
592
593    def cbGetSolution(self, vars):
594        """
595        Parameters
596        ----------
597        vars: iterable of vars
598        """
599        if not isinstance(vars, Iterable):
600            vars = [vars]
601        gurobi_vars = [self._pyomo_var_to_solver_var_map[i] for i in vars]
602        var_values = self._solver_model.cbGetSolution(gurobi_vars)
603        for i, v in enumerate(vars):
604            v.value = var_values[i]
605
606    def cbLazy(self, con):
607        """
608        Parameters
609        ----------
610        con: pyomo.core.base.constraint._GeneralConstraintData
611            The lazy constraint to add
612        """
613        if not con.active:
614            raise ValueError('cbLazy expected an active constraint.')
615
616        if is_fixed(con.body):
617            raise ValueError('cbLazy expected a non-trival constraint')
618
619        gurobi_expr, referenced_vars = self._get_expr_from_pyomo_expr(con.body, self._max_constraint_degree)
620
621        if con.has_lb():
622            if con.has_ub():
623                raise ValueError('Range constraints are not supported in cbLazy.')
624            if not is_fixed(con.lower):
625                raise ValueError('Lower bound of constraint {0} is not constant.'.format(con))
626        if con.has_ub():
627            if not is_fixed(con.upper):
628                raise ValueError('Upper bound of constraint {0} is not constant.'.format(con))
629
630        if con.equality:
631            self._solver_model.cbLazy(lhs=gurobi_expr, sense=gurobipy.GRB.EQUAL,
632                                      rhs=value(con.lower))
633        elif con.has_lb() and (value(con.lower) > -float('inf')):
634            self._solver_model.cbLazy(lhs=gurobi_expr, sense=gurobipy.GRB.GREATER_EQUAL,
635                                      rhs=value(con.lower))
636        elif con.has_ub() and (value(con.upper) < float('inf')):
637            self._solver_model.cbLazy(lhs=gurobi_expr, sense=gurobipy.GRB.LESS_EQUAL,
638                                      rhs=value(con.upper))
639        else:
640            raise ValueError('Constraint does not have a lower or an upper bound {0} \n'.format(con))
641
642    def cbSetSolution(self, vars, solution):
643        if not isinstance(vars, Iterable):
644            vars = [vars]
645        gurobi_vars = [self._pyomo_var_to_solver_var_map[i] for i in vars]
646        self._solver_model.cbSetSolution(gurobi_vars, solution)
647
648    def cbUseSolution(self):
649        return self._solver_model.cbUseSolution()
650
651    def _add_column(self, var, obj_coef, constraints, coefficients):
652        """Add a column to the solver's model
653
654        This will add the Pyomo variable var to the solver's
655        model, and put the coefficients on the associated
656        constraints in the solver model. If the obj_coef is
657        not zero, it will add obj_coef*var to the objective
658        of the solver's model.
659
660        Parameters
661        ----------
662        var: Var (scalar Var or single _VarData)
663        obj_coef: float
664        constraints: list of solver constraints
665        coefficients: list of coefficients to put on var in the associated constraint
666        """
667
668        ## set-up add var
669        varname = self._symbol_map.getSymbol(var, self._labeler)
670        vtype = self._gurobi_vtype_from_var(var)
671        lb, ub = self._gurobi_lb_ub_from_var(var)
672
673        gurobipy_var = self._solver_model.addVar(obj=obj_coef, lb=lb, ub=ub, vtype=vtype, name=varname,
674                            column=gurobipy.Column(coeffs=coefficients, constrs=constraints) )
675
676        self._pyomo_var_to_solver_var_map[var] = gurobipy_var
677        self._solver_var_to_pyomo_var_map[gurobipy_var] = var
678        self._referenced_variables[var] = len(coefficients)
679
680    def reset(self):
681        self._solver_model.reset()
682