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