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
11import logging
12import re
13import sys
14
15from pyomo.common.collections import ComponentSet, ComponentMap, Bunch
16from pyomo.common.dependencies import attempt_import
17from pyomo.common.errors import ApplicationError
18from pyomo.common.tempfiles import TempfileManager
19from pyomo.common.tee import capture_output
20from pyomo.core.expr.numvalue import is_fixed
21from pyomo.core.expr.numvalue import value
22from pyomo.repn import generate_standard_repn
23from pyomo.solvers.plugins.solvers.direct_solver import DirectSolver
24from pyomo.solvers.plugins.solvers.direct_or_persistent_solver import DirectOrPersistentSolver
25from pyomo.core.kernel.objective import minimize, maximize
26from pyomo.opt.results.results_ import SolverResults
27from pyomo.opt.results.solution import Solution, SolutionStatus
28from pyomo.opt.results.solver import TerminationCondition, SolverStatus
29from pyomo.opt.base import SolverFactory
30from pyomo.core.base.suffix import Suffix
31import pyomo.core.base.var
32
33
34logger = logging.getLogger('pyomo.solvers')
35
36
37class DegreeError(ValueError):
38    pass
39
40def _is_numeric(x):
41    try:
42        float(x)
43    except ValueError:
44        return False
45    return True
46
47
48def _parse_gurobi_version(gurobipy, avail):
49    if not avail:
50        return
51    GurobiDirect._version = gurobipy.gurobi.version()
52    GurobiDirect._name = "Gurobi %s.%s%s" % GurobiDirect._version
53    while len(GurobiDirect._version) < 4:
54        GurobiDirect._version += (0,)
55    GurobiDirect._version = GurobiDirect._version[:4]
56    GurobiDirect._version_major = GurobiDirect._version[0]
57
58gurobipy, gurobipy_available = attempt_import(
59    'gurobipy',
60    # Other forms of exceptions can be thrown by the gurobi python
61    # import.  For example, a gurobipy.GurobiError exception is thrown
62    # if all tokens for Gurobi are already in use; assuming, of course,
63    # the license is a token license.  Unfortunately, you can't import
64    # without a license, which means we can't explicitly test for that
65    # exception!
66    catch_exceptions=(Exception,),
67    callback=_parse_gurobi_version,
68)
69
70
71@SolverFactory.register('gurobi_direct', doc='Direct python interface to Gurobi')
72class GurobiDirect(DirectSolver):
73
74    _verified_license = None
75    _import_messages = ''
76    _name = None
77    _version = 0
78    _version_major = 0
79
80    def __init__(self, **kwds):
81        if 'type' not in kwds:
82            kwds['type'] = 'gurobi_direct'
83        super(GurobiDirect, self).__init__(**kwds)
84        self._pyomo_var_to_solver_var_map = ComponentMap()
85        self._solver_var_to_pyomo_var_map = ComponentMap()
86        self._pyomo_con_to_solver_con_map = dict()
87        self._solver_con_to_pyomo_con_map = ComponentMap()
88        self._needs_updated = True  # flag that indicates if solver_model.update() needs called before getting variable and constraint attributes
89        self._callback = None
90        self._callback_func = None
91
92        self._python_api_exists = gurobipy_available
93        self._range_constraints = set()
94
95        self._max_obj_degree = 2
96        self._max_constraint_degree = 2
97
98        # Note: Undefined capabilites default to None
99        self._capabilities.linear = True
100        self._capabilities.quadratic_objective = True
101        self._capabilities.quadratic_constraint = True
102        self._capabilities.integer = True
103        self._capabilities.sos1 = True
104        self._capabilities.sos2 = True
105
106        # fix for compatibility with pre-5.0 Gurobi
107        #
108        # Note: Unfortunately, this will trigger the immediate import
109        #    of the gurobipy module
110        if gurobipy_available and GurobiDirect._version_major < 5:
111            self._max_constraint_degree = 1
112            self._capabilities.quadratic_constraint = False
113
114        # remove the instance-level definition of the gurobi version:
115        # because the version comes from an imported module, only one
116        # version of gurobi is supported (and stored as a class attribute)
117        del self._version
118
119    def available(self, exception_flag=True):
120        if not gurobipy_available:
121            if exception_flag:
122                gurobipy.log_import_warning(logger=__name__)
123                raise ApplicationError(
124                    "No Python bindings available for %s solver plugin"
125                    % (type(self),))
126            return False
127        if self._verified_license is None:
128            with capture_output(capture_fd=True) as OUT:
129                try:
130                    # verify that we can get a Gurobi license
131                    # Gurobipy writes out license file information when creating
132                    # the environment
133                    m = gurobipy.Model()
134                    m.dispose()
135                    GurobiDirect._verified_license = True
136                except Exception as e:
137                    GurobiDirect._import_messages += \
138                        "\nCould not create Model - gurobi message=%s\n" % (e,)
139                    GurobiDirect._verified_license = False
140            if OUT.getvalue():
141                GurobiDirect._import_messages += "\n" + OUT.getvalue()
142        if exception_flag and not self._verified_license:
143            logger.warning(GurobiDirect._import_messages)
144            raise ApplicationError(
145                "Could not create a gurobipy Model for %s solver plugin"
146                % (type(self),))
147        return self._verified_license
148
149    def _apply_solver(self):
150        if not self._save_results:
151            for block in self._pyomo_model.block_data_objects(descend_into=True,
152                                                              active=True):
153                for var in block.component_data_objects(ctype=pyomo.core.base.var.Var,
154                                                        descend_into=False,
155                                                        active=True,
156                                                        sort=False):
157                    var.stale = True
158        if self._tee:
159            self._solver_model.setParam('OutputFlag', 1)
160        else:
161            self._solver_model.setParam('OutputFlag', 0)
162
163        self._solver_model.setParam('LogFile', self._log_file)
164
165        if self._keepfiles:
166            print("Solver log file: "+self._log_file)
167
168        # Options accepted by gurobi (case insensitive):
169        # ['Cutoff', 'IterationLimit', 'NodeLimit', 'SolutionLimit', 'TimeLimit',
170        #  'FeasibilityTol', 'IntFeasTol', 'MarkowitzTol', 'MIPGap', 'MIPGapAbs',
171        #  'OptimalityTol', 'PSDTol', 'Method', 'PerturbValue', 'ObjScale', 'ScaleFlag',
172        #  'SimplexPricing', 'Quad', 'NormAdjust', 'BarIterLimit', 'BarConvTol',
173        #  'BarCorrectors', 'BarOrder', 'Crossover', 'CrossoverBasis', 'BranchDir',
174        #  'Heuristics', 'MinRelNodes', 'MIPFocus', 'NodefileStart', 'NodefileDir',
175        #  'NodeMethod', 'PumpPasses', 'RINS', 'SolutionNumber', 'SubMIPNodes', 'Symmetry',
176        #  'VarBranch', 'Cuts', 'CutPasses', 'CliqueCuts', 'CoverCuts', 'CutAggPasses',
177        #  'FlowCoverCuts', 'FlowPathCuts', 'GomoryPasses', 'GUBCoverCuts', 'ImpliedCuts',
178        #  'MIPSepCuts', 'MIRCuts', 'NetworkCuts', 'SubMIPCuts', 'ZeroHalfCuts', 'ModKCuts',
179        #  'Aggregate', 'AggFill', 'PreDual', 'DisplayInterval', 'IISMethod', 'InfUnbdInfo',
180        #  'LogFile', 'PreCrush', 'PreDepRow', 'PreMIQPMethod', 'PrePasses', 'Presolve',
181        #  'ResultFile', 'ImproveStartTime', 'ImproveStartGap', 'Threads', 'Dummy', 'OutputFlag']
182        for key, option in self.options.items():
183            # When options come from the pyomo command, all
184            # values are string types, so we try to cast
185            # them to a numeric value in the event that
186            # setting the parameter fails.
187            try:
188                self._solver_model.setParam(key, option)
189            except TypeError:
190                # we place the exception handling for
191                # checking the cast of option to a float in
192                # another function so that we can simply
193                # call raise here instead of except
194                # TypeError as e / raise e, because the
195                # latter does not preserve the Gurobi stack
196                # trace
197                if not _is_numeric(option):
198                    raise
199                self._solver_model.setParam(key, float(option))
200
201        if self._version_major >= 5:
202            for suffix in self._suffixes:
203                if re.match(suffix, "dual"):
204                    self._solver_model.setParam(gurobipy.GRB.Param.QCPDual, 1)
205
206        self._solver_model.optimize(self._callback)
207        self._needs_updated = False
208
209        self._solver_model.setParam('LogFile', 'default')
210
211        # FIXME: can we get a return code indicating if Gurobi had a significant failure?
212        return Bunch(rc=None, log=None)
213
214    def _get_expr_from_pyomo_repn(self, repn, max_degree=2):
215        referenced_vars = ComponentSet()
216
217        degree = repn.polynomial_degree()
218        if (degree is None) or (degree > max_degree):
219            raise DegreeError('GurobiDirect does not support expressions of degree {0}.'.format(degree))
220
221        if len(repn.linear_vars) > 0:
222            referenced_vars.update(repn.linear_vars)
223            new_expr = gurobipy.LinExpr(repn.linear_coefs, [self._pyomo_var_to_solver_var_map[i] for i in repn.linear_vars])
224        else:
225            new_expr = 0.0
226
227        for i,v in enumerate(repn.quadratic_vars):
228            x,y = v
229            new_expr += repn.quadratic_coefs[i] * self._pyomo_var_to_solver_var_map[x] * self._pyomo_var_to_solver_var_map[y]
230            referenced_vars.add(x)
231            referenced_vars.add(y)
232
233        new_expr += repn.constant
234
235        return new_expr, referenced_vars
236
237    def _get_expr_from_pyomo_expr(self, expr, max_degree=2):
238        if max_degree == 2:
239            repn = generate_standard_repn(expr, quadratic=True)
240        else:
241            repn = generate_standard_repn(expr, quadratic=False)
242
243        try:
244            gurobi_expr, referenced_vars = self._get_expr_from_pyomo_repn(repn, max_degree)
245        except DegreeError as e:
246            msg = e.args[0]
247            msg += '\nexpr: {0}'.format(expr)
248            raise DegreeError(msg)
249
250        return gurobi_expr, referenced_vars
251
252    def _gurobi_lb_ub_from_var(self, var):
253        if var.is_fixed():
254            val = var.value
255            return val, val
256        if var.has_lb():
257            lb = value(var.lb)
258        else:
259            lb = -gurobipy.GRB.INFINITY
260        if var.has_ub():
261            ub = value(var.ub)
262        else:
263            ub = gurobipy.GRB.INFINITY
264        return lb, ub
265
266    def _add_var(self, var):
267        varname = self._symbol_map.getSymbol(var, self._labeler)
268        vtype = self._gurobi_vtype_from_var(var)
269        lb, ub = self._gurobi_lb_ub_from_var(var)
270
271        gurobipy_var = self._solver_model.addVar(lb=lb, ub=ub, vtype=vtype, name=varname)
272
273        self._pyomo_var_to_solver_var_map[var] = gurobipy_var
274        self._solver_var_to_pyomo_var_map[gurobipy_var] = var
275        self._referenced_variables[var] = 0
276
277        self._needs_updated = True
278
279    def _set_instance(self, model, kwds={}):
280        self._range_constraints = set()
281        DirectOrPersistentSolver._set_instance(self, model, kwds)
282        self._pyomo_con_to_solver_con_map = dict()
283        self._solver_con_to_pyomo_con_map = ComponentMap()
284        self._pyomo_var_to_solver_var_map = ComponentMap()
285        self._solver_var_to_pyomo_var_map = ComponentMap()
286        try:
287            if model.name is not None:
288                self._solver_model = gurobipy.Model(model.name)
289            else:
290                self._solver_model = gurobipy.Model()
291        except Exception:
292            e = sys.exc_info()[1]
293            msg = ("Unable to create Gurobi model. "
294                   "Have you installed the Python "
295                   "bindings for Gurobi?\n\n\t"+
296                   "Error message: {0}".format(e))
297            raise Exception(msg)
298
299        self._add_block(model)
300
301        for var, n_ref in self._referenced_variables.items():
302            if n_ref != 0:
303                if var.fixed:
304                    if not self._output_fixed_variable_bounds:
305                        raise ValueError(
306                            "Encountered a fixed variable (%s) inside "
307                            "an active objective or constraint "
308                            "expression on model %s, which is usually "
309                            "indicative of a preprocessing error. Use "
310                            "the IO-option 'output_fixed_variable_bounds=True' "
311                            "to suppress this error and fix the variable "
312                            "by overwriting its bounds in the Gurobi instance."
313                            % (var.name, self._pyomo_model.name,))
314
315    def _add_block(self, block):
316        DirectOrPersistentSolver._add_block(self, block)
317
318    def _add_constraint(self, con):
319        if not con.active:
320            return None
321
322        if is_fixed(con.body):
323            if self._skip_trivial_constraints:
324                return None
325
326        conname = self._symbol_map.getSymbol(con, self._labeler)
327
328        if con._linear_canonical_form:
329            gurobi_expr, referenced_vars = self._get_expr_from_pyomo_repn(
330                con.canonical_form(),
331                self._max_constraint_degree)
332        #elif isinstance(con, LinearCanonicalRepn):
333        #    gurobi_expr, referenced_vars = self._get_expr_from_pyomo_repn(
334        #        con,
335        #        self._max_constraint_degree)
336        else:
337            gurobi_expr, referenced_vars = self._get_expr_from_pyomo_expr(
338                con.body,
339                self._max_constraint_degree)
340
341        if con.has_lb():
342            if not is_fixed(con.lower):
343                raise ValueError("Lower bound of constraint {0} "
344                                 "is not constant.".format(con))
345        if con.has_ub():
346            if not is_fixed(con.upper):
347                raise ValueError("Upper bound of constraint {0} "
348                                 "is not constant.".format(con))
349
350        if con.equality:
351            gurobipy_con = self._solver_model.addConstr(lhs=gurobi_expr,
352                                                        sense=gurobipy.GRB.EQUAL,
353                                                        rhs=value(con.lower),
354                                                        name=conname)
355        elif con.has_lb() and con.has_ub():
356            gurobipy_con = self._solver_model.addRange(gurobi_expr,
357                                                       value(con.lower),
358                                                       value(con.upper),
359                                                       name=conname)
360            self._range_constraints.add(con)
361        elif con.has_lb():
362            gurobipy_con = self._solver_model.addConstr(lhs=gurobi_expr,
363                                                        sense=gurobipy.GRB.GREATER_EQUAL,
364                                                        rhs=value(con.lower),
365                                                        name=conname)
366        elif con.has_ub():
367            gurobipy_con = self._solver_model.addConstr(lhs=gurobi_expr,
368                                                        sense=gurobipy.GRB.LESS_EQUAL,
369                                                        rhs=value(con.upper),
370                                                        name=conname)
371        else:
372            raise ValueError("Constraint does not have a lower "
373                             "or an upper bound: {0} \n".format(con))
374
375        for var in referenced_vars:
376            self._referenced_variables[var] += 1
377        self._vars_referenced_by_con[con] = referenced_vars
378        self._pyomo_con_to_solver_con_map[con] = gurobipy_con
379        self._solver_con_to_pyomo_con_map[gurobipy_con] = con
380
381        self._needs_updated = True
382
383    def _add_sos_constraint(self, con):
384        if not con.active:
385            return None
386
387        conname = self._symbol_map.getSymbol(con, self._labeler)
388        level = con.level
389        if level == 1:
390            sos_type = gurobipy.GRB.SOS_TYPE1
391        elif level == 2:
392            sos_type = gurobipy.GRB.SOS_TYPE2
393        else:
394            raise ValueError("Solver does not support SOS "
395                             "level {0} constraints".format(level))
396
397        gurobi_vars = []
398        weights = []
399
400        self._vars_referenced_by_con[con] = ComponentSet()
401
402        if hasattr(con, 'get_items'):
403            # aml sos constraint
404            sos_items = list(con.get_items())
405        else:
406            # kernel sos constraint
407            sos_items = list(con.items())
408
409        for v, w in sos_items:
410            self._vars_referenced_by_con[con].add(v)
411            gurobi_vars.append(self._pyomo_var_to_solver_var_map[v])
412            self._referenced_variables[v] += 1
413            weights.append(w)
414
415        gurobipy_con = self._solver_model.addSOS(sos_type, gurobi_vars, weights)
416        self._pyomo_con_to_solver_con_map[con] = gurobipy_con
417        self._solver_con_to_pyomo_con_map[gurobipy_con] = con
418
419        self._needs_updated = True
420
421    def _gurobi_vtype_from_var(self, var):
422        """
423        This function takes a pyomo variable and returns the appropriate gurobi variable type
424        :param var: pyomo.core.base.var.Var
425        :return: gurobipy.GRB.CONTINUOUS or gurobipy.GRB.BINARY or gurobipy.GRB.INTEGER
426        """
427        if var.is_binary():
428            vtype = gurobipy.GRB.BINARY
429        elif var.is_integer():
430            vtype = gurobipy.GRB.INTEGER
431        elif var.is_continuous():
432            vtype = gurobipy.GRB.CONTINUOUS
433        else:
434            raise ValueError('Variable domain type is not recognized for {0}'.format(var.domain))
435        return vtype
436
437    def _set_objective(self, obj):
438        if self._objective is not None:
439            for var in self._vars_referenced_by_obj:
440                self._referenced_variables[var] -= 1
441            self._vars_referenced_by_obj = ComponentSet()
442            self._objective = None
443
444        if obj.active is False:
445            raise ValueError('Cannot add inactive objective to solver.')
446
447        if obj.sense == minimize:
448            sense = gurobipy.GRB.MINIMIZE
449        elif obj.sense == maximize:
450            sense = gurobipy.GRB.MAXIMIZE
451        else:
452            raise ValueError('Objective sense is not recognized: {0}'.format(obj.sense))
453
454        gurobi_expr, referenced_vars = self._get_expr_from_pyomo_expr(obj.expr, self._max_obj_degree)
455
456        for var in referenced_vars:
457            self._referenced_variables[var] += 1
458
459        self._solver_model.setObjective(gurobi_expr, sense=sense)
460        self._objective = obj
461        self._vars_referenced_by_obj = referenced_vars
462
463        self._needs_updated = True
464
465    def _postsolve(self):
466        # the only suffixes that we extract from GUROBI are
467        # constraint duals, constraint slacks, and variable
468        # reduced-costs. scan through the solver suffix list
469        # and throw an exception if the user has specified
470        # any others.
471        extract_duals = False
472        extract_slacks = False
473        extract_reduced_costs = False
474        for suffix in self._suffixes:
475            flag = False
476            if re.match(suffix, "dual"):
477                extract_duals = True
478                flag = True
479            if re.match(suffix, "slack"):
480                extract_slacks = True
481                flag = True
482            if re.match(suffix, "rc"):
483                extract_reduced_costs = True
484                flag = True
485            if not flag:
486                raise RuntimeError("***The gurobi_direct solver plugin cannot extract solution suffix="+suffix)
487
488        gprob = self._solver_model
489        grb = gurobipy.GRB
490        status = gprob.Status
491
492        if gprob.getAttr(gurobipy.GRB.Attr.IsMIP):
493            if extract_reduced_costs:
494                logger.warning("Cannot get reduced costs for MIP.")
495            if extract_duals:
496                logger.warning("Cannot get duals for MIP.")
497            extract_reduced_costs = False
498            extract_duals = False
499
500        self.results = SolverResults()
501        soln = Solution()
502
503        self.results.solver.name = GurobiDirect._name
504        self.results.solver.wallclock_time = gprob.Runtime
505
506        if status == grb.LOADED:  # problem is loaded, but no solution
507            self.results.solver.status = SolverStatus.aborted
508            self.results.solver.termination_message = "Model is loaded, but no solution information is available."
509            self.results.solver.termination_condition = TerminationCondition.error
510            soln.status = SolutionStatus.unknown
511        elif status == grb.OPTIMAL:  # optimal
512            self.results.solver.status = SolverStatus.ok
513            self.results.solver.termination_message = "Model was solved to optimality (subject to tolerances), " \
514                                                      "and an optimal solution is available."
515            self.results.solver.termination_condition = TerminationCondition.optimal
516            soln.status = SolutionStatus.optimal
517        elif status == grb.INFEASIBLE:
518            self.results.solver.status = SolverStatus.warning
519            self.results.solver.termination_message = "Model was proven to be infeasible"
520            self.results.solver.termination_condition = TerminationCondition.infeasible
521            soln.status = SolutionStatus.infeasible
522        elif status == grb.INF_OR_UNBD:
523            self.results.solver.status = SolverStatus.warning
524            self.results.solver.termination_message = "Problem proven to be infeasible or unbounded."
525            self.results.solver.termination_condition = TerminationCondition.infeasibleOrUnbounded
526            soln.status = SolutionStatus.unsure
527        elif status == grb.UNBOUNDED:
528            self.results.solver.status = SolverStatus.warning
529            self.results.solver.termination_message = "Model was proven to be unbounded."
530            self.results.solver.termination_condition = TerminationCondition.unbounded
531            soln.status = SolutionStatus.unbounded
532        elif status == grb.CUTOFF:
533            self.results.solver.status = SolverStatus.aborted
534            self.results.solver.termination_message = "Optimal objective for model was proven to be worse than the " \
535                                                      "value specified in the Cutoff parameter. No solution " \
536                                                      "information is available."
537            self.results.solver.termination_condition = TerminationCondition.minFunctionValue
538            soln.status = SolutionStatus.unknown
539        elif status == grb.ITERATION_LIMIT:
540            self.results.solver.status = SolverStatus.aborted
541            self.results.solver.termination_message = "Optimization terminated because the total number of simplex " \
542                                                      "iterations performed exceeded the value specified in the " \
543                                                      "IterationLimit parameter."
544            self.results.solver.termination_condition = TerminationCondition.maxIterations
545            soln.status = SolutionStatus.stoppedByLimit
546        elif status == grb.NODE_LIMIT:
547            self.results.solver.status = SolverStatus.aborted
548            self.results.solver.termination_message = "Optimization terminated because the total number of " \
549                                                      "branch-and-cut nodes explored exceeded the value specified " \
550                                                      "in the NodeLimit parameter"
551            self.results.solver.termination_condition = TerminationCondition.maxEvaluations
552            soln.status = SolutionStatus.stoppedByLimit
553        elif status == grb.TIME_LIMIT:
554            self.results.solver.status = SolverStatus.aborted
555            self.results.solver.termination_message = "Optimization terminated because the time expended exceeded " \
556                                                      "the value specified in the TimeLimit parameter."
557            self.results.solver.termination_condition = TerminationCondition.maxTimeLimit
558            soln.status = SolutionStatus.stoppedByLimit
559        elif status == grb.SOLUTION_LIMIT:
560            self.results.solver.status = SolverStatus.aborted
561            self.results.solver.termination_message = "Optimization terminated because the number of solutions found " \
562                                                      "reached the value specified in the SolutionLimit parameter."
563            self.results.solver.termination_condition = TerminationCondition.unknown
564            soln.status = SolutionStatus.stoppedByLimit
565        elif status == grb.INTERRUPTED:
566            self.results.solver.status = SolverStatus.aborted
567            self.results.solver.termination_message = "Optimization was terminated by the user."
568            self.results.solver.termination_condition = TerminationCondition.error
569            soln.status = SolutionStatus.error
570        elif status == grb.NUMERIC:
571            self.results.solver.status = SolverStatus.error
572            self.results.solver.termination_message = "Optimization was terminated due to unrecoverable numerical " \
573                                                      "difficulties."
574            self.results.solver.termination_condition = TerminationCondition.error
575            soln.status = SolutionStatus.error
576        elif status == grb.SUBOPTIMAL:
577            self.results.solver.status = SolverStatus.warning
578            self.results.solver.termination_message = "Unable to satisfy optimality tolerances; a sub-optimal " \
579                                                      "solution is available."
580            self.results.solver.termination_condition = TerminationCondition.other
581            soln.status = SolutionStatus.feasible
582        # note that USER_OBJ_LIMIT was added in Gurobi 7.0, so it may not be present
583        elif (status is not None) and \
584             (status == getattr(grb,'USER_OBJ_LIMIT',None)):
585            self.results.solver.status = SolverStatus.aborted
586            self.results.solver.termination_message = "User specified an objective limit " \
587                                                      "(a bound on either the best objective " \
588                                                      "or the best bound), and that limit has " \
589                                                      "been reached. Solution is available."
590            self.results.solver.termination_condition = TerminationCondition.other
591            soln.status = SolutionStatus.stoppedByLimit
592        else:
593            self.results.solver.status = SolverStatus.error
594            self.results.solver.termination_message = \
595                ("Unhandled Gurobi solve status "
596                 "("+str(status)+")")
597            self.results.solver.termination_condition = TerminationCondition.error
598            soln.status = SolutionStatus.error
599
600        self.results.problem.name = gprob.ModelName
601
602        if gprob.ModelSense == 1:
603            self.results.problem.sense = minimize
604        elif gprob.ModelSense == -1:
605            self.results.problem.sense = maximize
606        else:
607            raise RuntimeError('Unrecognized gurobi objective sense: {0}'.format(gprob.ModelSense))
608
609        self.results.problem.upper_bound = None
610        self.results.problem.lower_bound = None
611        if (gprob.NumBinVars + gprob.NumIntVars) == 0:
612            try:
613                self.results.problem.upper_bound = gprob.ObjVal
614                self.results.problem.lower_bound = gprob.ObjVal
615            except (gurobipy.GurobiError, AttributeError):
616                pass
617        elif gprob.ModelSense == 1:  # minimizing
618            try:
619                self.results.problem.upper_bound = gprob.ObjVal
620            except (gurobipy.GurobiError, AttributeError):
621                pass
622            try:
623                self.results.problem.lower_bound = gprob.ObjBound
624            except (gurobipy.GurobiError, AttributeError):
625                pass
626        elif gprob.ModelSense == -1:  # maximizing
627            try:
628                self.results.problem.upper_bound = gprob.ObjBound
629            except (gurobipy.GurobiError, AttributeError):
630                pass
631            try:
632                self.results.problem.lower_bound = gprob.ObjVal
633            except (gurobipy.GurobiError, AttributeError):
634                pass
635        else:
636            raise RuntimeError('Unrecognized gurobi objective sense: {0}'.format(gprob.ModelSense))
637
638        try:
639            soln.gap = self.results.problem.upper_bound - self.results.problem.lower_bound
640        except TypeError:
641            soln.gap = None
642
643        self.results.problem.number_of_constraints = gprob.NumConstrs + gprob.NumQConstrs + gprob.NumSOS
644        self.results.problem.number_of_nonzeros = gprob.NumNZs
645        self.results.problem.number_of_variables = gprob.NumVars
646        self.results.problem.number_of_binary_variables = gprob.NumBinVars
647        self.results.problem.number_of_integer_variables = gprob.NumIntVars
648        self.results.problem.number_of_continuous_variables = gprob.NumVars - gprob.NumIntVars - gprob.NumBinVars
649        self.results.problem.number_of_objectives = 1
650        self.results.problem.number_of_solutions = gprob.SolCount
651
652        # if a solve was stopped by a limit, we still need to check to
653        # see if there is a solution available - this may not always
654        # be the case, both in LP and MIP contexts.
655        if self._save_results:
656            """
657            This code in this if statement is only needed for backwards compatability. It is more efficient to set
658            _save_results to False and use load_vars, load_duals, etc.
659            """
660            if gprob.SolCount > 0:
661                soln_variables = soln.variable
662                soln_constraints = soln.constraint
663
664                gurobi_vars = self._solver_model.getVars()
665                gurobi_vars = list(set(gurobi_vars).intersection(set(self._pyomo_var_to_solver_var_map.values())))
666                var_vals = self._solver_model.getAttr("X", gurobi_vars)
667                names = self._solver_model.getAttr("VarName", gurobi_vars)
668                for gurobi_var, val, name in zip(gurobi_vars, var_vals, names):
669                    pyomo_var = self._solver_var_to_pyomo_var_map[gurobi_var]
670                    if self._referenced_variables[pyomo_var] > 0:
671                        pyomo_var.stale = False
672                        soln_variables[name] = {"Value": val}
673
674                if extract_reduced_costs:
675                    vals = self._solver_model.getAttr("Rc", gurobi_vars)
676                    for gurobi_var, val, name in zip(gurobi_vars, vals, names):
677                        pyomo_var = self._solver_var_to_pyomo_var_map[gurobi_var]
678                        if self._referenced_variables[pyomo_var] > 0:
679                            soln_variables[name]["Rc"] = val
680
681                if extract_duals or extract_slacks:
682                    gurobi_cons = self._solver_model.getConstrs()
683                    con_names = self._solver_model.getAttr("ConstrName", gurobi_cons)
684                    for name in con_names:
685                        soln_constraints[name] = {}
686                    if self._version_major >= 5:
687                        gurobi_q_cons = self._solver_model.getQConstrs()
688                        q_con_names = self._solver_model.getAttr("QCName", gurobi_q_cons)
689                        for name in q_con_names:
690                            soln_constraints[name] = {}
691
692                if extract_duals:
693                    vals = self._solver_model.getAttr("Pi", gurobi_cons)
694                    for val, name in zip(vals, con_names):
695                        soln_constraints[name]["Dual"] = val
696                    if self._version_major >= 5:
697                        q_vals = self._solver_model.getAttr("QCPi", gurobi_q_cons)
698                        for val, name in zip(q_vals, q_con_names):
699                            soln_constraints[name]["Dual"] = val
700
701                if extract_slacks:
702                    gurobi_range_con_vars = set(self._solver_model.getVars()) - set(self._pyomo_var_to_solver_var_map.values())
703                    vals = self._solver_model.getAttr("Slack", gurobi_cons)
704                    for gurobi_con, val, name in zip(gurobi_cons, vals, con_names):
705                        pyomo_con = self._solver_con_to_pyomo_con_map[gurobi_con]
706                        if pyomo_con in self._range_constraints:
707                            lin_expr = self._solver_model.getRow(gurobi_con)
708                            for i in reversed(range(lin_expr.size())):
709                                v = lin_expr.getVar(i)
710                                if v in gurobi_range_con_vars:
711                                    Us_ = v.X
712                                    Ls_ = v.UB - v.X
713                                    if Us_ > Ls_:
714                                        soln_constraints[name]["Slack"] = Us_
715                                    else:
716                                        soln_constraints[name]["Slack"] = -Ls_
717                                    break
718                        else:
719                            soln_constraints[name]["Slack"] = val
720                    if self._version_major >= 5:
721                        q_vals = self._solver_model.getAttr("QCSlack", gurobi_q_cons)
722                        for val, name in zip(q_vals, q_con_names):
723                            soln_constraints[name]["Slack"] = val
724        elif self._load_solutions:
725            if gprob.SolCount > 0:
726
727                self._load_vars()
728
729                if extract_reduced_costs:
730                    self._load_rc()
731
732                if extract_duals:
733                    self._load_duals()
734
735                if extract_slacks:
736                    self._load_slacks()
737
738        self.results.solution.insert(soln)
739
740        # finally, clean any temporary files registered with the temp file
741        # manager, created populated *directly* by this plugin.
742        TempfileManager.pop(remove=not self._keepfiles)
743
744        return DirectOrPersistentSolver._postsolve(self)
745
746    def warm_start_capable(self):
747        return True
748
749    def _warm_start(self):
750        for pyomo_var, gurobipy_var in self._pyomo_var_to_solver_var_map.items():
751            if pyomo_var.value is not None:
752                gurobipy_var.setAttr(gurobipy.GRB.Attr.Start, value(pyomo_var))
753        self._needs_updated = True
754
755    def _load_vars(self, vars_to_load=None):
756        var_map = self._pyomo_var_to_solver_var_map
757        ref_vars = self._referenced_variables
758        if vars_to_load is None:
759            vars_to_load = var_map.keys()
760
761        gurobi_vars_to_load = [var_map[pyomo_var] for pyomo_var in vars_to_load]
762        vals = self._solver_model.getAttr("X", gurobi_vars_to_load)
763
764        for var, val in zip(vars_to_load, vals):
765            if ref_vars[var] > 0:
766                var.stale = False
767                var.value = val
768
769    def _load_rc(self, vars_to_load=None):
770        if not hasattr(self._pyomo_model, 'rc'):
771            self._pyomo_model.rc = Suffix(direction=Suffix.IMPORT)
772        var_map = self._pyomo_var_to_solver_var_map
773        ref_vars = self._referenced_variables
774        rc = self._pyomo_model.rc
775        if vars_to_load is None:
776            vars_to_load = var_map.keys()
777
778        gurobi_vars_to_load = [var_map[pyomo_var] for pyomo_var in vars_to_load]
779        vals = self._solver_model.getAttr("Rc", gurobi_vars_to_load)
780
781        for var, val in zip(vars_to_load, vals):
782            if ref_vars[var] > 0:
783                rc[var] = val
784
785    def _load_duals(self, cons_to_load=None):
786        if not hasattr(self._pyomo_model, 'dual'):
787            self._pyomo_model.dual = Suffix(direction=Suffix.IMPORT)
788        con_map = self._pyomo_con_to_solver_con_map
789        reverse_con_map = self._solver_con_to_pyomo_con_map
790        dual = self._pyomo_model.dual
791
792        if cons_to_load is None:
793            linear_cons_to_load = self._solver_model.getConstrs()
794            if self._version_major >= 5:
795                quadratic_cons_to_load = self._solver_model.getQConstrs()
796        else:
797            gurobi_cons_to_load = set([con_map[pyomo_con] for pyomo_con in cons_to_load])
798            linear_cons_to_load = gurobi_cons_to_load.intersection(set(self._solver_model.getConstrs()))
799            if self._version_major >= 5:
800                quadratic_cons_to_load = gurobi_cons_to_load.intersection(set(self._solver_model.getQConstrs()))
801        linear_vals = self._solver_model.getAttr("Pi", linear_cons_to_load)
802        if self._version_major >= 5:
803            quadratic_vals = self._solver_model.getAttr("QCPi", quadratic_cons_to_load)
804
805        for gurobi_con, val in zip(linear_cons_to_load, linear_vals):
806            pyomo_con = reverse_con_map[gurobi_con]
807            dual[pyomo_con] = val
808        if self._version_major >= 5:
809            for gurobi_con, val in zip(quadratic_cons_to_load, quadratic_vals):
810                pyomo_con = reverse_con_map[gurobi_con]
811                dual[pyomo_con] = val
812
813    def _load_slacks(self, cons_to_load=None):
814        if not hasattr(self._pyomo_model, 'slack'):
815            self._pyomo_model.slack = Suffix(direction=Suffix.IMPORT)
816        con_map = self._pyomo_con_to_solver_con_map
817        reverse_con_map = self._solver_con_to_pyomo_con_map
818        slack = self._pyomo_model.slack
819
820        gurobi_range_con_vars = set(self._solver_model.getVars()) - set(self._pyomo_var_to_solver_var_map.values())
821
822        if cons_to_load is None:
823            linear_cons_to_load = self._solver_model.getConstrs()
824            if self._version_major >= 5:
825                quadratic_cons_to_load = self._solver_model.getQConstrs()
826        else:
827            gurobi_cons_to_load = set([con_map[pyomo_con] for pyomo_con in cons_to_load])
828            linear_cons_to_load = gurobi_cons_to_load.intersection(set(self._solver_model.getConstrs()))
829            if self._version_major >= 5:
830                quadratic_cons_to_load = gurobi_cons_to_load.intersection(set(self._solver_model.getQConstrs()))
831        linear_vals = self._solver_model.getAttr("Slack", linear_cons_to_load)
832        if self._version_major >= 5:
833            quadratic_vals = self._solver_model.getAttr("QCSlack", quadratic_cons_to_load)
834
835        for gurobi_con, val in zip(linear_cons_to_load, linear_vals):
836            pyomo_con = reverse_con_map[gurobi_con]
837            if pyomo_con in self._range_constraints:
838                lin_expr = self._solver_model.getRow(gurobi_con)
839                for i in reversed(range(lin_expr.size())):
840                    v = lin_expr.getVar(i)
841                    if v in gurobi_range_con_vars:
842                        Us_ = v.X
843                        Ls_ = v.UB - v.X
844                        if Us_ > Ls_:
845                            slack[pyomo_con] = Us_
846                        else:
847                            slack[pyomo_con] = -Ls_
848                        break
849            else:
850                slack[pyomo_con] = val
851        if self._version_major >= 5:
852            for gurobi_con, val in zip(quadratic_cons_to_load, quadratic_vals):
853                pyomo_con = reverse_con_map[gurobi_con]
854                slack[pyomo_con] = val
855
856    def load_duals(self, cons_to_load=None):
857        """
858        Load the duals into the 'dual' suffix. The 'dual' suffix must live on the parent model.
859
860        Parameters
861        ----------
862        cons_to_load: list of Constraint
863        """
864        self._load_duals(cons_to_load)
865
866    def load_rc(self, vars_to_load):
867        """
868        Load the reduced costs into the 'rc' suffix. The 'rc' suffix must live on the parent model.
869
870        Parameters
871        ----------
872        vars_to_load: list of Var
873        """
874        self._load_rc(vars_to_load)
875
876    def load_slacks(self, cons_to_load=None):
877        """
878        Load the values of the slack variables into the 'slack' suffix. The 'slack' suffix must live on the parent
879        model.
880
881        Parameters
882        ----------
883        cons_to_load: list of Constraint
884        """
885        self._load_slacks(cons_to_load)
886
887    def _update(self):
888        self._solver_model.update()
889        self._needs_updated = False
890