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