1import abc 2import enum 3from typing import Sequence, Dict, Optional, Mapping, NoReturn, List, Tuple 4from pyomo.core.base.constraint import _GeneralConstraintData, Constraint 5from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint 6from pyomo.core.base.var import _GeneralVarData, Var 7from pyomo.core.base.param import _ParamData, Param 8from pyomo.core.base.block import _BlockData, Block 9from pyomo.core.base.objective import _GeneralObjectiveData 10from pyomo.core.base.suffix import Suffix 11from pyomo.common.collections import ComponentMap, OrderedSet 12from .utils.get_objective import get_objective 13from .utils.identify_named_expressions import identify_named_expressions 14from pyomo.common.timing import HierarchicalTimer 15from pyomo.common.config import ConfigDict, ConfigValue, NonNegativeFloat 16from pyomo.common.errors import ApplicationError 17from pyomo.opt.base import SolverFactory as LegacySolverFactory 18from pyomo.common.factory import Factory 19import logging 20import os 21from pyomo.opt.results.results_ import SolverResults as LegacySolverResults 22from pyomo.opt.results.solution import Solution as LegacySolution, SolutionStatus as LegacySolutionStatus 23from pyomo.opt.results.solver import TerminationCondition as LegacyTerminationCondition, SolverStatus as LegacySolverStatus 24from pyomo.core.kernel.objective import minimize, maximize 25from pyomo.core.base import SymbolMap 26import weakref 27from io import StringIO 28 29 30class TerminationCondition(enum.Enum): 31 """ 32 An enumeration for checking the termination condition of solvers 33 """ 34 unknown = 0 35 """unknown serves as both a default value, and it is used when no other enum member makes sense""" 36 37 maxTimeLimit = 1 38 """The solver exited due to a time limit""" 39 40 maxIterations = 2 41 """The solver exited due to an iteration limit """ 42 43 objectiveLimit = 3 44 """The solver exited due to an objective limit""" 45 46 minStepLength = 4 47 """The solver exited due to a minimum step length""" 48 49 optimal = 5 50 """The solver exited with the optimal solution""" 51 52 unbounded = 8 53 """The solver exited because the problem is unbounded""" 54 55 infeasible = 9 56 """The solver exited because the problem is infeasible""" 57 58 infeasibleOrUnbounded = 10 59 """The solver exited because the problem is either infeasible or unbounded""" 60 61 error = 11 62 """The solver exited due to an error""" 63 64 interrupted = 12 65 """The solver exited because it was interrupted""" 66 67 licensingProblems = 13 68 """The solver exited due to licensing problems""" 69 70 71class SolverConfig(ConfigDict): 72 """ 73 Attributes 74 ---------- 75 time_limit: float 76 Time limit for the solver 77 stream_solver: bool 78 If True, then the solver log goes to stdout 79 load_solution: bool 80 If False, then the values of the primal variables will not be 81 loaded into the model 82 symbolic_solver_labels: bool 83 If True, the names given to the solver will reflect the names 84 of the pyomo components. Cannot be changed after set_instance 85 is called. 86 report_timing: bool 87 If True, then some timing information will be printed at the 88 end of the solve. 89 """ 90 def __init__(self, 91 description=None, 92 doc=None, 93 implicit=False, 94 implicit_domain=None, 95 visibility=0): 96 super(SolverConfig, self).__init__(description=description, 97 doc=doc, 98 implicit=implicit, 99 implicit_domain=implicit_domain, 100 visibility=visibility) 101 102 self.declare('time_limit', ConfigValue(domain=NonNegativeFloat)) 103 self.declare('stream_solver', ConfigValue(domain=bool)) 104 self.declare('load_solution', ConfigValue(domain=bool)) 105 self.declare('symbolic_solver_labels', ConfigValue(domain=bool)) 106 self.declare('report_timing', ConfigValue(domain=bool)) 107 108 self.time_limit: Optional[float] = None 109 self.stream_solver: bool = False 110 self.load_solution: bool = True 111 self.symbolic_solver_labels: bool = False 112 self.report_timing: bool = False 113 114 115class MIPSolverConfig(SolverConfig): 116 """ 117 Attributes 118 ---------- 119 mip_gap: float 120 Solver will terminate if the mip gap is less than mip_gap 121 relax_integrality: bool 122 If True, all integer variables will be relaxed to continuous 123 variables before solving 124 """ 125 def __init__(self, 126 description=None, 127 doc=None, 128 implicit=False, 129 implicit_domain=None, 130 visibility=0): 131 super(MIPSolverConfig, self).__init__(description=description, 132 doc=doc, 133 implicit=implicit, 134 implicit_domain=implicit_domain, 135 visibility=visibility) 136 137 self.declare('mip_gap', ConfigValue(domain=NonNegativeFloat)) 138 self.declare('relax_integrality', ConfigValue(domain=bool)) 139 140 self.mip_gap: Optional[float] = None 141 self.relax_integrality: bool = False 142 143 144class SolutionLoaderBase(abc.ABC): 145 def load_vars(self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None) -> NoReturn: 146 """ 147 Load the solution of the primal variables into the value attribute of the variables. 148 149 Parameters 150 ---------- 151 vars_to_load: list 152 A list of the variables whose solution should be loaded. If vars_to_load is None, then the solution 153 to all primal variables will be loaded. 154 """ 155 for v, val in self.get_primals(vars_to_load=vars_to_load).items(): 156 v.value = val 157 158 @abc.abstractmethod 159 def get_primals(self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None) -> Mapping[_GeneralVarData, float]: 160 """ 161 Returns a ComponentMap mapping variable to var value. 162 163 Parameters 164 ---------- 165 vars_to_load: list 166 A list of the variables whose solution value should be retreived. If vars_to_load is None, 167 then the values for all variables will be retreived. 168 169 Returns 170 ------- 171 primals: ComponentMap 172 Maps variables to solution values 173 """ 174 pass 175 176 def get_duals(self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None) -> Dict[_GeneralConstraintData, float]: 177 """ 178 Returns a dictionary mapping constraint to dual value. 179 180 Parameters 181 ---------- 182 cons_to_load: list 183 A list of the constraints whose duals should be retreived. If cons_to_load is None, then the duals for all 184 constraints will be retreived. 185 186 Returns 187 ------- 188 duals: dict 189 Maps constraints to dual values 190 """ 191 raise NotImplementedError(f'{type(self)} does not support the get_duals method') 192 193 def get_slacks(self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None) -> Dict[_GeneralConstraintData, float]: 194 """ 195 Returns a dictionary mapping constraint to slack. 196 197 Parameters 198 ---------- 199 cons_to_load: list 200 A list of the constraints whose duals should be loaded. If cons_to_load is None, then the duals for all 201 constraints will be loaded. 202 203 Returns 204 ------- 205 slacks: dict 206 Maps constraints to slacks 207 """ 208 raise NotImplementedError(f'{type(self)} does not support the get_slacks method') 209 210 def get_reduced_costs(self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None) -> Mapping[_GeneralVarData, float]: 211 """ 212 Returns a ComponentMap mapping variable to reduced cost. 213 214 Parameters 215 ---------- 216 vars_to_load: list 217 A list of the variables whose reduced cost should be retreived. If vars_to_load is None, then the 218 reduced costs for all variables will be loaded. 219 220 Returns 221 ------- 222 reduced_costs: ComponentMap 223 Maps variables to reduced costs 224 """ 225 raise NotImplementedError(f'{type(self)} does not support the get_reduced_costs method') 226 227 228class SolutionLoader(SolutionLoaderBase): 229 def __init__(self, primals, duals, slacks, reduced_costs): 230 """ 231 Parameters 232 ---------- 233 primals: dict 234 maps id(Var) to (var, value) 235 duals: dict 236 maps Constraint to dual value 237 slacks: dict 238 maps Constraint to slack value 239 reduced_costs: dict 240 maps id(Var) to (var, reduced_cost) 241 """ 242 self._primals = primals 243 self._duals = duals 244 self._slacks = slacks 245 self._reduced_costs = reduced_costs 246 247 def get_primals(self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None) -> Mapping[_GeneralVarData, float]: 248 if vars_to_load is None: 249 return ComponentMap(self._primals.values()) 250 else: 251 primals = ComponentMap() 252 for v in vars_to_load: 253 primals[v] = self._primals[id(v)][1] 254 255 def get_duals(self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None) -> Dict[_GeneralConstraintData, float]: 256 if cons_to_load is None: 257 duals = dict(self._duals) 258 else: 259 duals = dict() 260 for c in cons_to_load: 261 duals[c] = self._duals[c] 262 return duals 263 264 def get_slacks(self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None) -> Dict[_GeneralConstraintData, float]: 265 if cons_to_load is None: 266 slacks = dict(self._slacks) 267 else: 268 slacks = dict() 269 for c in cons_to_load: 270 slacks[c] = self._slacks[c] 271 return slacks 272 273 def get_reduced_costs(self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None) -> Mapping[_GeneralVarData, float]: 274 if vars_to_load is None: 275 rc = ComponentMap(self._reduced_costs.values()) 276 else: 277 rc = ComponentMap() 278 for v in vars_to_load: 279 rc[v] = self._reduced_costs[id(v)][1] 280 return rc 281 282 283class Results(object): 284 """ 285 Attributes 286 ---------- 287 termination_condition: TerminationCondition 288 The reason the solver exited. This is a member of the 289 TerminationCondition enum. 290 best_feasible_objective: float 291 If a feasible solution was found, this is the objective value of 292 the best solution found. If no feasible solution was found, this is 293 None. 294 best_objective_bound: float 295 The best objective bound found. For minimization problems, this is 296 the lower bound. For maximization problems, this is the upper bound. 297 For solvers that do not provide an objective bound, this should be -inf 298 (minimization) or inf (maximization) 299 300 Here is an example workflow: 301 302 >>> import pyomo.environ as pe 303 >>> from pyomo.contrib import appsi 304 >>> m = pe.ConcreteModel() 305 >>> m.x = pe.Var() 306 >>> m.obj = pe.Objective(expr=m.x**2) 307 >>> opt = appsi.solvers.Ipopt() 308 >>> opt.config.load_solution = False 309 >>> results = opt.solve(m) #doctest:+SKIP 310 >>> if results.termination_condition == appsi.base.TerminationCondition.optimal: #doctest:+SKIP 311 ... print('optimal solution found: ', results.best_feasible_objective) #doctest:+SKIP 312 ... results.solution_loader.load_vars() #doctest:+SKIP 313 ... print('the optimal value of x is ', m.x.value) #doctest:+SKIP 314 ... elif results.best_feasible_objective is not None: #doctest:+SKIP 315 ... print('sub-optimal but feasible solution found: ', results.best_feasible_objective) #doctest:+SKIP 316 ... results.solution_loader.load_vars(vars_to_load=[m.x]) #doctest:+SKIP 317 ... print('The value of x in the feasible solution is ', m.x.value) #doctest:+SKIP 318 ... elif results.termination_condition in {appsi.base.TerminationCondition.maxIterations, appsi.base.TerminationCondition.maxTimeLimit}: #doctest:+SKIP 319 ... print('No feasible solution was found. The best lower bound found was ', results.best_objective_bound) #doctest:+SKIP 320 ... else: #doctest:+SKIP 321 ... print('The following termination condition was encountered: ', results.termination_condition) #doctest:+SKIP 322 """ 323 def __init__(self): 324 self.solution_loader: Optional[SolutionLoaderBase] = None 325 self.termination_condition: TerminationCondition = TerminationCondition.unknown 326 self.best_feasible_objective: Optional[float] = None 327 self.best_objective_bound: Optional[float] = None 328 329 def __str__(self): 330 s = '' 331 s += 'termination_condition: ' + str(self.termination_condition) + '\n' 332 s += 'best_feasible_objective: ' + str(self.best_feasible_objective) + '\n' 333 s += 'best_objective_bound: ' + str(self.best_objective_bound) 334 return s 335 336 337class UpdateConfig(ConfigDict): 338 """ 339 Attributes 340 ---------- 341 check_for_new_or_removed_constraints: bool 342 check_for_new_or_removed_vars: bool 343 check_for_new_or_removed_params: bool 344 update_constraints: bool 345 update_vars: bool 346 update_params: bool 347 update_named_expressions: bool 348 """ 349 def __init__(self): 350 super(UpdateConfig, self).__init__() 351 352 self.declare('check_for_new_or_removed_constraints', ConfigValue(domain=bool)) 353 self.declare('check_for_new_or_removed_vars', ConfigValue(domain=bool)) 354 self.declare('check_for_new_or_removed_params', ConfigValue(domain=bool)) 355 self.declare('update_constraints', ConfigValue(domain=bool)) 356 self.declare('update_vars', ConfigValue(domain=bool)) 357 self.declare('update_params', ConfigValue(domain=bool)) 358 self.declare('update_named_expressions', ConfigValue(domain=bool)) 359 360 self.check_for_new_or_removed_constraints: bool = True 361 self.check_for_new_or_removed_vars: bool = True 362 self.check_for_new_or_removed_params: bool = True 363 self.update_constraints: bool = True 364 self.update_vars: bool = True 365 self.update_params: bool = True 366 self.update_named_expressions: bool = True 367 368 369class Solver(abc.ABC): 370 class Availability(enum.IntEnum): 371 NotFound = 0 372 BadVersion = -1 373 BadLicense = -2 374 FullLicense = 1 375 LimitedLicense = 2 376 377 def __bool__(self): 378 return self._value_ > 0 379 380 @abc.abstractmethod 381 def solve(self, model: _BlockData, timer: HierarchicalTimer = None) -> Results: 382 """ 383 Solve a Pyomo model. 384 385 Parameters 386 ---------- 387 model: _BlockData 388 The Pyomo model to be solved 389 timer: HierarchicalTimer 390 An option timer for reporting timing 391 392 Returns 393 ------- 394 results: Results 395 A results object 396 """ 397 pass 398 399 @abc.abstractmethod 400 def available(self): 401 """Test if the solver is available on this system. 402 403 Nominally, this will return True if the solver interface is 404 valid and can be used to solve problems and False if it cannot. 405 406 Note that for licensed solvers there are a number of "levels" of 407 available: depending on the license, the solver may be available 408 with limitations on problem size or runtime (e.g., 'demo' 409 vs. 'community' vs. 'full'). In these cases, the solver may 410 return a subclass of enum.IntEnum, with members that resolve to 411 True if the solver is available (possibly with limitations). 412 The Enum may also have multiple members that all resolve to 413 False indicating the reason why the interface is not available 414 (not found, bad license, unsupported version, etc). 415 416 Returns 417 ------- 418 available: Solver.Availability 419 An enum that indicates "how available" the solver is. 420 Note that the enum can be cast to bool, which will 421 be True if the solver is runable at all and False 422 otherwise. 423 """ 424 pass 425 426 @abc.abstractmethod 427 def version(self) -> Tuple: 428 """ 429 Returns 430 ------- 431 version: tuple 432 A tuple representing the version 433 """ 434 435 @property 436 @abc.abstractmethod 437 def config(self): 438 """ 439 An object for configuring solve options. 440 441 Returns 442 ------- 443 SolverConfig 444 An object for configuring pyomo solve options such as the time limit. 445 These options are mostly independent of the solver. 446 """ 447 pass 448 449 @property 450 @abc.abstractmethod 451 def symbol_map(self): 452 pass 453 454 def is_persistent(self): 455 """ 456 Returns 457 ------- 458 is_persistent: bool 459 True if the solver is a persistent solver. 460 """ 461 return False 462 463 464class PersistentSolver(Solver): 465 def is_persistent(self): 466 return True 467 468 def load_vars(self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None) -> NoReturn: 469 """ 470 Load the solution of the primal variables into the value attribut of the variables. 471 472 Parameters 473 ---------- 474 vars_to_load: list 475 A list of the variables whose solution should be loaded. If vars_to_load is None, then the solution 476 to all primal variables will be loaded. 477 """ 478 for v, val in self.get_primals(vars_to_load=vars_to_load).items(): 479 v.value = val 480 481 @abc.abstractmethod 482 def get_primals(self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None) -> Mapping[_GeneralVarData, float]: 483 pass 484 485 def get_duals(self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None) -> Dict[_GeneralConstraintData, float]: 486 """ 487 Declare sign convention in docstring here. 488 489 Parameters 490 ---------- 491 cons_to_load: list 492 A list of the constraints whose duals should be loaded. If cons_to_load is None, then the duals for all 493 constraints will be loaded. 494 495 Returns 496 ------- 497 duals: dict 498 Maps constraints to dual values 499 """ 500 raise NotImplementedError('{0} does not support the get_duals method'.format(type(self))) 501 502 def get_slacks(self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None) -> Dict[_GeneralConstraintData, float]: 503 """ 504 Parameters 505 ---------- 506 cons_to_load: list 507 A list of the constraints whose slacks should be loaded. If cons_to_load is None, then the slacks for all 508 constraints will be loaded. 509 510 Returns 511 ------- 512 slacks: dict 513 Maps constraints to slack values 514 """ 515 raise NotImplementedError('{0} does not support the get_slacks method'.format(type(self))) 516 517 def get_reduced_costs(self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None) -> Mapping[_GeneralVarData, float]: 518 """ 519 Parameters 520 ---------- 521 vars_to_load: list 522 A list of the variables whose reduced cost should be loaded. If vars_to_load is None, then all reduced costs 523 will be loaded. 524 525 Returns 526 ------- 527 reduced_costs: ComponentMap 528 Maps variable to reduced cost 529 """ 530 raise NotImplementedError('{0} does not support the get_reduced_costs method'.format(type(self))) 531 532 @property 533 @abc.abstractmethod 534 def update_config(self) -> UpdateConfig: 535 pass 536 537 @abc.abstractmethod 538 def set_instance(self, model): 539 pass 540 541 @abc.abstractmethod 542 def add_variables(self, variables: List[_GeneralVarData]): 543 pass 544 545 @abc.abstractmethod 546 def add_params(self, params: List[_ParamData]): 547 pass 548 549 @abc.abstractmethod 550 def add_constraints(self, cons: List[_GeneralConstraintData]): 551 pass 552 553 @abc.abstractmethod 554 def add_block(self, block: _BlockData): 555 pass 556 557 @abc.abstractmethod 558 def remove_variables(self, variables: List[_GeneralVarData]): 559 pass 560 561 @abc.abstractmethod 562 def remove_params(self, params: List[_ParamData]): 563 pass 564 565 @abc.abstractmethod 566 def remove_constraints(self, cons: List[_GeneralConstraintData]): 567 pass 568 569 @abc.abstractmethod 570 def remove_block(self, block: _BlockData): 571 pass 572 573 @abc.abstractmethod 574 def set_objective(self, obj: _GeneralObjectiveData): 575 pass 576 577 @abc.abstractmethod 578 def update_variables(self, variables: List[_GeneralVarData]): 579 pass 580 581 @abc.abstractmethod 582 def update_params(self): 583 pass 584 585 586class PersistentSolutionLoader(SolutionLoaderBase): 587 def __init__(self, solver: PersistentSolver): 588 self._solver = solver 589 self._valid = True 590 591 def _assert_solution_still_valid(self): 592 if not self._valid: 593 raise RuntimeError('The results in the solver are no longer valid.') 594 595 def get_primals(self, vars_to_load=None): 596 self._assert_solution_still_valid() 597 return self._solver.get_primals(vars_to_load=vars_to_load) 598 599 def get_duals(self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None) -> Dict[_GeneralConstraintData, float]: 600 self._assert_solution_still_valid() 601 return self._solver.get_duals(cons_to_load=cons_to_load) 602 603 def get_slacks(self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None) -> Dict[_GeneralConstraintData, float]: 604 self._assert_solution_still_valid() 605 return self._solver.get_slacks(cons_to_load=cons_to_load) 606 607 def get_reduced_costs(self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None) -> Mapping[_GeneralVarData, float]: 608 self._assert_solution_still_valid() 609 return self._solver.get_reduced_costs(vars_to_load=vars_to_load) 610 611 def invalidate(self): 612 self._valid = False 613 614 615""" 616What can change in a pyomo model? 617- variables added or removed 618- constraints added or removed 619- objective changed 620- objective expr changed 621- params added or removed 622- variable modified 623 - lb 624 - ub 625 - fixed or unfixed 626 - domain 627 - value 628- constraint modified 629 - lower 630 - upper 631 - body 632 - active or not 633- named expressions modified 634 - expr 635- param modified 636 - value 637 638Ideas: 639- Consider explicitly handling deactivated constraints; favor deactivation over removal 640 and activation over addition 641 642Notes: 643- variable bounds cannot be updated with mutable params; you must call update_variables 644""" 645 646 647class PersistentBase(abc.ABC): 648 def __init__(self): 649 self._model = None 650 self._active_constraints = dict() # maps constraint to (lower, body, upper) 651 self._vars = dict() # maps var id to (var, lb, ub, fixed, domain, value) 652 self._params = dict() # maps param id to param 653 self._objective = None 654 self._objective_expr = None 655 self._objective_sense = None 656 self._named_expressions = dict() # maps constraint to list of tuples (named_expr, named_expr.expr) 657 self._obj_named_expressions = list() 658 self._update_config = UpdateConfig() 659 self._referenced_variables = dict() # number of constraints/objectives each variable is used in 660 self._vars_referenced_by_con = dict() 661 self._vars_referenced_by_obj = list() 662 663 @property 664 def update_config(self): 665 return self._update_config 666 667 @update_config.setter 668 def update_config(self, val: UpdateConfig): 669 self._update_config = val 670 671 def set_instance(self, model): 672 saved_update_config = self.update_config 673 self.__init__() 674 self.update_config = saved_update_config 675 self._model = model 676 self.add_block(model) 677 if self._objective is None: 678 self.set_objective(None) 679 680 @abc.abstractmethod 681 def _add_variables(self, variables: List[_GeneralVarData]): 682 pass 683 684 def add_variables(self, variables: List[_GeneralVarData]): 685 for v in variables: 686 if id(v) in self._referenced_variables: 687 raise ValueError('variable {name} has already been added'.format(name=v.name)) 688 self._referenced_variables[id(v)] = 0 689 self._vars[id(v)] = (v, v.lb, v.ub, v.is_fixed(), v.domain, v.value) 690 self._add_variables(variables) 691 692 @abc.abstractmethod 693 def _add_params(self, params: List[_ParamData]): 694 pass 695 696 def add_params(self, params: List[_ParamData]): 697 for p in params: 698 self._params[id(p)] = p 699 self._add_params(params) 700 701 @abc.abstractmethod 702 def _add_constraints(self, cons: List[_GeneralConstraintData]): 703 pass 704 705 def add_constraints(self, cons: List[_GeneralConstraintData]): 706 all_fixed_vars = dict() 707 for con in cons: 708 if con in self._named_expressions: 709 raise ValueError('constraint {name} has already been added'.format(name=con.name)) 710 self._active_constraints[con] = (con.lower, con.body, con.upper) 711 named_exprs, variables, fixed_vars = identify_named_expressions(con.body) 712 self._named_expressions[con] = [(e, e.expr) for e in named_exprs] 713 self._vars_referenced_by_con[con] = variables 714 for v in variables: 715 self._referenced_variables[id(v)] += 1 716 for v in fixed_vars: 717 v.unfix() 718 all_fixed_vars[id(v)] = v 719 self._add_constraints(cons) 720 for v in all_fixed_vars.values(): 721 v.fix() 722 723 @abc.abstractmethod 724 def _add_sos_constraints(self, cons: List[_SOSConstraintData]): 725 pass 726 727 def add_sos_constraints(self, cons: List[_SOSConstraintData]): 728 for con in cons: 729 if con in self._vars_referenced_by_con: 730 raise ValueError('constraint {name} has already been added'.format(name=con.name)) 731 self._active_constraints[con] = tuple() 732 variables = con.get_variables() 733 self._named_expressions[con] = list() 734 self._vars_referenced_by_con[con] = variables 735 for v in variables: 736 self._referenced_variables[id(v)] += 1 737 self._add_sos_constraints(cons) 738 739 @abc.abstractmethod 740 def _set_objective(self, obj: _GeneralObjectiveData): 741 pass 742 743 def set_objective(self, obj: _GeneralObjectiveData): 744 if self._objective is not None: 745 for v in self._vars_referenced_by_obj: 746 self._referenced_variables[id(v)] -= 1 747 if obj is not None: 748 self._objective = obj 749 self._objective_expr = obj.expr 750 self._objective_sense = obj.sense 751 named_exprs, variables, fixed_vars = identify_named_expressions(obj.expr) 752 self._obj_named_expressions = [(i, i.expr) for i in named_exprs] 753 self._vars_referenced_by_obj = variables 754 for v in variables: 755 self._referenced_variables[id(v)] += 1 756 for v in fixed_vars: 757 v.unfix() 758 self._set_objective(obj) 759 for v in fixed_vars: 760 v.fix() 761 else: 762 self._vars_referenced_by_obj = list() 763 self._objective = None 764 self._objective_expr = None 765 self._objective_sense = None 766 self._obj_named_expressions = list() 767 self._set_objective(obj) 768 769 def add_block(self, block): 770 self.add_variables([var for var in block.component_data_objects(Var, descend_into=True, sort=False)]) 771 self.add_params([p for p in block.component_data_objects(Param, descend_into=True, sort=False)]) 772 self.add_constraints([con for con in block.component_data_objects(Constraint, descend_into=True, 773 active=True, sort=False)]) 774 self.add_sos_constraints([con for con in block.component_data_objects(SOSConstraint, descend_into=True, 775 active=True, sort=False)]) 776 obj = get_objective(block) 777 if obj is not None: 778 self.set_objective(obj) 779 780 @abc.abstractmethod 781 def _remove_constraints(self, cons: List[_GeneralConstraintData]): 782 pass 783 784 def remove_constraints(self, cons: List[_GeneralConstraintData]): 785 self._remove_constraints(cons) 786 for con in cons: 787 if con not in self._named_expressions: 788 raise ValueError('cannot remove constraint {name} - it was not added'.format(name=con.name)) 789 for v in self._vars_referenced_by_con[con]: 790 self._referenced_variables[id(v)] -= 1 791 del self._active_constraints[con] 792 del self._named_expressions[con] 793 del self._vars_referenced_by_con[con] 794 795 @abc.abstractmethod 796 def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): 797 pass 798 799 def remove_sos_constraints(self, cons: List[_SOSConstraintData]): 800 self._remove_sos_constraints(cons) 801 for con in cons: 802 if con not in self._vars_referenced_by_con: 803 raise ValueError('cannot remove constraint {name} - it was not added'.format(name=con.name)) 804 for v in self._vars_referenced_by_con[con]: 805 self._referenced_variables[id(v)] -= 1 806 del self._active_constraints[con] 807 del self._named_expressions[con] 808 del self._vars_referenced_by_con[con] 809 810 @abc.abstractmethod 811 def _remove_variables(self, variables: List[_GeneralVarData]): 812 pass 813 814 def remove_variables(self, variables: List[_GeneralVarData]): 815 self._remove_variables(variables) 816 for v in variables: 817 if id(v) not in self._referenced_variables: 818 raise ValueError('cannot remove variable {name} - it has not been added'.format(name=v.name)) 819 if self._referenced_variables[id(v)] != 0: 820 raise ValueError('cannot remove variable {name} - it is still being used by constraints or the objective'.format(name=v.name)) 821 del self._referenced_variables[id(v)] 822 del self._vars[id(v)] 823 824 @abc.abstractmethod 825 def _remove_params(self, params: List[_ParamData]): 826 pass 827 828 def remove_params(self, params: List[_ParamData]): 829 self._remove_params(params) 830 for p in params: 831 del self._params[id(p)] 832 833 def remove_block(self, block): 834 self.remove_constraints([con for con in block.component_data_objects(ctype=Constraint, descend_into=True, 835 active=True, sort=False)]) 836 self.remove_sos_constraints([con for con in block.component_data_objects(ctype=SOSConstraint, descend_into=True, 837 active=True, sort=False)]) 838 self.remove_variables([var for var in block.component_data_objects(ctype=Var, descend_into=True, sort=False)]) 839 self.remove_params([p for p in block.component_data_objects(ctype=Param, descend_into=True, sort=False)]) 840 841 @abc.abstractmethod 842 def _update_variables(self, variables: List[_GeneralVarData]): 843 pass 844 845 def update_variables(self, variables: List[_GeneralVarData]): 846 for v in variables: 847 self._vars[id(v)] = (v, v.lb, v.ub, v.is_fixed(), v.domain, v.value) 848 self._update_variables(variables) 849 850 @abc.abstractmethod 851 def update_params(self): 852 pass 853 854 def solve_sub_block(self, block): 855 raise NotImplementedError('This is just an idea right now') 856 857 def update(self, timer: HierarchicalTimer = None): 858 if timer is None: 859 timer = HierarchicalTimer() 860 config = self.update_config 861 new_vars = list() 862 old_vars = list() 863 new_params = list() 864 old_params = list() 865 new_cons = list() 866 old_cons = list() 867 old_sos = list() 868 new_sos = list() 869 current_vars_dict = dict() 870 current_cons_dict = dict() 871 current_sos_dict = dict() 872 timer.start('vars') 873 if config.check_for_new_or_removed_vars or config.update_vars: 874 current_vars_dict = {id(v): v for v in self._model.component_data_objects(Var, descend_into=True, sort=False)} 875 for v_id, v in current_vars_dict.items(): 876 if v_id not in self._vars: 877 new_vars.append(v) 878 for v_id, v_tuple in self._vars.items(): 879 if v_id not in current_vars_dict: 880 old_vars.append(v_tuple[0]) 881 timer.stop('vars') 882 timer.start('params') 883 if config.check_for_new_or_removed_params: 884 current_params_dict = {id(p): p for p in self._model.component_data_objects(Param, descend_into=True, sort=False)} 885 for p_id, p in current_params_dict.items(): 886 if p_id not in self._params: 887 new_params.append(p) 888 for p_id, p in self._params.items(): 889 if p_id not in current_params_dict: 890 old_params.append(p) 891 timer.stop('params') 892 timer.start('cons') 893 if config.check_for_new_or_removed_constraints or config.update_constraints: 894 current_cons_dict = {c: None for c in self._model.component_data_objects(Constraint, descend_into=True, active=True, sort=False)} 895 current_sos_dict = {c: None for c in self._model.component_data_objects(SOSConstraint, descend_into=True, active=True, sort=False)} 896 for c in current_cons_dict.keys(): 897 if c not in self._vars_referenced_by_con: 898 new_cons.append(c) 899 for c in current_sos_dict.keys(): 900 if c not in self._vars_referenced_by_con: 901 new_sos.append(c) 902 for c in self._vars_referenced_by_con.keys(): 903 if c not in current_cons_dict and c not in current_sos_dict: 904 if (c.ctype is Constraint) or (c.ctype is None and isinstance(c, _GeneralConstraintData)): 905 old_cons.append(c) 906 else: 907 assert (c.ctype is SOSConstraint) or (c.ctype is None and isinstance(c, _SOSConstraintData)) 908 old_sos.append(c) 909 self.remove_constraints(old_cons) 910 self.remove_sos_constraints(old_sos) 911 timer.stop('cons') 912 timer.start('vars') 913 self.remove_variables(old_vars) 914 timer.stop('vars') 915 timer.start('params') 916 self.remove_params(old_params) 917 918 # sticking this between removal and addition 919 # is important so that we don't do unnecessary work 920 if config.update_params: 921 self.update_params() 922 923 self.add_params(new_params) 924 timer.stop('params') 925 timer.start('vars') 926 self.add_variables(new_vars) 927 timer.stop('vars') 928 timer.start('cons') 929 self.add_constraints(new_cons) 930 self.add_sos_constraints(new_sos) 931 new_cons_set = set(new_cons) 932 new_sos_set = set(new_sos) 933 new_vars_set = set(id(v) for v in new_vars) 934 if config.update_constraints: 935 cons_to_update = list() 936 sos_to_update = list() 937 for c in current_cons_dict.keys(): 938 if c not in new_cons_set: 939 cons_to_update.append(c) 940 for c in current_sos_dict.keys(): 941 if c not in new_sos_set: 942 sos_to_update.append(c) 943 cons_to_remove_and_add = list() 944 for c in cons_to_update: 945 lower, body, upper = self._active_constraints[c] 946 if c.lower is not lower or c.body is not body or c.upper is not upper: 947 cons_to_remove_and_add.append(c) 948 self.remove_constraints(cons_to_remove_and_add) 949 self.add_constraints(cons_to_remove_and_add) 950 self.remove_sos_constraints(sos_to_update) 951 self.add_sos_constraints(sos_to_update) 952 timer.stop('cons') 953 timer.start('vars') 954 if config.update_vars: 955 vars_to_check = list() 956 for v_id, v in current_vars_dict.items(): 957 if v_id not in new_vars_set: 958 vars_to_check.append(v) 959 vars_to_update = list() 960 for v in vars_to_check: 961 _v, lb, ub, fixed, domain, value = self._vars[id(v)] 962 if lb is not v.lb: 963 vars_to_update.append(v) 964 elif ub is not v.ub: 965 vars_to_update.append(v) 966 elif fixed is not v.is_fixed(): 967 vars_to_update.append(v) 968 elif domain is not v.domain: 969 vars_to_update.append(v) 970 elif fixed and (value is not v.value): 971 vars_to_update.append(v) 972 self.update_variables(vars_to_update) 973 timer.stop('vars') 974 timer.start('named expressions') 975 if config.update_named_expressions: 976 cons_to_update = list() 977 for c, expr_list in self._named_expressions.items(): 978 if c in new_cons_set: 979 continue 980 for named_expr, old_expr in expr_list: 981 if named_expr.expr is not old_expr: 982 cons_to_update.append(c) 983 break 984 self.remove_constraints(cons_to_update) 985 self.add_constraints(cons_to_update) 986 timer.stop('named expressions') 987 timer.start('objective') 988 pyomo_obj = get_objective(self._model) 989 need_to_set_objective = False 990 if pyomo_obj is not self._objective: 991 need_to_set_objective = True 992 elif pyomo_obj is not None and pyomo_obj.expr is not self._objective_expr: 993 need_to_set_objective = True 994 elif pyomo_obj is not None and pyomo_obj.sense is not self._objective_sense: 995 need_to_set_objective = True 996 elif config.update_named_expressions: 997 for named_expr, old_expr in self._obj_named_expressions: 998 if named_expr.expr is not old_expr: 999 need_to_set_objective = True 1000 break 1001 if need_to_set_objective: 1002 self.set_objective(pyomo_obj) 1003 timer.stop('objective') 1004 1005 1006legacy_termination_condition_map = { 1007 TerminationCondition.unknown: LegacyTerminationCondition.unknown, 1008 TerminationCondition.maxTimeLimit: LegacyTerminationCondition.maxTimeLimit, 1009 TerminationCondition.maxIterations: LegacyTerminationCondition.maxIterations, 1010 TerminationCondition.objectiveLimit: LegacyTerminationCondition.minFunctionValue, 1011 TerminationCondition.minStepLength: LegacyTerminationCondition.minStepLength, 1012 TerminationCondition.optimal: LegacyTerminationCondition.optimal, 1013 TerminationCondition.unbounded: LegacyTerminationCondition.unbounded, 1014 TerminationCondition.infeasible: LegacyTerminationCondition.infeasible, 1015 TerminationCondition.infeasibleOrUnbounded: LegacyTerminationCondition.infeasibleOrUnbounded, 1016 TerminationCondition.error: LegacyTerminationCondition.error, 1017 TerminationCondition.interrupted: LegacyTerminationCondition.resourceInterrupt, 1018 TerminationCondition.licensingProblems: LegacyTerminationCondition.licensingProblems 1019} 1020 1021 1022legacy_solver_status_map = { 1023 TerminationCondition.unknown: LegacySolverStatus.unknown, 1024 TerminationCondition.maxTimeLimit: LegacySolverStatus.aborted, 1025 TerminationCondition.maxIterations: LegacySolverStatus.aborted, 1026 TerminationCondition.objectiveLimit: LegacySolverStatus.aborted, 1027 TerminationCondition.minStepLength: LegacySolverStatus.error, 1028 TerminationCondition.optimal: LegacySolverStatus.ok, 1029 TerminationCondition.unbounded: LegacySolverStatus.error, 1030 TerminationCondition.infeasible: LegacySolverStatus.error, 1031 TerminationCondition.infeasibleOrUnbounded: LegacySolverStatus.error, 1032 TerminationCondition.error: LegacySolverStatus.error, 1033 TerminationCondition.interrupted: LegacySolverStatus.aborted, 1034 TerminationCondition.licensingProblems: LegacySolverStatus.error 1035} 1036 1037 1038legacy_solution_status_map = { 1039 TerminationCondition.unknown: LegacySolutionStatus.unknown, 1040 TerminationCondition.maxTimeLimit: LegacySolutionStatus.stoppedByLimit, 1041 TerminationCondition.maxIterations: LegacySolutionStatus.stoppedByLimit, 1042 TerminationCondition.objectiveLimit: LegacySolutionStatus.stoppedByLimit, 1043 TerminationCondition.minStepLength: LegacySolutionStatus.error, 1044 TerminationCondition.optimal: LegacySolutionStatus.optimal, 1045 TerminationCondition.unbounded: LegacySolutionStatus.unbounded, 1046 TerminationCondition.infeasible: LegacySolutionStatus.infeasible, 1047 TerminationCondition.infeasibleOrUnbounded: LegacySolutionStatus.unsure, 1048 TerminationCondition.error: LegacySolutionStatus.error, 1049 TerminationCondition.interrupted: LegacySolutionStatus.error, 1050 TerminationCondition.licensingProblems: LegacySolutionStatus.error 1051} 1052 1053 1054class LegacySolverInterface(object): 1055 def solve(self, 1056 model: _BlockData, 1057 tee: bool = False, 1058 load_solutions: bool = True, 1059 logfile: Optional[str] = None, 1060 solnfile: Optional[str] = None, 1061 timelimit: Optional[float] = None, 1062 report_timing: bool = False, 1063 solver_io: Optional[str] = None, 1064 suffixes: Optional[Sequence] = None, 1065 options: Optional[Dict] = None, 1066 keepfiles: bool = False, 1067 symbolic_solver_labels: bool = False): 1068 original_config = self.config 1069 self.config = self.config() 1070 self.config.stream_solver = tee 1071 self.config.load_solution = load_solutions 1072 self.config.symbolic_solver_labels = symbolic_solver_labels 1073 self.config.time_limit = timelimit 1074 self.config.report_timing = report_timing 1075 if solver_io is not None: 1076 raise NotImplementedError('Still working on this') 1077 if suffixes is not None: 1078 raise NotImplementedError('Still working on this') 1079 if logfile is not None: 1080 raise NotImplementedError('Still working on this') 1081 if 'keepfiles' in self.config: 1082 self.config.keepfiles = keepfiles 1083 if solnfile is not None: 1084 if 'filename' in self.config: 1085 filename = os.path.splitext(solnfile)[0] 1086 self.config.filename = filename 1087 original_options = self.options 1088 if options is not None: 1089 self.options = options 1090 1091 results: Results = super(LegacySolverInterface, self).solve(model) 1092 1093 legacy_results = LegacySolverResults() 1094 legacy_soln = LegacySolution() 1095 legacy_results.solver.status = legacy_solver_status_map[results.termination_condition] 1096 legacy_results.solver.termination_condition = legacy_termination_condition_map[results.termination_condition] 1097 legacy_soln.status = legacy_solution_status_map[results.termination_condition] 1098 legacy_results.solver.termination_message = str(results.termination_condition) 1099 1100 obj = get_objective(model) 1101 legacy_results.problem.sense = obj.sense 1102 1103 if obj.sense == minimize: 1104 legacy_results.problem.lower_bound = results.best_objective_bound 1105 legacy_results.problem.upper_bound = results.best_feasible_objective 1106 else: 1107 legacy_results.problem.upper_bound = results.best_objective_bound 1108 legacy_results.problem.lower_bound = results.best_feasible_objective 1109 if results.best_feasible_objective is not None and results.best_objective_bound is not None: 1110 legacy_soln.gap = abs(results.best_feasible_objective - results.best_objective_bound) 1111 else: 1112 legacy_soln.gap = None 1113 1114 symbol_map = SymbolMap() 1115 symbol_map.byObject = dict(self.symbol_map.byObject) 1116 symbol_map.bySymbol = {symb: weakref.ref(obj()) for symb, obj in self.symbol_map.bySymbol.items()} 1117 symbol_map.aliases = {symb: weakref.ref(obj()) for symb, obj in self.symbol_map.aliases.items()} 1118 symbol_map.default_labeler = self.symbol_map.default_labeler 1119 model.solutions.add_symbol_map(symbol_map) 1120 legacy_results._smap_id = id(symbol_map) 1121 1122 delete_legacy_soln = True 1123 if load_solutions: 1124 if hasattr(model, 'dual') and model.dual.import_enabled(): 1125 for c, val in results.solution_loader.get_duals().items(): 1126 model.dual[c] = val 1127 if hasattr(model, 'slack') and model.slack.import_enabled(): 1128 for c, val in results.solution_loader.get_slacks().items(): 1129 model.slack[c] = val 1130 if hasattr(model, 'rc') and model.rc.import_enabled(): 1131 for v, val in results.solution_loader.get_reduced_costs().items(): 1132 model.rc[v] = val 1133 elif results.best_feasible_objective is not None: 1134 delete_legacy_soln = False 1135 for v, val in results.solution_loader.get_primals().items(): 1136 legacy_soln.variable[symbol_map.getSymbol(v)] = {'Value': val} 1137 if hasattr(model, 'dual') and model.dual.import_enabled(): 1138 for c, val in results.solution_loader.get_duals().items(): 1139 legacy_soln.constraint[symbol_map.getSymbol(c)] = {'Dual': val} 1140 if hasattr(model, 'slack') and model.slack.import_enabled(): 1141 for c, val in results.solution_loader.get_slacks().items(): 1142 symbol = symbol_map.getSymbol(c) 1143 if symbol in legacy_soln.constraint: 1144 legacy_soln.constraint[symbol]['Slack'] = val 1145 if hasattr(model, 'rc') and model.rc.import_enabled(): 1146 for v, val in results.solution_loader.get_reduced_costs().items(): 1147 legacy_soln.variable['Rc'] = val 1148 1149 legacy_results.solution.insert(legacy_soln) 1150 if delete_legacy_soln: 1151 legacy_results.solution.delete(0) 1152 1153 self.config = original_config 1154 self.options = original_options 1155 1156 return legacy_results 1157 1158 def available(self, exception_flag=True): 1159 ans = super(LegacySolverInterface, self).available() 1160 if exception_flag and not ans: 1161 raise ApplicationError(f'Solver {self.__class__} is not available ({ans}).') 1162 return bool(ans) 1163 1164 def license_is_valid(self) -> bool: 1165 """Test if the solver license is valid on this system. 1166 1167 Note that this method is included for compatibility with the 1168 legacy SolverFactory interface. Unlicensed or open source 1169 solvers will return True by definition. Licensed solvers will 1170 return True if a valid license is found. 1171 1172 Returns 1173 ------- 1174 available: bool 1175 True if the solver license is valid. Otherwise, False. 1176 1177 """ 1178 return bool(self.available()) 1179 1180 @property 1181 def options(self): 1182 for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc']: 1183 if hasattr(self, solver_name + '_options'): 1184 return getattr(self, solver_name + '_options') 1185 raise NotImplementedError('Could not find the correct options') 1186 1187 @options.setter 1188 def options(self, val): 1189 found = False 1190 for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc']: 1191 if hasattr(self, solver_name + '_options'): 1192 setattr(self, solver_name + '_options', val) 1193 found = True 1194 if not found: 1195 raise NotImplementedError('Could not find the correct options') 1196 1197 1198 def __enter__(self): 1199 return self 1200 1201 def __exit__(self, t, v, traceback): 1202 pass 1203 1204 1205class SolverFactoryClass(Factory): 1206 def register(self, name, doc=None): 1207 def decorator(cls): 1208 self._cls[name] = cls 1209 self._doc[name] = doc 1210 1211 class LegacySolver(LegacySolverInterface, cls): 1212 pass 1213 LegacySolverFactory.register(name, doc)(LegacySolver) 1214 1215 return cls 1216 return decorator 1217 1218 1219SolverFactory = SolverFactoryClass() 1220