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