ortools.math_opt.python.result
The output from solving a mathematical optimization problem from model.py.
1# Copyright 2010-2025 Google LLC 2# Licensed under the Apache License, Version 2.0 (the "License"); 3# you may not use this file except in compliance with the License. 4# You may obtain a copy of the License at 5# 6# http://www.apache.org/licenses/LICENSE-2.0 7# 8# Unless required by applicable law or agreed to in writing, software 9# distributed under the License is distributed on an "AS IS" BASIS, 10# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11# See the License for the specific language governing permissions and 12# limitations under the License. 13 14"""The output from solving a mathematical optimization problem from model.py.""" 15import dataclasses 16import datetime 17import enum 18from typing import Dict, Iterable, List, Optional, overload 19 20from ortools.gscip import gscip_pb2 21from ortools.math_opt import result_pb2 22from ortools.math_opt.python import model 23from ortools.math_opt.python import solution 24from ortools.math_opt.solvers import osqp_pb2 25 26_NO_DUAL_SOLUTION_ERROR = ( 27 "Best solution does not have an associated dual feasible solution." 28) 29_NO_BASIS_ERROR = "Best solution does not have an associated basis." 30 31 32@enum.unique 33class FeasibilityStatus(enum.Enum): 34 """Problem feasibility status as claimed by the solver. 35 36 (solver is not required to return a certificate for the claim.) 37 38 Attributes: 39 UNDETERMINED: Solver does not claim a status. 40 FEASIBLE: Solver claims the problem is feasible. 41 INFEASIBLE: Solver claims the problem is infeasible. 42 """ 43 44 UNDETERMINED = result_pb2.FEASIBILITY_STATUS_UNDETERMINED 45 FEASIBLE = result_pb2.FEASIBILITY_STATUS_FEASIBLE 46 INFEASIBLE = result_pb2.FEASIBILITY_STATUS_INFEASIBLE 47 48 49@dataclasses.dataclass(frozen=True) 50class ProblemStatus: 51 """Feasibility status of the primal problem and its dual (or dual relaxation). 52 53 Statuses are as claimed by the solver and a dual relaxation is the dual of a 54 continuous relaxation for the original problem (e.g. the LP relaxation of a 55 MIP). The solver is not required to return a certificate for the feasibility 56 or infeasibility claims (e.g. the solver may claim primal feasibility without 57 returning a primal feasible solutuion). This combined status gives a 58 comprehensive description of a solver's claims about feasibility and 59 unboundedness of the solved problem. For instance, 60 * a feasible status for primal and dual problems indicates the primal is 61 feasible and bounded and likely has an optimal solution (guaranteed for 62 problems without non-linear constraints). 63 * a primal feasible and a dual infeasible status indicates the primal 64 problem is unbounded (i.e. has arbitrarily good solutions). 65 Note that a dual infeasible status by itself (i.e. accompanied by an 66 undetermined primal status) does not imply the primal problem is unbounded as 67 we could have both problems be infeasible. Also, while a primal and dual 68 feasible status may imply the existence of an optimal solution, it does not 69 guarantee the solver has actually found such optimal solution. 70 71 Attributes: 72 primal_status: Status for the primal problem. 73 dual_status: Status for the dual problem (or for the dual of a continuous 74 relaxation). 75 primal_or_dual_infeasible: If true, the solver claims the primal or dual 76 problem is infeasible, but it does not know which (or if both are 77 infeasible). Can be true only when primal_problem_status = 78 dual_problem_status = kUndetermined. This extra information is often 79 needed when preprocessing determines there is no optimal solution to the 80 problem (but can't determine if it is due to infeasibility, unboundedness, 81 or both). 82 """ 83 84 primal_status: FeasibilityStatus = FeasibilityStatus.UNDETERMINED 85 dual_status: FeasibilityStatus = FeasibilityStatus.UNDETERMINED 86 primal_or_dual_infeasible: bool = False 87 88 def to_proto(self) -> result_pb2.ProblemStatusProto: 89 """Returns an equivalent proto for a problem status.""" 90 return result_pb2.ProblemStatusProto( 91 primal_status=self.primal_status.value, 92 dual_status=self.dual_status.value, 93 primal_or_dual_infeasible=self.primal_or_dual_infeasible, 94 ) 95 96 97def parse_problem_status(proto: result_pb2.ProblemStatusProto) -> ProblemStatus: 98 """Returns an equivalent ProblemStatus from the input proto.""" 99 primal_status_proto = proto.primal_status 100 if primal_status_proto == result_pb2.FEASIBILITY_STATUS_UNSPECIFIED: 101 raise ValueError("Primal feasibility status should not be UNSPECIFIED") 102 dual_status_proto = proto.dual_status 103 if dual_status_proto == result_pb2.FEASIBILITY_STATUS_UNSPECIFIED: 104 raise ValueError("Dual feasibility status should not be UNSPECIFIED") 105 return ProblemStatus( 106 primal_status=FeasibilityStatus(primal_status_proto), 107 dual_status=FeasibilityStatus(dual_status_proto), 108 primal_or_dual_infeasible=proto.primal_or_dual_infeasible, 109 ) 110 111 112@dataclasses.dataclass(frozen=True) 113class ObjectiveBounds: 114 """Bounds on the optimal objective value. 115 116 MOE:begin_intracomment_strip 117 See go/mathopt-objective-bounds for more details. 118 MOE:end_intracomment_strip 119 120 Attributes: 121 primal_bound: Solver claims there exists a primal solution that is 122 numerically feasible (i.e. feasible up to the solvers tolerance), and 123 whose objective value is primal_bound. 124 125 The optimal value is equal or better (smaller for min objectives and 126 larger for max objectives) than primal_bound, but only up to 127 solver-tolerances. 128 129 MOE:begin_intracomment_strip 130 See go/mathopt-objective-bounds for more details. 131 MOE:end_intracomment_strip 132 dual_bound: Solver claims there exists a dual solution that is numerically 133 feasible (i.e. feasible up to the solvers tolerance), and whose objective 134 value is dual_bound. 135 136 For MIP solvers, the associated dual problem may be some continuous 137 relaxation (e.g. LP relaxation), but it is often an implicitly defined 138 problem that is a complex consequence of the solvers execution. For both 139 continuous and MIP solvers, the optimal value is equal or worse (larger 140 for min objective and smaller for max objectives) than dual_bound, but 141 only up to solver-tolerances. Some continuous solvers provide a 142 numerically safer dual bound through solver's specific output (e.g. for 143 PDLP, pdlp_output.convergence_information.corrected_dual_objective). 144 145 MOE:begin_intracomment_strip 146 See go/mathopt-objective-bounds for more details. 147 MOE:end_intracomment_strip 148 """ # fmt: skip 149 150 primal_bound: float = 0.0 151 dual_bound: float = 0.0 152 153 def to_proto(self) -> result_pb2.ObjectiveBoundsProto: 154 """Returns an equivalent proto for objective bounds.""" 155 return result_pb2.ObjectiveBoundsProto( 156 primal_bound=self.primal_bound, dual_bound=self.dual_bound 157 ) 158 159 160def parse_objective_bounds( 161 proto: result_pb2.ObjectiveBoundsProto, 162) -> ObjectiveBounds: 163 """Returns an equivalent ObjectiveBounds from the input proto.""" 164 return ObjectiveBounds(primal_bound=proto.primal_bound, dual_bound=proto.dual_bound) 165 166 167@dataclasses.dataclass 168class SolveStats: 169 """Problem statuses and solve statistics returned by the solver. 170 171 Attributes: 172 solve_time: Elapsed wall clock time as measured by math_opt, roughly the 173 time inside solve(). Note: this does not include work done building the 174 model. 175 simplex_iterations: Simplex iterations. 176 barrier_iterations: Barrier iterations. 177 first_order_iterations: First order iterations. 178 node_count: Node count. 179 """ 180 181 solve_time: datetime.timedelta = datetime.timedelta() 182 simplex_iterations: int = 0 183 barrier_iterations: int = 0 184 first_order_iterations: int = 0 185 node_count: int = 0 186 187 def to_proto(self) -> result_pb2.SolveStatsProto: 188 """Returns an equivalent proto for a solve stats.""" 189 result = result_pb2.SolveStatsProto( 190 simplex_iterations=self.simplex_iterations, 191 barrier_iterations=self.barrier_iterations, 192 first_order_iterations=self.first_order_iterations, 193 node_count=self.node_count, 194 ) 195 result.solve_time.FromTimedelta(self.solve_time) 196 return result 197 198 199def parse_solve_stats(proto: result_pb2.SolveStatsProto) -> SolveStats: 200 """Returns an equivalent SolveStats from the input proto.""" 201 result = SolveStats() 202 result.solve_time = proto.solve_time.ToTimedelta() 203 result.simplex_iterations = proto.simplex_iterations 204 result.barrier_iterations = proto.barrier_iterations 205 result.first_order_iterations = proto.first_order_iterations 206 result.node_count = proto.node_count 207 return result 208 209 210@enum.unique 211class TerminationReason(enum.Enum): 212 """The reason a solve of a model terminated. 213 214 These reasons are typically as reported by the underlying solver, e.g. we do 215 not attempt to verify the precision of the solution returned. 216 217 The values are: 218 * OPTIMAL: A provably optimal solution (up to numerical tolerances) has 219 been found. 220 * INFEASIBLE: The primal problem has no feasible solutions. 221 * UNBOUNDED: The primal problem is feasible and arbitrarily good solutions 222 can be found along a primal ray. 223 * INFEASIBLE_OR_UNBOUNDED: The primal problem is either infeasible or 224 unbounded. More details on the problem status may be available in 225 solve_stats.problem_status. Note that Gurobi's unbounded status may be 226 mapped here as explained in 227 go/mathopt-solver-specific#gurobi-inf-or-unb. 228 * IMPRECISE: The problem was solved to one of the criteria above (Optimal, 229 Infeasible, Unbounded, or InfeasibleOrUnbounded), but one or more 230 tolerances was not met. Some primal/dual solutions/rays may be present, 231 but either they will be slightly infeasible, or (if the problem was 232 nearly optimal) their may be a gap between the best solution objective 233 and best objective bound. 234 235 Users can still query primal/dual solutions/rays and solution stats, 236 but they are responsible for dealing with the numerical imprecision. 237 * FEASIBLE: The optimizer reached some kind of limit and a primal feasible 238 solution is returned. See SolveResultProto.limit_detail for detailed 239 description of the kind of limit that was reached. 240 * NO_SOLUTION_FOUND: The optimizer reached some kind of limit and it did 241 not find a primal feasible solution. See SolveResultProto.limit_detail 242 for detailed description of the kind of limit that was reached. 243 * NUMERICAL_ERROR: The algorithm stopped because it encountered 244 unrecoverable numerical error. No solution information is present. 245 * OTHER_ERROR: The algorithm stopped because of an error not covered by one 246 of the statuses defined above. No solution information is present. 247 """ 248 249 OPTIMAL = result_pb2.TERMINATION_REASON_OPTIMAL 250 INFEASIBLE = result_pb2.TERMINATION_REASON_INFEASIBLE 251 UNBOUNDED = result_pb2.TERMINATION_REASON_UNBOUNDED 252 INFEASIBLE_OR_UNBOUNDED = result_pb2.TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED 253 IMPRECISE = result_pb2.TERMINATION_REASON_IMPRECISE 254 FEASIBLE = result_pb2.TERMINATION_REASON_FEASIBLE 255 NO_SOLUTION_FOUND = result_pb2.TERMINATION_REASON_NO_SOLUTION_FOUND 256 NUMERICAL_ERROR = result_pb2.TERMINATION_REASON_NUMERICAL_ERROR 257 OTHER_ERROR = result_pb2.TERMINATION_REASON_OTHER_ERROR 258 259 260@enum.unique 261class Limit(enum.Enum): 262 """The optimizer reached a limit, partial solution information may be present. 263 264 Values are: 265 * UNDETERMINED: The underlying solver does not expose which limit was 266 reached. 267 * ITERATION: An iterative algorithm stopped after conducting the 268 maximum number of iterations (e.g. simplex or barrier iterations). 269 * TIME: The algorithm stopped after a user-specified amount of 270 computation time. 271 * NODE: A branch-and-bound algorithm stopped because it explored a 272 maximum number of nodes in the branch-and-bound tree. 273 * SOLUTION: The algorithm stopped because it found the required 274 number of solutions. This is often used in MIPs to get the solver to 275 return the first feasible solution it encounters. 276 * MEMORY: The algorithm stopped because it ran out of memory. 277 * OBJECTIVE: The algorithm stopped because it found a solution better 278 than a minimum limit set by the user. 279 * NORM: The algorithm stopped because the norm of an iterate became 280 too large. 281 * INTERRUPTED: The algorithm stopped because of an interrupt signal or a 282 user interrupt request. 283 * SLOW_PROGRESS: The algorithm stopped because it was unable to continue 284 making progress towards the solution. 285 * OTHER: The algorithm stopped due to a limit not covered by one of the 286 above. Note that UNDETERMINED is used when the reason cannot be 287 determined, and OTHER is used when the reason is known but does not fit 288 into any of the above alternatives. 289 """ 290 291 UNDETERMINED = result_pb2.LIMIT_UNDETERMINED 292 ITERATION = result_pb2.LIMIT_ITERATION 293 TIME = result_pb2.LIMIT_TIME 294 NODE = result_pb2.LIMIT_NODE 295 SOLUTION = result_pb2.LIMIT_SOLUTION 296 MEMORY = result_pb2.LIMIT_MEMORY 297 OBJECTIVE = result_pb2.LIMIT_OBJECTIVE 298 NORM = result_pb2.LIMIT_NORM 299 INTERRUPTED = result_pb2.LIMIT_INTERRUPTED 300 SLOW_PROGRESS = result_pb2.LIMIT_SLOW_PROGRESS 301 OTHER = result_pb2.LIMIT_OTHER 302 303 304@dataclasses.dataclass 305class Termination: 306 """An explanation of why the solver stopped. 307 308 Attributes: 309 reason: Why the solver stopped, e.g. it found a provably optimal solution. 310 Additional information in `limit` when value is FEASIBLE or 311 NO_SOLUTION_FOUND, see `limit` for details. 312 limit: If the solver stopped early, what caused it to stop. Have value 313 UNSPECIFIED when reason is not NO_SOLUTION_FOUND or FEASIBLE. May still be 314 UNSPECIFIED when reason is NO_SOLUTION_FOUND or FEASIBLE, some solvers 315 cannot fill this in. 316 detail: Additional, information beyond reason about why the solver stopped, 317 typically solver specific. 318 problem_status: Feasibility statuses for primal and dual problems. 319 objective_bounds: Bounds on the optimal objective value. 320 """ 321 322 reason: TerminationReason = TerminationReason.OPTIMAL 323 limit: Optional[Limit] = None 324 detail: str = "" 325 problem_status: ProblemStatus = ProblemStatus() 326 objective_bounds: ObjectiveBounds = ObjectiveBounds() 327 328 def to_proto(self) -> result_pb2.TerminationProto: 329 """Returns an equivalent protocol buffer to this Termination.""" 330 return result_pb2.TerminationProto( 331 reason=self.reason.value, 332 limit=( 333 result_pb2.LIMIT_UNSPECIFIED if self.limit is None else self.limit.value 334 ), 335 detail=self.detail, 336 problem_status=self.problem_status.to_proto(), 337 objective_bounds=self.objective_bounds.to_proto(), 338 ) 339 340 341def parse_termination( 342 termination_proto: result_pb2.TerminationProto, 343) -> Termination: 344 """Returns a Termination that is equivalent to termination_proto.""" 345 reason_proto = termination_proto.reason 346 limit_proto = termination_proto.limit 347 if reason_proto == result_pb2.TERMINATION_REASON_UNSPECIFIED: 348 raise ValueError("Termination reason should not be UNSPECIFIED") 349 reason_is_limit = ( 350 reason_proto == result_pb2.TERMINATION_REASON_NO_SOLUTION_FOUND 351 ) or (reason_proto == result_pb2.TERMINATION_REASON_FEASIBLE) 352 limit_set = limit_proto != result_pb2.LIMIT_UNSPECIFIED 353 if reason_is_limit != limit_set: 354 raise ValueError( 355 f"Termination limit (={limit_proto})) should take value other than " 356 f"UNSPECIFIED if and only if termination reason (={reason_proto}) is " 357 "FEASIBLE or NO_SOLUTION_FOUND" 358 ) 359 termination = Termination() 360 termination.reason = TerminationReason(reason_proto) 361 termination.limit = Limit(limit_proto) if limit_set else None 362 termination.detail = termination_proto.detail 363 termination.problem_status = parse_problem_status(termination_proto.problem_status) 364 termination.objective_bounds = parse_objective_bounds( 365 termination_proto.objective_bounds 366 ) 367 return termination 368 369 370@dataclasses.dataclass 371class SolveResult: 372 """The result of solving an optimization problem defined by a Model. 373 374 We attempt to return as much solution information (primal_solutions, 375 primal_rays, dual_solutions, dual_rays) as each underlying solver will provide 376 given its return status. Differences in the underlying solvers result in a 377 weak contract on what fields will be populated for a given termination 378 reason. This is discussed in detail in termination_reasons.md, and the most 379 important points are summarized below: 380 * When the termination reason is optimal, there will be at least one primal 381 solution provided that will be feasible up to the underlying solver's 382 tolerances. 383 * Dual solutions are only given for convex optimization problems (e.g. 384 linear programs, not integer programs). 385 * A basis is only given for linear programs when solved by the simplex 386 method (e.g., not with PDLP). 387 * Solvers have widely varying support for returning primal and dual rays. 388 E.g. a termination_reason of unbounded does not ensure that a feasible 389 solution or a primal ray is returned, check termination_reasons.md for 390 solver specific guarantees if this is needed. Further, many solvers will 391 provide the ray but not the feasible solution when returning an unbounded 392 status. 393 * When the termination reason is that a limit was reached or that the result 394 is imprecise, a solution may or may not be present. Further, for some 395 solvers (generally, convex optimization solvers, not MIP solvers), the 396 primal or dual solution may not be feasible. 397 398 Solver specific output is also returned for some solvers (and only information 399 for the solver used will be populated). 400 401 Attributes: 402 termination: The reason the solver stopped. 403 solve_stats: Statistics on the solve process, e.g. running time, iterations. 404 solutions: Lexicographically by primal feasibility status, dual feasibility 405 status, (basic dual feasibility for simplex solvers), primal objective 406 value and dual objective value. 407 primal_rays: Directions of unbounded primal improvement, or equivalently, 408 dual infeasibility certificates. Typically provided for terminal reasons 409 UNBOUNDED and DUAL_INFEASIBLE. 410 dual_rays: Directions of unbounded dual improvement, or equivalently, primal 411 infeasibility certificates. Typically provided for termination reason 412 INFEASIBLE. 413 gscip_specific_output: statistics returned by the gSCIP solver, if used. 414 osqp_specific_output: statistics returned by the OSQP solver, if used. 415 pdlp_specific_output: statistics returned by the PDLP solver, if used. 416 """ 417 418 termination: Termination = dataclasses.field(default_factory=Termination) 419 solve_stats: SolveStats = dataclasses.field(default_factory=SolveStats) 420 solutions: List[solution.Solution] = dataclasses.field(default_factory=list) 421 primal_rays: List[solution.PrimalRay] = dataclasses.field(default_factory=list) 422 dual_rays: List[solution.DualRay] = dataclasses.field(default_factory=list) 423 # At most one of the below will be set 424 gscip_specific_output: Optional[gscip_pb2.GScipOutput] = None 425 osqp_specific_output: Optional[osqp_pb2.OsqpOutput] = None 426 pdlp_specific_output: Optional[result_pb2.SolveResultProto.PdlpOutput] = None 427 428 def solve_time(self) -> datetime.timedelta: 429 """Shortcut for SolveResult.solve_stats.solve_time.""" 430 return self.solve_stats.solve_time 431 432 def primal_bound(self) -> float: 433 """Returns a primal bound on the optimal objective value as described in ObjectiveBounds. 434 435 Will return a valid (possibly infinite) bound even if no primal feasible 436 solutions are available. 437 """ 438 return self.termination.objective_bounds.primal_bound 439 440 def dual_bound(self) -> float: 441 """Returns a dual bound on the optimal objective value as described in ObjectiveBounds. 442 443 Will return a valid (possibly infinite) bound even if no dual feasible 444 solutions are available. 445 """ 446 return self.termination.objective_bounds.dual_bound 447 448 def has_primal_feasible_solution(self) -> bool: 449 """Indicates if at least one primal feasible solution is available. 450 451 When termination.reason is TerminationReason.OPTIMAL or 452 TerminationReason.FEASIBLE, this is guaranteed to be true and need not be 453 checked. 454 455 Returns: 456 True if there is at least one primal feasible solution is available, 457 False, otherwise. 458 """ 459 if not self.solutions: 460 return False 461 return ( 462 self.solutions[0].primal_solution is not None 463 and self.solutions[0].primal_solution.feasibility_status 464 == solution.SolutionStatus.FEASIBLE 465 ) 466 467 def objective_value(self) -> float: 468 """Returns the objective value of the best primal feasible solution. 469 470 An error will be raised if there are no primal feasible solutions. 471 primal_bound() above is guaranteed to be at least as good (larger or equal 472 for max problems and smaller or equal for min problems) as objective_value() 473 and will never raise an error, so it may be preferable in some cases. Note 474 that primal_bound() could be better than objective_value() even for optimal 475 terminations, but on such optimal termination, both should satisfy the 476 optimality tolerances. 477 478 Returns: 479 The objective value of the best primal feasible solution. 480 481 Raises: 482 ValueError: There are no primal feasible solutions. 483 """ 484 if not self.has_primal_feasible_solution(): 485 raise ValueError("No primal feasible solution available.") 486 assert self.solutions[0].primal_solution is not None 487 return self.solutions[0].primal_solution.objective_value 488 489 def best_objective_bound(self) -> float: 490 """Returns a bound on the best possible objective value. 491 492 best_objective_bound() is always equal to dual_bound(), so they can be 493 used interchangeably. 494 """ 495 return self.termination.objective_bounds.dual_bound 496 497 @overload 498 def variable_values(self, variables: None = ...) -> Dict[model.Variable, float]: ... 499 500 @overload 501 def variable_values(self, variables: model.Variable) -> float: ... 502 503 @overload 504 def variable_values(self, variables: Iterable[model.Variable]) -> List[float]: ... 505 506 def variable_values(self, variables=None): 507 """The variable values from the best primal feasible solution. 508 509 An error will be raised if there are no primal feasible solutions. 510 511 Args: 512 variables: an optional Variable or iterator of Variables indicating what 513 variable values to return. If not provided, variable_values returns a 514 dictionary with all the variable values for all variables. 515 516 Returns: 517 The variable values from the best primal feasible solution. 518 519 Raises: 520 ValueError: There are no primal feasible solutions. 521 TypeError: Argument is not None, a Variable or an iterable of Variables. 522 KeyError: Variable values requested for an invalid variable (e.g. is not a 523 Variable or is a variable for another model). 524 """ 525 if not self.has_primal_feasible_solution(): 526 raise ValueError("No primal feasible solution available.") 527 assert self.solutions[0].primal_solution is not None 528 if variables is None: 529 return self.solutions[0].primal_solution.variable_values 530 if isinstance(variables, model.Variable): 531 return self.solutions[0].primal_solution.variable_values[variables] 532 if isinstance(variables, Iterable): 533 return [ 534 self.solutions[0].primal_solution.variable_values[v] for v in variables 535 ] 536 raise TypeError( 537 "unsupported type in argument for " 538 f"variable_values: {type(variables).__name__!r}" 539 ) 540 541 def bounded(self) -> bool: 542 """Returns true only if the problem has been shown to be feasible and bounded.""" 543 return ( 544 self.termination.problem_status.primal_status == FeasibilityStatus.FEASIBLE 545 and self.termination.problem_status.dual_status 546 == FeasibilityStatus.FEASIBLE 547 ) 548 549 def has_ray(self) -> bool: 550 """Indicates if at least one primal ray is available. 551 552 This is NOT guaranteed to be true when termination.reason is 553 TerminationReason.kUnbounded or TerminationReason.kInfeasibleOrUnbounded. 554 555 Returns: 556 True if at least one primal ray is available. 557 """ 558 return bool(self.primal_rays) 559 560 @overload 561 def ray_variable_values( 562 self, variables: None = ... 563 ) -> Dict[model.Variable, float]: ... 564 565 @overload 566 def ray_variable_values(self, variables: model.Variable) -> float: ... 567 568 @overload 569 def ray_variable_values( 570 self, variables: Iterable[model.Variable] 571 ) -> List[float]: ... 572 573 def ray_variable_values(self, variables=None): 574 """The variable values from the first primal ray. 575 576 An error will be raised if there are no primal rays. 577 578 Args: 579 variables: an optional Variable or iterator of Variables indicating what 580 variable values to return. If not provided, variable_values() returns a 581 dictionary with the variable values for all variables. 582 583 Returns: 584 The variable values from the first primal ray. 585 586 Raises: 587 ValueError: There are no primal rays. 588 TypeError: Argument is not None, a Variable or an iterable of Variables. 589 KeyError: Variable values requested for an invalid variable (e.g. is not a 590 Variable or is a variable for another model). 591 """ 592 if not self.has_ray(): 593 raise ValueError("No primal ray available.") 594 if variables is None: 595 return self.primal_rays[0].variable_values 596 if isinstance(variables, model.Variable): 597 return self.primal_rays[0].variable_values[variables] 598 if isinstance(variables, Iterable): 599 return [self.primal_rays[0].variable_values[v] for v in variables] 600 raise TypeError( 601 "unsupported type in argument for " 602 f"ray_variable_values: {type(variables).__name__!r}" 603 ) 604 605 def has_dual_feasible_solution(self) -> bool: 606 """Indicates if the best solution has an associated dual feasible solution. 607 608 This is NOT guaranteed to be true when termination.reason is 609 TerminationReason.Optimal. It also may be true even when the best solution 610 does not have an associated primal feasible solution. 611 612 Returns: 613 True if the best solution has an associated dual feasible solution. 614 """ 615 if not self.solutions: 616 return False 617 return ( 618 self.solutions[0].dual_solution is not None 619 and self.solutions[0].dual_solution.feasibility_status 620 == solution.SolutionStatus.FEASIBLE 621 ) 622 623 @overload 624 def dual_values( 625 self, linear_constraints: None = ... 626 ) -> Dict[model.LinearConstraint, float]: ... 627 628 @overload 629 def dual_values(self, linear_constraints: model.LinearConstraint) -> float: ... 630 631 @overload 632 def dual_values( 633 self, linear_constraints: Iterable[model.LinearConstraint] 634 ) -> List[float]: ... 635 636 def dual_values(self, linear_constraints=None): 637 """The dual values associated to the best solution. 638 639 If there is at least one primal feasible solution, this corresponds to the 640 dual values associated to the best primal feasible solution. An error will 641 be raised if the best solution does not have an associated dual feasible 642 solution. 643 644 Args: 645 linear_constraints: an optional LinearConstraint or iterator of 646 LinearConstraint indicating what dual values to return. If not provided, 647 dual_values() returns a dictionary with the dual values for all linear 648 constraints. 649 650 Returns: 651 The dual values associated to the best solution. 652 653 Raises: 654 ValueError: The best solution does not have an associated dual feasible 655 solution. 656 TypeError: Argument is not None, a LinearConstraint or an iterable of 657 LinearConstraint. 658 KeyError: LinearConstraint values requested for an invalid 659 linear constraint (e.g. is not a LinearConstraint or is a linear 660 constraint for another model). 661 """ 662 if not self.has_dual_feasible_solution(): 663 raise ValueError(_NO_DUAL_SOLUTION_ERROR) 664 assert self.solutions[0].dual_solution is not None 665 if linear_constraints is None: 666 return self.solutions[0].dual_solution.dual_values 667 if isinstance(linear_constraints, model.LinearConstraint): 668 return self.solutions[0].dual_solution.dual_values[linear_constraints] 669 if isinstance(linear_constraints, Iterable): 670 return [ 671 self.solutions[0].dual_solution.dual_values[c] 672 for c in linear_constraints 673 ] 674 raise TypeError( 675 "unsupported type in argument for " 676 f"dual_values: {type(linear_constraints).__name__!r}" 677 ) 678 679 @overload 680 def reduced_costs(self, variables: None = ...) -> Dict[model.Variable, float]: ... 681 682 @overload 683 def reduced_costs(self, variables: model.Variable) -> float: ... 684 685 @overload 686 def reduced_costs(self, variables: Iterable[model.Variable]) -> List[float]: ... 687 688 def reduced_costs(self, variables=None): 689 """The reduced costs associated to the best solution. 690 691 If there is at least one primal feasible solution, this corresponds to the 692 reduced costs associated to the best primal feasible solution. An error will 693 be raised if the best solution does not have an associated dual feasible 694 solution. 695 696 Args: 697 variables: an optional Variable or iterator of Variables indicating what 698 reduced costs to return. If not provided, reduced_costs() returns a 699 dictionary with the reduced costs for all variables. 700 701 Returns: 702 The reduced costs associated to the best solution. 703 704 Raises: 705 ValueError: The best solution does not have an associated dual feasible 706 solution. 707 TypeError: Argument is not None, a Variable or an iterable of Variables. 708 KeyError: Variable values requested for an invalid variable (e.g. is not a 709 Variable or is a variable for another model). 710 """ 711 if not self.has_dual_feasible_solution(): 712 raise ValueError(_NO_DUAL_SOLUTION_ERROR) 713 assert self.solutions[0].dual_solution is not None 714 if variables is None: 715 return self.solutions[0].dual_solution.reduced_costs 716 if isinstance(variables, model.Variable): 717 return self.solutions[0].dual_solution.reduced_costs[variables] 718 if isinstance(variables, Iterable): 719 return [self.solutions[0].dual_solution.reduced_costs[v] for v in variables] 720 raise TypeError( 721 "unsupported type in argument for " 722 f"reduced_costs: {type(variables).__name__!r}" 723 ) 724 725 def has_dual_ray(self) -> bool: 726 """Indicates if at least one dual ray is available. 727 728 This is NOT guaranteed to be true when termination.reason is 729 TerminationReason.Infeasible. 730 731 Returns: 732 True if at least one dual ray is available. 733 """ 734 return bool(self.dual_rays) 735 736 @overload 737 def ray_dual_values( 738 self, linear_constraints: None = ... 739 ) -> Dict[model.LinearConstraint, float]: ... 740 741 @overload 742 def ray_dual_values(self, linear_constraints: model.LinearConstraint) -> float: ... 743 744 @overload 745 def ray_dual_values( 746 self, linear_constraints: Iterable[model.LinearConstraint] 747 ) -> List[float]: ... 748 749 def ray_dual_values(self, linear_constraints=None): 750 """The dual values from the first dual ray. 751 752 An error will be raised if there are no dual rays. 753 754 Args: 755 linear_constraints: an optional LinearConstraint or iterator of 756 LinearConstraint indicating what dual values to return. If not provided, 757 ray_dual_values() returns a dictionary with the dual values for all 758 linear constraints. 759 760 Returns: 761 The dual values from the first dual ray. 762 763 Raises: 764 ValueError: There are no dual rays. 765 TypeError: Argument is not None, a LinearConstraint or an iterable of 766 LinearConstraint. 767 KeyError: LinearConstraint values requested for an invalid 768 linear constraint (e.g. is not a LinearConstraint or is a linear 769 constraint for another model). 770 """ 771 if not self.has_dual_ray(): 772 raise ValueError("No dual ray available.") 773 if linear_constraints is None: 774 return self.dual_rays[0].dual_values 775 if isinstance(linear_constraints, model.LinearConstraint): 776 return self.dual_rays[0].dual_values[linear_constraints] 777 if isinstance(linear_constraints, Iterable): 778 return [self.dual_rays[0].dual_values[v] for v in linear_constraints] 779 raise TypeError( 780 "unsupported type in argument for " 781 f"ray_dual_values: {type(linear_constraints).__name__!r}" 782 ) 783 784 @overload 785 def ray_reduced_costs( 786 self, variables: None = ... 787 ) -> Dict[model.Variable, float]: ... 788 789 @overload 790 def ray_reduced_costs(self, variables: model.Variable) -> float: ... 791 792 @overload 793 def ray_reduced_costs(self, variables: Iterable[model.Variable]) -> List[float]: ... 794 795 def ray_reduced_costs(self, variables=None): 796 """The reduced costs from the first dual ray. 797 798 An error will be raised if there are no dual rays. 799 800 Args: 801 variables: an optional Variable or iterator of Variables indicating what 802 reduced costs to return. If not provided, ray_reduced_costs() returns a 803 dictionary with the reduced costs for all variables. 804 805 Returns: 806 The reduced costs from the first dual ray. 807 808 Raises: 809 ValueError: There are no dual rays. 810 TypeError: Argument is not None, a Variable or an iterable of Variables. 811 KeyError: Variable values requested for an invalid variable (e.g. is not a 812 Variable or is a variable for another model). 813 """ 814 if not self.has_dual_ray(): 815 raise ValueError("No dual ray available.") 816 if variables is None: 817 return self.dual_rays[0].reduced_costs 818 if isinstance(variables, model.Variable): 819 return self.dual_rays[0].reduced_costs[variables] 820 if isinstance(variables, Iterable): 821 return [self.dual_rays[0].reduced_costs[v] for v in variables] 822 raise TypeError( 823 "unsupported type in argument for " 824 f"ray_reduced_costs: {type(variables).__name__!r}" 825 ) 826 827 def has_basis(self) -> bool: 828 """Indicates if the best solution has an associated basis. 829 830 This is NOT guaranteed to be true when termination.reason is 831 TerminationReason.Optimal. It also may be true even when the best solution 832 does not have an associated primal feasible solution. 833 834 Returns: 835 True if the best solution has an associated basis. 836 """ 837 if not self.solutions: 838 return False 839 return self.solutions[0].basis is not None 840 841 @overload 842 def constraint_status( 843 self, linear_constraints: None = ... 844 ) -> Dict[model.LinearConstraint, solution.BasisStatus]: ... 845 846 @overload 847 def constraint_status( 848 self, linear_constraints: model.LinearConstraint 849 ) -> solution.BasisStatus: ... 850 851 @overload 852 def constraint_status( 853 self, linear_constraints: Iterable[model.LinearConstraint] 854 ) -> List[solution.BasisStatus]: ... 855 856 def constraint_status(self, linear_constraints=None): 857 """The constraint basis status associated to the best solution. 858 859 If there is at least one primal feasible solution, this corresponds to the 860 basis associated to the best primal feasible solution. An error will 861 be raised if the best solution does not have an associated basis. 862 863 864 Args: 865 linear_constraints: an optional LinearConstraint or iterator of 866 LinearConstraint indicating what constraint statuses to return. If not 867 provided, returns a dictionary with the constraint statuses for all 868 linear constraints. 869 870 Returns: 871 The constraint basis status associated to the best solution. 872 873 Raises: 874 ValueError: The best solution does not have an associated basis. 875 TypeError: Argument is not None, a LinearConstraint or an iterable of 876 LinearConstraint. 877 KeyError: LinearConstraint values requested for an invalid 878 linear constraint (e.g. is not a LinearConstraint or is a linear 879 constraint for another model). 880 """ 881 if not self.has_basis(): 882 raise ValueError(_NO_BASIS_ERROR) 883 assert self.solutions[0].basis is not None 884 if linear_constraints is None: 885 return self.solutions[0].basis.constraint_status 886 if isinstance(linear_constraints, model.LinearConstraint): 887 return self.solutions[0].basis.constraint_status[linear_constraints] 888 if isinstance(linear_constraints, Iterable): 889 return [ 890 self.solutions[0].basis.constraint_status[c] for c in linear_constraints 891 ] 892 raise TypeError( 893 "unsupported type in argument for " 894 f"constraint_status: {type(linear_constraints).__name__!r}" 895 ) 896 897 @overload 898 def variable_status( 899 self, variables: None = ... 900 ) -> Dict[model.Variable, solution.BasisStatus]: ... 901 902 @overload 903 def variable_status(self, variables: model.Variable) -> solution.BasisStatus: ... 904 905 @overload 906 def variable_status( 907 self, variables: Iterable[model.Variable] 908 ) -> List[solution.BasisStatus]: ... 909 910 def variable_status(self, variables=None): 911 """The variable basis status associated to the best solution. 912 913 If there is at least one primal feasible solution, this corresponds to the 914 basis associated to the best primal feasible solution. An error will 915 be raised if the best solution does not have an associated basis. 916 917 Args: 918 variables: an optional Variable or iterator of Variables indicating what 919 reduced costs to return. If not provided, variable_status() returns a 920 dictionary with the reduced costs for all variables. 921 922 Returns: 923 The variable basis status associated to the best solution. 924 925 Raises: 926 ValueError: The best solution does not have an associated basis. 927 TypeError: Argument is not None, a Variable or an iterable of Variables. 928 KeyError: Variable values requested for an invalid variable (e.g. is not a 929 Variable or is a variable for another model). 930 """ 931 if not self.has_basis(): 932 raise ValueError(_NO_BASIS_ERROR) 933 assert self.solutions[0].basis is not None 934 if variables is None: 935 return self.solutions[0].basis.variable_status 936 if isinstance(variables, model.Variable): 937 return self.solutions[0].basis.variable_status[variables] 938 if isinstance(variables, Iterable): 939 return [self.solutions[0].basis.variable_status[v] for v in variables] 940 raise TypeError( 941 "unsupported type in argument for " 942 f"variable_status: {type(variables).__name__!r}" 943 ) 944 945 def to_proto(self) -> result_pb2.SolveResultProto: 946 """Returns an equivalent protocol buffer for a SolveResult.""" 947 proto = result_pb2.SolveResultProto( 948 termination=self.termination.to_proto(), 949 solutions=[s.to_proto() for s in self.solutions], 950 primal_rays=[r.to_proto() for r in self.primal_rays], 951 dual_rays=[r.to_proto() for r in self.dual_rays], 952 solve_stats=self.solve_stats.to_proto(), 953 ) 954 955 # Ensure that at most solver has solver specific output. 956 existing_solver_specific_output = None 957 958 def has_solver_specific_output(solver_name: str) -> None: 959 nonlocal existing_solver_specific_output 960 if existing_solver_specific_output is not None: 961 raise ValueError( 962 "found solver specific output for both" 963 f" {existing_solver_specific_output} and {solver_name}" 964 ) 965 existing_solver_specific_output = solver_name 966 967 if self.gscip_specific_output is not None: 968 has_solver_specific_output("gscip") 969 proto.gscip_output.CopyFrom(self.gscip_specific_output) 970 if self.osqp_specific_output is not None: 971 has_solver_specific_output("osqp") 972 proto.osqp_output.CopyFrom(self.osqp_specific_output) 973 if self.pdlp_specific_output is not None: 974 has_solver_specific_output("pdlp") 975 proto.pdlp_output.CopyFrom(self.pdlp_specific_output) 976 return proto 977 978 979def _get_problem_status( 980 result_proto: result_pb2.SolveResultProto, 981) -> result_pb2.ProblemStatusProto: 982 if result_proto.termination.HasField("problem_status"): 983 return result_proto.termination.problem_status 984 return result_proto.solve_stats.problem_status 985 986 987def _get_objective_bounds( 988 result_proto: result_pb2.SolveResultProto, 989) -> result_pb2.ObjectiveBoundsProto: 990 if result_proto.termination.HasField("objective_bounds"): 991 return result_proto.termination.objective_bounds 992 return result_pb2.ObjectiveBoundsProto( 993 primal_bound=result_proto.solve_stats.best_primal_bound, 994 dual_bound=result_proto.solve_stats.best_dual_bound, 995 ) 996 997 998def _upgrade_termination( 999 result_proto: result_pb2.SolveResultProto, 1000) -> result_pb2.TerminationProto: 1001 return result_pb2.TerminationProto( 1002 reason=result_proto.termination.reason, 1003 limit=result_proto.termination.limit, 1004 detail=result_proto.termination.detail, 1005 problem_status=_get_problem_status(result_proto), 1006 objective_bounds=_get_objective_bounds(result_proto), 1007 ) 1008 1009 1010def parse_solve_result( 1011 proto: result_pb2.SolveResultProto, mod: model.Model 1012) -> SolveResult: 1013 """Returns a SolveResult equivalent to the input proto.""" 1014 result = SolveResult() 1015 # TODO(b/290091715): change to parse_termination(proto.termination) 1016 # once solve_stats proto no longer has best_primal/dual_bound/problem_status 1017 # and problem_status/objective_bounds are guaranteed to be present in 1018 # termination proto. 1019 result.termination = parse_termination(_upgrade_termination(proto)) 1020 result.solve_stats = parse_solve_stats(proto.solve_stats) 1021 for solution_proto in proto.solutions: 1022 result.solutions.append(solution.parse_solution(solution_proto, mod)) 1023 for primal_ray_proto in proto.primal_rays: 1024 result.primal_rays.append(solution.parse_primal_ray(primal_ray_proto, mod)) 1025 for dual_ray_proto in proto.dual_rays: 1026 result.dual_rays.append(solution.parse_dual_ray(dual_ray_proto, mod)) 1027 if proto.HasField("gscip_output"): 1028 result.gscip_specific_output = proto.gscip_output 1029 elif proto.HasField("osqp_output"): 1030 result.osqp_specific_output = proto.osqp_output 1031 elif proto.HasField("pdlp_output"): 1032 result.pdlp_specific_output = proto.pdlp_output 1033 return result
33@enum.unique 34class FeasibilityStatus(enum.Enum): 35 """Problem feasibility status as claimed by the solver. 36 37 (solver is not required to return a certificate for the claim.) 38 39 Attributes: 40 UNDETERMINED: Solver does not claim a status. 41 FEASIBLE: Solver claims the problem is feasible. 42 INFEASIBLE: Solver claims the problem is infeasible. 43 """ 44 45 UNDETERMINED = result_pb2.FEASIBILITY_STATUS_UNDETERMINED 46 FEASIBLE = result_pb2.FEASIBILITY_STATUS_FEASIBLE 47 INFEASIBLE = result_pb2.FEASIBILITY_STATUS_INFEASIBLE
Problem feasibility status as claimed by the solver.
(solver is not required to return a certificate for the claim.)
Attributes:
- UNDETERMINED: Solver does not claim a status.
- FEASIBLE: Solver claims the problem is feasible.
- INFEASIBLE: Solver claims the problem is infeasible.
50@dataclasses.dataclass(frozen=True) 51class ProblemStatus: 52 """Feasibility status of the primal problem and its dual (or dual relaxation). 53 54 Statuses are as claimed by the solver and a dual relaxation is the dual of a 55 continuous relaxation for the original problem (e.g. the LP relaxation of a 56 MIP). The solver is not required to return a certificate for the feasibility 57 or infeasibility claims (e.g. the solver may claim primal feasibility without 58 returning a primal feasible solutuion). This combined status gives a 59 comprehensive description of a solver's claims about feasibility and 60 unboundedness of the solved problem. For instance, 61 * a feasible status for primal and dual problems indicates the primal is 62 feasible and bounded and likely has an optimal solution (guaranteed for 63 problems without non-linear constraints). 64 * a primal feasible and a dual infeasible status indicates the primal 65 problem is unbounded (i.e. has arbitrarily good solutions). 66 Note that a dual infeasible status by itself (i.e. accompanied by an 67 undetermined primal status) does not imply the primal problem is unbounded as 68 we could have both problems be infeasible. Also, while a primal and dual 69 feasible status may imply the existence of an optimal solution, it does not 70 guarantee the solver has actually found such optimal solution. 71 72 Attributes: 73 primal_status: Status for the primal problem. 74 dual_status: Status for the dual problem (or for the dual of a continuous 75 relaxation). 76 primal_or_dual_infeasible: If true, the solver claims the primal or dual 77 problem is infeasible, but it does not know which (or if both are 78 infeasible). Can be true only when primal_problem_status = 79 dual_problem_status = kUndetermined. This extra information is often 80 needed when preprocessing determines there is no optimal solution to the 81 problem (but can't determine if it is due to infeasibility, unboundedness, 82 or both). 83 """ 84 85 primal_status: FeasibilityStatus = FeasibilityStatus.UNDETERMINED 86 dual_status: FeasibilityStatus = FeasibilityStatus.UNDETERMINED 87 primal_or_dual_infeasible: bool = False 88 89 def to_proto(self) -> result_pb2.ProblemStatusProto: 90 """Returns an equivalent proto for a problem status.""" 91 return result_pb2.ProblemStatusProto( 92 primal_status=self.primal_status.value, 93 dual_status=self.dual_status.value, 94 primal_or_dual_infeasible=self.primal_or_dual_infeasible, 95 )
Feasibility status of the primal problem and its dual (or dual relaxation).
Statuses are as claimed by the solver and a dual relaxation is the dual of a continuous relaxation for the original problem (e.g. the LP relaxation of a MIP). The solver is not required to return a certificate for the feasibility or infeasibility claims (e.g. the solver may claim primal feasibility without returning a primal feasible solutuion). This combined status gives a comprehensive description of a solver's claims about feasibility and unboundedness of the solved problem. For instance,
- a feasible status for primal and dual problems indicates the primal is feasible and bounded and likely has an optimal solution (guaranteed for problems without non-linear constraints).
- a primal feasible and a dual infeasible status indicates the primal problem is unbounded (i.e. has arbitrarily good solutions). Note that a dual infeasible status by itself (i.e. accompanied by an undetermined primal status) does not imply the primal problem is unbounded as we could have both problems be infeasible. Also, while a primal and dual feasible status may imply the existence of an optimal solution, it does not guarantee the solver has actually found such optimal solution.
Attributes:
- primal_status: Status for the primal problem.
- dual_status: Status for the dual problem (or for the dual of a continuous relaxation).
- primal_or_dual_infeasible: If true, the solver claims the primal or dual problem is infeasible, but it does not know which (or if both are infeasible). Can be true only when primal_problem_status = dual_problem_status = kUndetermined. This extra information is often needed when preprocessing determines there is no optimal solution to the problem (but can't determine if it is due to infeasibility, unboundedness, or both).
89 def to_proto(self) -> result_pb2.ProblemStatusProto: 90 """Returns an equivalent proto for a problem status.""" 91 return result_pb2.ProblemStatusProto( 92 primal_status=self.primal_status.value, 93 dual_status=self.dual_status.value, 94 primal_or_dual_infeasible=self.primal_or_dual_infeasible, 95 )
Returns an equivalent proto for a problem status.
98def parse_problem_status(proto: result_pb2.ProblemStatusProto) -> ProblemStatus: 99 """Returns an equivalent ProblemStatus from the input proto.""" 100 primal_status_proto = proto.primal_status 101 if primal_status_proto == result_pb2.FEASIBILITY_STATUS_UNSPECIFIED: 102 raise ValueError("Primal feasibility status should not be UNSPECIFIED") 103 dual_status_proto = proto.dual_status 104 if dual_status_proto == result_pb2.FEASIBILITY_STATUS_UNSPECIFIED: 105 raise ValueError("Dual feasibility status should not be UNSPECIFIED") 106 return ProblemStatus( 107 primal_status=FeasibilityStatus(primal_status_proto), 108 dual_status=FeasibilityStatus(dual_status_proto), 109 primal_or_dual_infeasible=proto.primal_or_dual_infeasible, 110 )
Returns an equivalent ProblemStatus from the input proto.
113@dataclasses.dataclass(frozen=True) 114class ObjectiveBounds: 115 """Bounds on the optimal objective value. 116 117 MOE:begin_intracomment_strip 118 See go/mathopt-objective-bounds for more details. 119 MOE:end_intracomment_strip 120 121 Attributes: 122 primal_bound: Solver claims there exists a primal solution that is 123 numerically feasible (i.e. feasible up to the solvers tolerance), and 124 whose objective value is primal_bound. 125 126 The optimal value is equal or better (smaller for min objectives and 127 larger for max objectives) than primal_bound, but only up to 128 solver-tolerances. 129 130 MOE:begin_intracomment_strip 131 See go/mathopt-objective-bounds for more details. 132 MOE:end_intracomment_strip 133 dual_bound: Solver claims there exists a dual solution that is numerically 134 feasible (i.e. feasible up to the solvers tolerance), and whose objective 135 value is dual_bound. 136 137 For MIP solvers, the associated dual problem may be some continuous 138 relaxation (e.g. LP relaxation), but it is often an implicitly defined 139 problem that is a complex consequence of the solvers execution. For both 140 continuous and MIP solvers, the optimal value is equal or worse (larger 141 for min objective and smaller for max objectives) than dual_bound, but 142 only up to solver-tolerances. Some continuous solvers provide a 143 numerically safer dual bound through solver's specific output (e.g. for 144 PDLP, pdlp_output.convergence_information.corrected_dual_objective). 145 146 MOE:begin_intracomment_strip 147 See go/mathopt-objective-bounds for more details. 148 MOE:end_intracomment_strip 149 """ # fmt: skip 150 151 primal_bound: float = 0.0 152 dual_bound: float = 0.0 153 154 def to_proto(self) -> result_pb2.ObjectiveBoundsProto: 155 """Returns an equivalent proto for objective bounds.""" 156 return result_pb2.ObjectiveBoundsProto( 157 primal_bound=self.primal_bound, dual_bound=self.dual_bound 158 )
Bounds on the optimal objective value.
MOE:begin_intracomment_strip See go/mathopt-objective-bounds for more details. MOE:end_intracomment_strip
Attributes:
primal_bound: Solver claims there exists a primal solution that is numerically feasible (i.e. feasible up to the solvers tolerance), and whose objective value is primal_bound.
The optimal value is equal or better (smaller for min objectives and larger for max objectives) than primal_bound, but only up to solver-tolerances.
MOE:begin_intracomment_strip See go/mathopt-objective-bounds for more details. MOE:end_intracomment_strip
dual_bound: Solver claims there exists a dual solution that is numerically feasible (i.e. feasible up to the solvers tolerance), and whose objective value is dual_bound.
For MIP solvers, the associated dual problem may be some continuous relaxation (e.g. LP relaxation), but it is often an implicitly defined problem that is a complex consequence of the solvers execution. For both continuous and MIP solvers, the optimal value is equal or worse (larger for min objective and smaller for max objectives) than dual_bound, but only up to solver-tolerances. Some continuous solvers provide a numerically safer dual bound through solver's specific output (e.g. for PDLP, pdlp_output.convergence_information.corrected_dual_objective).
MOE:begin_intracomment_strip See go/mathopt-objective-bounds for more details. MOE:end_intracomment_strip
161def parse_objective_bounds( 162 proto: result_pb2.ObjectiveBoundsProto, 163) -> ObjectiveBounds: 164 """Returns an equivalent ObjectiveBounds from the input proto.""" 165 return ObjectiveBounds(primal_bound=proto.primal_bound, dual_bound=proto.dual_bound)
Returns an equivalent ObjectiveBounds from the input proto.
168@dataclasses.dataclass 169class SolveStats: 170 """Problem statuses and solve statistics returned by the solver. 171 172 Attributes: 173 solve_time: Elapsed wall clock time as measured by math_opt, roughly the 174 time inside solve(). Note: this does not include work done building the 175 model. 176 simplex_iterations: Simplex iterations. 177 barrier_iterations: Barrier iterations. 178 first_order_iterations: First order iterations. 179 node_count: Node count. 180 """ 181 182 solve_time: datetime.timedelta = datetime.timedelta() 183 simplex_iterations: int = 0 184 barrier_iterations: int = 0 185 first_order_iterations: int = 0 186 node_count: int = 0 187 188 def to_proto(self) -> result_pb2.SolveStatsProto: 189 """Returns an equivalent proto for a solve stats.""" 190 result = result_pb2.SolveStatsProto( 191 simplex_iterations=self.simplex_iterations, 192 barrier_iterations=self.barrier_iterations, 193 first_order_iterations=self.first_order_iterations, 194 node_count=self.node_count, 195 ) 196 result.solve_time.FromTimedelta(self.solve_time) 197 return result
Problem statuses and solve statistics returned by the solver.
Attributes:
- solve_time: Elapsed wall clock time as measured by math_opt, roughly the time inside solve(). Note: this does not include work done building the model.
- simplex_iterations: Simplex iterations.
- barrier_iterations: Barrier iterations.
- first_order_iterations: First order iterations.
- node_count: Node count.
188 def to_proto(self) -> result_pb2.SolveStatsProto: 189 """Returns an equivalent proto for a solve stats.""" 190 result = result_pb2.SolveStatsProto( 191 simplex_iterations=self.simplex_iterations, 192 barrier_iterations=self.barrier_iterations, 193 first_order_iterations=self.first_order_iterations, 194 node_count=self.node_count, 195 ) 196 result.solve_time.FromTimedelta(self.solve_time) 197 return result
Returns an equivalent proto for a solve stats.
200def parse_solve_stats(proto: result_pb2.SolveStatsProto) -> SolveStats: 201 """Returns an equivalent SolveStats from the input proto.""" 202 result = SolveStats() 203 result.solve_time = proto.solve_time.ToTimedelta() 204 result.simplex_iterations = proto.simplex_iterations 205 result.barrier_iterations = proto.barrier_iterations 206 result.first_order_iterations = proto.first_order_iterations 207 result.node_count = proto.node_count 208 return result
Returns an equivalent SolveStats from the input proto.
211@enum.unique 212class TerminationReason(enum.Enum): 213 """The reason a solve of a model terminated. 214 215 These reasons are typically as reported by the underlying solver, e.g. we do 216 not attempt to verify the precision of the solution returned. 217 218 The values are: 219 * OPTIMAL: A provably optimal solution (up to numerical tolerances) has 220 been found. 221 * INFEASIBLE: The primal problem has no feasible solutions. 222 * UNBOUNDED: The primal problem is feasible and arbitrarily good solutions 223 can be found along a primal ray. 224 * INFEASIBLE_OR_UNBOUNDED: The primal problem is either infeasible or 225 unbounded. More details on the problem status may be available in 226 solve_stats.problem_status. Note that Gurobi's unbounded status may be 227 mapped here as explained in 228 go/mathopt-solver-specific#gurobi-inf-or-unb. 229 * IMPRECISE: The problem was solved to one of the criteria above (Optimal, 230 Infeasible, Unbounded, or InfeasibleOrUnbounded), but one or more 231 tolerances was not met. Some primal/dual solutions/rays may be present, 232 but either they will be slightly infeasible, or (if the problem was 233 nearly optimal) their may be a gap between the best solution objective 234 and best objective bound. 235 236 Users can still query primal/dual solutions/rays and solution stats, 237 but they are responsible for dealing with the numerical imprecision. 238 * FEASIBLE: The optimizer reached some kind of limit and a primal feasible 239 solution is returned. See SolveResultProto.limit_detail for detailed 240 description of the kind of limit that was reached. 241 * NO_SOLUTION_FOUND: The optimizer reached some kind of limit and it did 242 not find a primal feasible solution. See SolveResultProto.limit_detail 243 for detailed description of the kind of limit that was reached. 244 * NUMERICAL_ERROR: The algorithm stopped because it encountered 245 unrecoverable numerical error. No solution information is present. 246 * OTHER_ERROR: The algorithm stopped because of an error not covered by one 247 of the statuses defined above. No solution information is present. 248 """ 249 250 OPTIMAL = result_pb2.TERMINATION_REASON_OPTIMAL 251 INFEASIBLE = result_pb2.TERMINATION_REASON_INFEASIBLE 252 UNBOUNDED = result_pb2.TERMINATION_REASON_UNBOUNDED 253 INFEASIBLE_OR_UNBOUNDED = result_pb2.TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED 254 IMPRECISE = result_pb2.TERMINATION_REASON_IMPRECISE 255 FEASIBLE = result_pb2.TERMINATION_REASON_FEASIBLE 256 NO_SOLUTION_FOUND = result_pb2.TERMINATION_REASON_NO_SOLUTION_FOUND 257 NUMERICAL_ERROR = result_pb2.TERMINATION_REASON_NUMERICAL_ERROR 258 OTHER_ERROR = result_pb2.TERMINATION_REASON_OTHER_ERROR
The reason a solve of a model terminated.
These reasons are typically as reported by the underlying solver, e.g. we do not attempt to verify the precision of the solution returned.
The values are:
- OPTIMAL: A provably optimal solution (up to numerical tolerances) has been found.
- INFEASIBLE: The primal problem has no feasible solutions.
- UNBOUNDED: The primal problem is feasible and arbitrarily good solutions can be found along a primal ray.
- INFEASIBLE_OR_UNBOUNDED: The primal problem is either infeasible or unbounded. More details on the problem status may be available in solve_stats.problem_status. Note that Gurobi's unbounded status may be mapped here as explained in go/mathopt-solver-specific#gurobi-inf-or-unb.
IMPRECISE: The problem was solved to one of the criteria above (Optimal, Infeasible, Unbounded, or InfeasibleOrUnbounded), but one or more tolerances was not met. Some primal/dual solutions/rays may be present, but either they will be slightly infeasible, or (if the problem was nearly optimal) their may be a gap between the best solution objective and best objective bound.
Users can still query primal/dual solutions/rays and solution stats, but they are responsible for dealing with the numerical imprecision.
- FEASIBLE: The optimizer reached some kind of limit and a primal feasible solution is returned. See SolveResultProto.limit_detail for detailed description of the kind of limit that was reached.
- NO_SOLUTION_FOUND: The optimizer reached some kind of limit and it did not find a primal feasible solution. See SolveResultProto.limit_detail for detailed description of the kind of limit that was reached.
- NUMERICAL_ERROR: The algorithm stopped because it encountered unrecoverable numerical error. No solution information is present.
- OTHER_ERROR: The algorithm stopped because of an error not covered by one of the statuses defined above. No solution information is present.
261@enum.unique 262class Limit(enum.Enum): 263 """The optimizer reached a limit, partial solution information may be present. 264 265 Values are: 266 * UNDETERMINED: The underlying solver does not expose which limit was 267 reached. 268 * ITERATION: An iterative algorithm stopped after conducting the 269 maximum number of iterations (e.g. simplex or barrier iterations). 270 * TIME: The algorithm stopped after a user-specified amount of 271 computation time. 272 * NODE: A branch-and-bound algorithm stopped because it explored a 273 maximum number of nodes in the branch-and-bound tree. 274 * SOLUTION: The algorithm stopped because it found the required 275 number of solutions. This is often used in MIPs to get the solver to 276 return the first feasible solution it encounters. 277 * MEMORY: The algorithm stopped because it ran out of memory. 278 * OBJECTIVE: The algorithm stopped because it found a solution better 279 than a minimum limit set by the user. 280 * NORM: The algorithm stopped because the norm of an iterate became 281 too large. 282 * INTERRUPTED: The algorithm stopped because of an interrupt signal or a 283 user interrupt request. 284 * SLOW_PROGRESS: The algorithm stopped because it was unable to continue 285 making progress towards the solution. 286 * OTHER: The algorithm stopped due to a limit not covered by one of the 287 above. Note that UNDETERMINED is used when the reason cannot be 288 determined, and OTHER is used when the reason is known but does not fit 289 into any of the above alternatives. 290 """ 291 292 UNDETERMINED = result_pb2.LIMIT_UNDETERMINED 293 ITERATION = result_pb2.LIMIT_ITERATION 294 TIME = result_pb2.LIMIT_TIME 295 NODE = result_pb2.LIMIT_NODE 296 SOLUTION = result_pb2.LIMIT_SOLUTION 297 MEMORY = result_pb2.LIMIT_MEMORY 298 OBJECTIVE = result_pb2.LIMIT_OBJECTIVE 299 NORM = result_pb2.LIMIT_NORM 300 INTERRUPTED = result_pb2.LIMIT_INTERRUPTED 301 SLOW_PROGRESS = result_pb2.LIMIT_SLOW_PROGRESS 302 OTHER = result_pb2.LIMIT_OTHER
The optimizer reached a limit, partial solution information may be present.
Values are:
- UNDETERMINED: The underlying solver does not expose which limit was reached.
- ITERATION: An iterative algorithm stopped after conducting the maximum number of iterations (e.g. simplex or barrier iterations).
- TIME: The algorithm stopped after a user-specified amount of computation time.
- NODE: A branch-and-bound algorithm stopped because it explored a maximum number of nodes in the branch-and-bound tree.
- SOLUTION: The algorithm stopped because it found the required number of solutions. This is often used in MIPs to get the solver to return the first feasible solution it encounters.
- MEMORY: The algorithm stopped because it ran out of memory.
- OBJECTIVE: The algorithm stopped because it found a solution better than a minimum limit set by the user.
- NORM: The algorithm stopped because the norm of an iterate became too large.
- INTERRUPTED: The algorithm stopped because of an interrupt signal or a user interrupt request.
- SLOW_PROGRESS: The algorithm stopped because it was unable to continue making progress towards the solution.
- OTHER: The algorithm stopped due to a limit not covered by one of the above. Note that UNDETERMINED is used when the reason cannot be determined, and OTHER is used when the reason is known but does not fit into any of the above alternatives.
305@dataclasses.dataclass 306class Termination: 307 """An explanation of why the solver stopped. 308 309 Attributes: 310 reason: Why the solver stopped, e.g. it found a provably optimal solution. 311 Additional information in `limit` when value is FEASIBLE or 312 NO_SOLUTION_FOUND, see `limit` for details. 313 limit: If the solver stopped early, what caused it to stop. Have value 314 UNSPECIFIED when reason is not NO_SOLUTION_FOUND or FEASIBLE. May still be 315 UNSPECIFIED when reason is NO_SOLUTION_FOUND or FEASIBLE, some solvers 316 cannot fill this in. 317 detail: Additional, information beyond reason about why the solver stopped, 318 typically solver specific. 319 problem_status: Feasibility statuses for primal and dual problems. 320 objective_bounds: Bounds on the optimal objective value. 321 """ 322 323 reason: TerminationReason = TerminationReason.OPTIMAL 324 limit: Optional[Limit] = None 325 detail: str = "" 326 problem_status: ProblemStatus = ProblemStatus() 327 objective_bounds: ObjectiveBounds = ObjectiveBounds() 328 329 def to_proto(self) -> result_pb2.TerminationProto: 330 """Returns an equivalent protocol buffer to this Termination.""" 331 return result_pb2.TerminationProto( 332 reason=self.reason.value, 333 limit=( 334 result_pb2.LIMIT_UNSPECIFIED if self.limit is None else self.limit.value 335 ), 336 detail=self.detail, 337 problem_status=self.problem_status.to_proto(), 338 objective_bounds=self.objective_bounds.to_proto(), 339 )
An explanation of why the solver stopped.
Attributes:
- reason: Why the solver stopped, e.g. it found a provably optimal solution.
Additional information in
limit
when value is FEASIBLE or NO_SOLUTION_FOUND, seelimit
for details. - limit: If the solver stopped early, what caused it to stop. Have value UNSPECIFIED when reason is not NO_SOLUTION_FOUND or FEASIBLE. May still be UNSPECIFIED when reason is NO_SOLUTION_FOUND or FEASIBLE, some solvers cannot fill this in.
- detail: Additional, information beyond reason about why the solver stopped, typically solver specific.
- problem_status: Feasibility statuses for primal and dual problems.
- objective_bounds: Bounds on the optimal objective value.
329 def to_proto(self) -> result_pb2.TerminationProto: 330 """Returns an equivalent protocol buffer to this Termination.""" 331 return result_pb2.TerminationProto( 332 reason=self.reason.value, 333 limit=( 334 result_pb2.LIMIT_UNSPECIFIED if self.limit is None else self.limit.value 335 ), 336 detail=self.detail, 337 problem_status=self.problem_status.to_proto(), 338 objective_bounds=self.objective_bounds.to_proto(), 339 )
Returns an equivalent protocol buffer to this Termination.
342def parse_termination( 343 termination_proto: result_pb2.TerminationProto, 344) -> Termination: 345 """Returns a Termination that is equivalent to termination_proto.""" 346 reason_proto = termination_proto.reason 347 limit_proto = termination_proto.limit 348 if reason_proto == result_pb2.TERMINATION_REASON_UNSPECIFIED: 349 raise ValueError("Termination reason should not be UNSPECIFIED") 350 reason_is_limit = ( 351 reason_proto == result_pb2.TERMINATION_REASON_NO_SOLUTION_FOUND 352 ) or (reason_proto == result_pb2.TERMINATION_REASON_FEASIBLE) 353 limit_set = limit_proto != result_pb2.LIMIT_UNSPECIFIED 354 if reason_is_limit != limit_set: 355 raise ValueError( 356 f"Termination limit (={limit_proto})) should take value other than " 357 f"UNSPECIFIED if and only if termination reason (={reason_proto}) is " 358 "FEASIBLE or NO_SOLUTION_FOUND" 359 ) 360 termination = Termination() 361 termination.reason = TerminationReason(reason_proto) 362 termination.limit = Limit(limit_proto) if limit_set else None 363 termination.detail = termination_proto.detail 364 termination.problem_status = parse_problem_status(termination_proto.problem_status) 365 termination.objective_bounds = parse_objective_bounds( 366 termination_proto.objective_bounds 367 ) 368 return termination
Returns a Termination that is equivalent to termination_proto.
371@dataclasses.dataclass 372class SolveResult: 373 """The result of solving an optimization problem defined by a Model. 374 375 We attempt to return as much solution information (primal_solutions, 376 primal_rays, dual_solutions, dual_rays) as each underlying solver will provide 377 given its return status. Differences in the underlying solvers result in a 378 weak contract on what fields will be populated for a given termination 379 reason. This is discussed in detail in termination_reasons.md, and the most 380 important points are summarized below: 381 * When the termination reason is optimal, there will be at least one primal 382 solution provided that will be feasible up to the underlying solver's 383 tolerances. 384 * Dual solutions are only given for convex optimization problems (e.g. 385 linear programs, not integer programs). 386 * A basis is only given for linear programs when solved by the simplex 387 method (e.g., not with PDLP). 388 * Solvers have widely varying support for returning primal and dual rays. 389 E.g. a termination_reason of unbounded does not ensure that a feasible 390 solution or a primal ray is returned, check termination_reasons.md for 391 solver specific guarantees if this is needed. Further, many solvers will 392 provide the ray but not the feasible solution when returning an unbounded 393 status. 394 * When the termination reason is that a limit was reached or that the result 395 is imprecise, a solution may or may not be present. Further, for some 396 solvers (generally, convex optimization solvers, not MIP solvers), the 397 primal or dual solution may not be feasible. 398 399 Solver specific output is also returned for some solvers (and only information 400 for the solver used will be populated). 401 402 Attributes: 403 termination: The reason the solver stopped. 404 solve_stats: Statistics on the solve process, e.g. running time, iterations. 405 solutions: Lexicographically by primal feasibility status, dual feasibility 406 status, (basic dual feasibility for simplex solvers), primal objective 407 value and dual objective value. 408 primal_rays: Directions of unbounded primal improvement, or equivalently, 409 dual infeasibility certificates. Typically provided for terminal reasons 410 UNBOUNDED and DUAL_INFEASIBLE. 411 dual_rays: Directions of unbounded dual improvement, or equivalently, primal 412 infeasibility certificates. Typically provided for termination reason 413 INFEASIBLE. 414 gscip_specific_output: statistics returned by the gSCIP solver, if used. 415 osqp_specific_output: statistics returned by the OSQP solver, if used. 416 pdlp_specific_output: statistics returned by the PDLP solver, if used. 417 """ 418 419 termination: Termination = dataclasses.field(default_factory=Termination) 420 solve_stats: SolveStats = dataclasses.field(default_factory=SolveStats) 421 solutions: List[solution.Solution] = dataclasses.field(default_factory=list) 422 primal_rays: List[solution.PrimalRay] = dataclasses.field(default_factory=list) 423 dual_rays: List[solution.DualRay] = dataclasses.field(default_factory=list) 424 # At most one of the below will be set 425 gscip_specific_output: Optional[gscip_pb2.GScipOutput] = None 426 osqp_specific_output: Optional[osqp_pb2.OsqpOutput] = None 427 pdlp_specific_output: Optional[result_pb2.SolveResultProto.PdlpOutput] = None 428 429 def solve_time(self) -> datetime.timedelta: 430 """Shortcut for SolveResult.solve_stats.solve_time.""" 431 return self.solve_stats.solve_time 432 433 def primal_bound(self) -> float: 434 """Returns a primal bound on the optimal objective value as described in ObjectiveBounds. 435 436 Will return a valid (possibly infinite) bound even if no primal feasible 437 solutions are available. 438 """ 439 return self.termination.objective_bounds.primal_bound 440 441 def dual_bound(self) -> float: 442 """Returns a dual bound on the optimal objective value as described in ObjectiveBounds. 443 444 Will return a valid (possibly infinite) bound even if no dual feasible 445 solutions are available. 446 """ 447 return self.termination.objective_bounds.dual_bound 448 449 def has_primal_feasible_solution(self) -> bool: 450 """Indicates if at least one primal feasible solution is available. 451 452 When termination.reason is TerminationReason.OPTIMAL or 453 TerminationReason.FEASIBLE, this is guaranteed to be true and need not be 454 checked. 455 456 Returns: 457 True if there is at least one primal feasible solution is available, 458 False, otherwise. 459 """ 460 if not self.solutions: 461 return False 462 return ( 463 self.solutions[0].primal_solution is not None 464 and self.solutions[0].primal_solution.feasibility_status 465 == solution.SolutionStatus.FEASIBLE 466 ) 467 468 def objective_value(self) -> float: 469 """Returns the objective value of the best primal feasible solution. 470 471 An error will be raised if there are no primal feasible solutions. 472 primal_bound() above is guaranteed to be at least as good (larger or equal 473 for max problems and smaller or equal for min problems) as objective_value() 474 and will never raise an error, so it may be preferable in some cases. Note 475 that primal_bound() could be better than objective_value() even for optimal 476 terminations, but on such optimal termination, both should satisfy the 477 optimality tolerances. 478 479 Returns: 480 The objective value of the best primal feasible solution. 481 482 Raises: 483 ValueError: There are no primal feasible solutions. 484 """ 485 if not self.has_primal_feasible_solution(): 486 raise ValueError("No primal feasible solution available.") 487 assert self.solutions[0].primal_solution is not None 488 return self.solutions[0].primal_solution.objective_value 489 490 def best_objective_bound(self) -> float: 491 """Returns a bound on the best possible objective value. 492 493 best_objective_bound() is always equal to dual_bound(), so they can be 494 used interchangeably. 495 """ 496 return self.termination.objective_bounds.dual_bound 497 498 @overload 499 def variable_values(self, variables: None = ...) -> Dict[model.Variable, float]: ... 500 501 @overload 502 def variable_values(self, variables: model.Variable) -> float: ... 503 504 @overload 505 def variable_values(self, variables: Iterable[model.Variable]) -> List[float]: ... 506 507 def variable_values(self, variables=None): 508 """The variable values from the best primal feasible solution. 509 510 An error will be raised if there are no primal feasible solutions. 511 512 Args: 513 variables: an optional Variable or iterator of Variables indicating what 514 variable values to return. If not provided, variable_values returns a 515 dictionary with all the variable values for all variables. 516 517 Returns: 518 The variable values from the best primal feasible solution. 519 520 Raises: 521 ValueError: There are no primal feasible solutions. 522 TypeError: Argument is not None, a Variable or an iterable of Variables. 523 KeyError: Variable values requested for an invalid variable (e.g. is not a 524 Variable or is a variable for another model). 525 """ 526 if not self.has_primal_feasible_solution(): 527 raise ValueError("No primal feasible solution available.") 528 assert self.solutions[0].primal_solution is not None 529 if variables is None: 530 return self.solutions[0].primal_solution.variable_values 531 if isinstance(variables, model.Variable): 532 return self.solutions[0].primal_solution.variable_values[variables] 533 if isinstance(variables, Iterable): 534 return [ 535 self.solutions[0].primal_solution.variable_values[v] for v in variables 536 ] 537 raise TypeError( 538 "unsupported type in argument for " 539 f"variable_values: {type(variables).__name__!r}" 540 ) 541 542 def bounded(self) -> bool: 543 """Returns true only if the problem has been shown to be feasible and bounded.""" 544 return ( 545 self.termination.problem_status.primal_status == FeasibilityStatus.FEASIBLE 546 and self.termination.problem_status.dual_status 547 == FeasibilityStatus.FEASIBLE 548 ) 549 550 def has_ray(self) -> bool: 551 """Indicates if at least one primal ray is available. 552 553 This is NOT guaranteed to be true when termination.reason is 554 TerminationReason.kUnbounded or TerminationReason.kInfeasibleOrUnbounded. 555 556 Returns: 557 True if at least one primal ray is available. 558 """ 559 return bool(self.primal_rays) 560 561 @overload 562 def ray_variable_values( 563 self, variables: None = ... 564 ) -> Dict[model.Variable, float]: ... 565 566 @overload 567 def ray_variable_values(self, variables: model.Variable) -> float: ... 568 569 @overload 570 def ray_variable_values( 571 self, variables: Iterable[model.Variable] 572 ) -> List[float]: ... 573 574 def ray_variable_values(self, variables=None): 575 """The variable values from the first primal ray. 576 577 An error will be raised if there are no primal rays. 578 579 Args: 580 variables: an optional Variable or iterator of Variables indicating what 581 variable values to return. If not provided, variable_values() returns a 582 dictionary with the variable values for all variables. 583 584 Returns: 585 The variable values from the first primal ray. 586 587 Raises: 588 ValueError: There are no primal rays. 589 TypeError: Argument is not None, a Variable or an iterable of Variables. 590 KeyError: Variable values requested for an invalid variable (e.g. is not a 591 Variable or is a variable for another model). 592 """ 593 if not self.has_ray(): 594 raise ValueError("No primal ray available.") 595 if variables is None: 596 return self.primal_rays[0].variable_values 597 if isinstance(variables, model.Variable): 598 return self.primal_rays[0].variable_values[variables] 599 if isinstance(variables, Iterable): 600 return [self.primal_rays[0].variable_values[v] for v in variables] 601 raise TypeError( 602 "unsupported type in argument for " 603 f"ray_variable_values: {type(variables).__name__!r}" 604 ) 605 606 def has_dual_feasible_solution(self) -> bool: 607 """Indicates if the best solution has an associated dual feasible solution. 608 609 This is NOT guaranteed to be true when termination.reason is 610 TerminationReason.Optimal. It also may be true even when the best solution 611 does not have an associated primal feasible solution. 612 613 Returns: 614 True if the best solution has an associated dual feasible solution. 615 """ 616 if not self.solutions: 617 return False 618 return ( 619 self.solutions[0].dual_solution is not None 620 and self.solutions[0].dual_solution.feasibility_status 621 == solution.SolutionStatus.FEASIBLE 622 ) 623 624 @overload 625 def dual_values( 626 self, linear_constraints: None = ... 627 ) -> Dict[model.LinearConstraint, float]: ... 628 629 @overload 630 def dual_values(self, linear_constraints: model.LinearConstraint) -> float: ... 631 632 @overload 633 def dual_values( 634 self, linear_constraints: Iterable[model.LinearConstraint] 635 ) -> List[float]: ... 636 637 def dual_values(self, linear_constraints=None): 638 """The dual values associated to the best solution. 639 640 If there is at least one primal feasible solution, this corresponds to the 641 dual values associated to the best primal feasible solution. An error will 642 be raised if the best solution does not have an associated dual feasible 643 solution. 644 645 Args: 646 linear_constraints: an optional LinearConstraint or iterator of 647 LinearConstraint indicating what dual values to return. If not provided, 648 dual_values() returns a dictionary with the dual values for all linear 649 constraints. 650 651 Returns: 652 The dual values associated to the best solution. 653 654 Raises: 655 ValueError: The best solution does not have an associated dual feasible 656 solution. 657 TypeError: Argument is not None, a LinearConstraint or an iterable of 658 LinearConstraint. 659 KeyError: LinearConstraint values requested for an invalid 660 linear constraint (e.g. is not a LinearConstraint or is a linear 661 constraint for another model). 662 """ 663 if not self.has_dual_feasible_solution(): 664 raise ValueError(_NO_DUAL_SOLUTION_ERROR) 665 assert self.solutions[0].dual_solution is not None 666 if linear_constraints is None: 667 return self.solutions[0].dual_solution.dual_values 668 if isinstance(linear_constraints, model.LinearConstraint): 669 return self.solutions[0].dual_solution.dual_values[linear_constraints] 670 if isinstance(linear_constraints, Iterable): 671 return [ 672 self.solutions[0].dual_solution.dual_values[c] 673 for c in linear_constraints 674 ] 675 raise TypeError( 676 "unsupported type in argument for " 677 f"dual_values: {type(linear_constraints).__name__!r}" 678 ) 679 680 @overload 681 def reduced_costs(self, variables: None = ...) -> Dict[model.Variable, float]: ... 682 683 @overload 684 def reduced_costs(self, variables: model.Variable) -> float: ... 685 686 @overload 687 def reduced_costs(self, variables: Iterable[model.Variable]) -> List[float]: ... 688 689 def reduced_costs(self, variables=None): 690 """The reduced costs associated to the best solution. 691 692 If there is at least one primal feasible solution, this corresponds to the 693 reduced costs associated to the best primal feasible solution. An error will 694 be raised if the best solution does not have an associated dual feasible 695 solution. 696 697 Args: 698 variables: an optional Variable or iterator of Variables indicating what 699 reduced costs to return. If not provided, reduced_costs() returns a 700 dictionary with the reduced costs for all variables. 701 702 Returns: 703 The reduced costs associated to the best solution. 704 705 Raises: 706 ValueError: The best solution does not have an associated dual feasible 707 solution. 708 TypeError: Argument is not None, a Variable or an iterable of Variables. 709 KeyError: Variable values requested for an invalid variable (e.g. is not a 710 Variable or is a variable for another model). 711 """ 712 if not self.has_dual_feasible_solution(): 713 raise ValueError(_NO_DUAL_SOLUTION_ERROR) 714 assert self.solutions[0].dual_solution is not None 715 if variables is None: 716 return self.solutions[0].dual_solution.reduced_costs 717 if isinstance(variables, model.Variable): 718 return self.solutions[0].dual_solution.reduced_costs[variables] 719 if isinstance(variables, Iterable): 720 return [self.solutions[0].dual_solution.reduced_costs[v] for v in variables] 721 raise TypeError( 722 "unsupported type in argument for " 723 f"reduced_costs: {type(variables).__name__!r}" 724 ) 725 726 def has_dual_ray(self) -> bool: 727 """Indicates if at least one dual ray is available. 728 729 This is NOT guaranteed to be true when termination.reason is 730 TerminationReason.Infeasible. 731 732 Returns: 733 True if at least one dual ray is available. 734 """ 735 return bool(self.dual_rays) 736 737 @overload 738 def ray_dual_values( 739 self, linear_constraints: None = ... 740 ) -> Dict[model.LinearConstraint, float]: ... 741 742 @overload 743 def ray_dual_values(self, linear_constraints: model.LinearConstraint) -> float: ... 744 745 @overload 746 def ray_dual_values( 747 self, linear_constraints: Iterable[model.LinearConstraint] 748 ) -> List[float]: ... 749 750 def ray_dual_values(self, linear_constraints=None): 751 """The dual values from the first dual ray. 752 753 An error will be raised if there are no dual rays. 754 755 Args: 756 linear_constraints: an optional LinearConstraint or iterator of 757 LinearConstraint indicating what dual values to return. If not provided, 758 ray_dual_values() returns a dictionary with the dual values for all 759 linear constraints. 760 761 Returns: 762 The dual values from the first dual ray. 763 764 Raises: 765 ValueError: There are no dual rays. 766 TypeError: Argument is not None, a LinearConstraint or an iterable of 767 LinearConstraint. 768 KeyError: LinearConstraint values requested for an invalid 769 linear constraint (e.g. is not a LinearConstraint or is a linear 770 constraint for another model). 771 """ 772 if not self.has_dual_ray(): 773 raise ValueError("No dual ray available.") 774 if linear_constraints is None: 775 return self.dual_rays[0].dual_values 776 if isinstance(linear_constraints, model.LinearConstraint): 777 return self.dual_rays[0].dual_values[linear_constraints] 778 if isinstance(linear_constraints, Iterable): 779 return [self.dual_rays[0].dual_values[v] for v in linear_constraints] 780 raise TypeError( 781 "unsupported type in argument for " 782 f"ray_dual_values: {type(linear_constraints).__name__!r}" 783 ) 784 785 @overload 786 def ray_reduced_costs( 787 self, variables: None = ... 788 ) -> Dict[model.Variable, float]: ... 789 790 @overload 791 def ray_reduced_costs(self, variables: model.Variable) -> float: ... 792 793 @overload 794 def ray_reduced_costs(self, variables: Iterable[model.Variable]) -> List[float]: ... 795 796 def ray_reduced_costs(self, variables=None): 797 """The reduced costs from the first dual ray. 798 799 An error will be raised if there are no dual rays. 800 801 Args: 802 variables: an optional Variable or iterator of Variables indicating what 803 reduced costs to return. If not provided, ray_reduced_costs() returns a 804 dictionary with the reduced costs for all variables. 805 806 Returns: 807 The reduced costs from the first dual ray. 808 809 Raises: 810 ValueError: There are no dual rays. 811 TypeError: Argument is not None, a Variable or an iterable of Variables. 812 KeyError: Variable values requested for an invalid variable (e.g. is not a 813 Variable or is a variable for another model). 814 """ 815 if not self.has_dual_ray(): 816 raise ValueError("No dual ray available.") 817 if variables is None: 818 return self.dual_rays[0].reduced_costs 819 if isinstance(variables, model.Variable): 820 return self.dual_rays[0].reduced_costs[variables] 821 if isinstance(variables, Iterable): 822 return [self.dual_rays[0].reduced_costs[v] for v in variables] 823 raise TypeError( 824 "unsupported type in argument for " 825 f"ray_reduced_costs: {type(variables).__name__!r}" 826 ) 827 828 def has_basis(self) -> bool: 829 """Indicates if the best solution has an associated basis. 830 831 This is NOT guaranteed to be true when termination.reason is 832 TerminationReason.Optimal. It also may be true even when the best solution 833 does not have an associated primal feasible solution. 834 835 Returns: 836 True if the best solution has an associated basis. 837 """ 838 if not self.solutions: 839 return False 840 return self.solutions[0].basis is not None 841 842 @overload 843 def constraint_status( 844 self, linear_constraints: None = ... 845 ) -> Dict[model.LinearConstraint, solution.BasisStatus]: ... 846 847 @overload 848 def constraint_status( 849 self, linear_constraints: model.LinearConstraint 850 ) -> solution.BasisStatus: ... 851 852 @overload 853 def constraint_status( 854 self, linear_constraints: Iterable[model.LinearConstraint] 855 ) -> List[solution.BasisStatus]: ... 856 857 def constraint_status(self, linear_constraints=None): 858 """The constraint basis status associated to the best solution. 859 860 If there is at least one primal feasible solution, this corresponds to the 861 basis associated to the best primal feasible solution. An error will 862 be raised if the best solution does not have an associated basis. 863 864 865 Args: 866 linear_constraints: an optional LinearConstraint or iterator of 867 LinearConstraint indicating what constraint statuses to return. If not 868 provided, returns a dictionary with the constraint statuses for all 869 linear constraints. 870 871 Returns: 872 The constraint basis status associated to the best solution. 873 874 Raises: 875 ValueError: The best solution does not have an associated basis. 876 TypeError: Argument is not None, a LinearConstraint or an iterable of 877 LinearConstraint. 878 KeyError: LinearConstraint values requested for an invalid 879 linear constraint (e.g. is not a LinearConstraint or is a linear 880 constraint for another model). 881 """ 882 if not self.has_basis(): 883 raise ValueError(_NO_BASIS_ERROR) 884 assert self.solutions[0].basis is not None 885 if linear_constraints is None: 886 return self.solutions[0].basis.constraint_status 887 if isinstance(linear_constraints, model.LinearConstraint): 888 return self.solutions[0].basis.constraint_status[linear_constraints] 889 if isinstance(linear_constraints, Iterable): 890 return [ 891 self.solutions[0].basis.constraint_status[c] for c in linear_constraints 892 ] 893 raise TypeError( 894 "unsupported type in argument for " 895 f"constraint_status: {type(linear_constraints).__name__!r}" 896 ) 897 898 @overload 899 def variable_status( 900 self, variables: None = ... 901 ) -> Dict[model.Variable, solution.BasisStatus]: ... 902 903 @overload 904 def variable_status(self, variables: model.Variable) -> solution.BasisStatus: ... 905 906 @overload 907 def variable_status( 908 self, variables: Iterable[model.Variable] 909 ) -> List[solution.BasisStatus]: ... 910 911 def variable_status(self, variables=None): 912 """The variable basis status associated to the best solution. 913 914 If there is at least one primal feasible solution, this corresponds to the 915 basis associated to the best primal feasible solution. An error will 916 be raised if the best solution does not have an associated basis. 917 918 Args: 919 variables: an optional Variable or iterator of Variables indicating what 920 reduced costs to return. If not provided, variable_status() returns a 921 dictionary with the reduced costs for all variables. 922 923 Returns: 924 The variable basis status associated to the best solution. 925 926 Raises: 927 ValueError: The best solution does not have an associated basis. 928 TypeError: Argument is not None, a Variable or an iterable of Variables. 929 KeyError: Variable values requested for an invalid variable (e.g. is not a 930 Variable or is a variable for another model). 931 """ 932 if not self.has_basis(): 933 raise ValueError(_NO_BASIS_ERROR) 934 assert self.solutions[0].basis is not None 935 if variables is None: 936 return self.solutions[0].basis.variable_status 937 if isinstance(variables, model.Variable): 938 return self.solutions[0].basis.variable_status[variables] 939 if isinstance(variables, Iterable): 940 return [self.solutions[0].basis.variable_status[v] for v in variables] 941 raise TypeError( 942 "unsupported type in argument for " 943 f"variable_status: {type(variables).__name__!r}" 944 ) 945 946 def to_proto(self) -> result_pb2.SolveResultProto: 947 """Returns an equivalent protocol buffer for a SolveResult.""" 948 proto = result_pb2.SolveResultProto( 949 termination=self.termination.to_proto(), 950 solutions=[s.to_proto() for s in self.solutions], 951 primal_rays=[r.to_proto() for r in self.primal_rays], 952 dual_rays=[r.to_proto() for r in self.dual_rays], 953 solve_stats=self.solve_stats.to_proto(), 954 ) 955 956 # Ensure that at most solver has solver specific output. 957 existing_solver_specific_output = None 958 959 def has_solver_specific_output(solver_name: str) -> None: 960 nonlocal existing_solver_specific_output 961 if existing_solver_specific_output is not None: 962 raise ValueError( 963 "found solver specific output for both" 964 f" {existing_solver_specific_output} and {solver_name}" 965 ) 966 existing_solver_specific_output = solver_name 967 968 if self.gscip_specific_output is not None: 969 has_solver_specific_output("gscip") 970 proto.gscip_output.CopyFrom(self.gscip_specific_output) 971 if self.osqp_specific_output is not None: 972 has_solver_specific_output("osqp") 973 proto.osqp_output.CopyFrom(self.osqp_specific_output) 974 if self.pdlp_specific_output is not None: 975 has_solver_specific_output("pdlp") 976 proto.pdlp_output.CopyFrom(self.pdlp_specific_output) 977 return proto
The result of solving an optimization problem defined by a Model.
We attempt to return as much solution information (primal_solutions, primal_rays, dual_solutions, dual_rays) as each underlying solver will provide given its return status. Differences in the underlying solvers result in a weak contract on what fields will be populated for a given termination reason. This is discussed in detail in termination_reasons.md, and the most important points are summarized below:
- When the termination reason is optimal, there will be at least one primal solution provided that will be feasible up to the underlying solver's tolerances.
- Dual solutions are only given for convex optimization problems (e.g. linear programs, not integer programs).
- A basis is only given for linear programs when solved by the simplex method (e.g., not with PDLP).
- Solvers have widely varying support for returning primal and dual rays. E.g. a termination_reason of unbounded does not ensure that a feasible solution or a primal ray is returned, check termination_reasons.md for solver specific guarantees if this is needed. Further, many solvers will provide the ray but not the feasible solution when returning an unbounded status.
- When the termination reason is that a limit was reached or that the result is imprecise, a solution may or may not be present. Further, for some solvers (generally, convex optimization solvers, not MIP solvers), the primal or dual solution may not be feasible.
Solver specific output is also returned for some solvers (and only information for the solver used will be populated).
Attributes:
- termination: The reason the solver stopped.
- solve_stats: Statistics on the solve process, e.g. running time, iterations.
- solutions: Lexicographically by primal feasibility status, dual feasibility status, (basic dual feasibility for simplex solvers), primal objective value and dual objective value.
- primal_rays: Directions of unbounded primal improvement, or equivalently, dual infeasibility certificates. Typically provided for terminal reasons UNBOUNDED and DUAL_INFEASIBLE.
- dual_rays: Directions of unbounded dual improvement, or equivalently, primal infeasibility certificates. Typically provided for termination reason INFEASIBLE.
- gscip_specific_output: statistics returned by the gSCIP solver, if used.
- osqp_specific_output: statistics returned by the OSQP solver, if used.
- pdlp_specific_output: statistics returned by the PDLP solver, if used.
429 def solve_time(self) -> datetime.timedelta: 430 """Shortcut for SolveResult.solve_stats.solve_time.""" 431 return self.solve_stats.solve_time
Shortcut for SolveResult.solve_stats.solve_time.
433 def primal_bound(self) -> float: 434 """Returns a primal bound on the optimal objective value as described in ObjectiveBounds. 435 436 Will return a valid (possibly infinite) bound even if no primal feasible 437 solutions are available. 438 """ 439 return self.termination.objective_bounds.primal_bound
Returns a primal bound on the optimal objective value as described in ObjectiveBounds.
Will return a valid (possibly infinite) bound even if no primal feasible solutions are available.
441 def dual_bound(self) -> float: 442 """Returns a dual bound on the optimal objective value as described in ObjectiveBounds. 443 444 Will return a valid (possibly infinite) bound even if no dual feasible 445 solutions are available. 446 """ 447 return self.termination.objective_bounds.dual_bound
Returns a dual bound on the optimal objective value as described in ObjectiveBounds.
Will return a valid (possibly infinite) bound even if no dual feasible solutions are available.
449 def has_primal_feasible_solution(self) -> bool: 450 """Indicates if at least one primal feasible solution is available. 451 452 When termination.reason is TerminationReason.OPTIMAL or 453 TerminationReason.FEASIBLE, this is guaranteed to be true and need not be 454 checked. 455 456 Returns: 457 True if there is at least one primal feasible solution is available, 458 False, otherwise. 459 """ 460 if not self.solutions: 461 return False 462 return ( 463 self.solutions[0].primal_solution is not None 464 and self.solutions[0].primal_solution.feasibility_status 465 == solution.SolutionStatus.FEASIBLE 466 )
Indicates if at least one primal feasible solution is available.
When termination.reason is TerminationReason.OPTIMAL or TerminationReason.FEASIBLE, this is guaranteed to be true and need not be checked.
Returns:
True if there is at least one primal feasible solution is available, False, otherwise.
468 def objective_value(self) -> float: 469 """Returns the objective value of the best primal feasible solution. 470 471 An error will be raised if there are no primal feasible solutions. 472 primal_bound() above is guaranteed to be at least as good (larger or equal 473 for max problems and smaller or equal for min problems) as objective_value() 474 and will never raise an error, so it may be preferable in some cases. Note 475 that primal_bound() could be better than objective_value() even for optimal 476 terminations, but on such optimal termination, both should satisfy the 477 optimality tolerances. 478 479 Returns: 480 The objective value of the best primal feasible solution. 481 482 Raises: 483 ValueError: There are no primal feasible solutions. 484 """ 485 if not self.has_primal_feasible_solution(): 486 raise ValueError("No primal feasible solution available.") 487 assert self.solutions[0].primal_solution is not None 488 return self.solutions[0].primal_solution.objective_value
Returns the objective value of the best primal feasible solution.
An error will be raised if there are no primal feasible solutions. primal_bound() above is guaranteed to be at least as good (larger or equal for max problems and smaller or equal for min problems) as objective_value() and will never raise an error, so it may be preferable in some cases. Note that primal_bound() could be better than objective_value() even for optimal terminations, but on such optimal termination, both should satisfy the optimality tolerances.
Returns: The objective value of the best primal feasible solution.
Raises: ValueError: There are no primal feasible solutions.
490 def best_objective_bound(self) -> float: 491 """Returns a bound on the best possible objective value. 492 493 best_objective_bound() is always equal to dual_bound(), so they can be 494 used interchangeably. 495 """ 496 return self.termination.objective_bounds.dual_bound
Returns a bound on the best possible objective value.
best_objective_bound() is always equal to dual_bound(), so they can be used interchangeably.
507 def variable_values(self, variables=None): 508 """The variable values from the best primal feasible solution. 509 510 An error will be raised if there are no primal feasible solutions. 511 512 Args: 513 variables: an optional Variable or iterator of Variables indicating what 514 variable values to return. If not provided, variable_values returns a 515 dictionary with all the variable values for all variables. 516 517 Returns: 518 The variable values from the best primal feasible solution. 519 520 Raises: 521 ValueError: There are no primal feasible solutions. 522 TypeError: Argument is not None, a Variable or an iterable of Variables. 523 KeyError: Variable values requested for an invalid variable (e.g. is not a 524 Variable or is a variable for another model). 525 """ 526 if not self.has_primal_feasible_solution(): 527 raise ValueError("No primal feasible solution available.") 528 assert self.solutions[0].primal_solution is not None 529 if variables is None: 530 return self.solutions[0].primal_solution.variable_values 531 if isinstance(variables, model.Variable): 532 return self.solutions[0].primal_solution.variable_values[variables] 533 if isinstance(variables, Iterable): 534 return [ 535 self.solutions[0].primal_solution.variable_values[v] for v in variables 536 ] 537 raise TypeError( 538 "unsupported type in argument for " 539 f"variable_values: {type(variables).__name__!r}" 540 )
The variable values from the best primal feasible solution.
An error will be raised if there are no primal feasible solutions.
Arguments:
- variables: an optional Variable or iterator of Variables indicating what variable values to return. If not provided, variable_values returns a dictionary with all the variable values for all variables.
Returns:
The variable values from the best primal feasible solution.
Raises:
- ValueError: There are no primal feasible solutions.
- TypeError: Argument is not None, a Variable or an iterable of Variables.
- KeyError: Variable values requested for an invalid variable (e.g. is not a Variable or is a variable for another model).
542 def bounded(self) -> bool: 543 """Returns true only if the problem has been shown to be feasible and bounded.""" 544 return ( 545 self.termination.problem_status.primal_status == FeasibilityStatus.FEASIBLE 546 and self.termination.problem_status.dual_status 547 == FeasibilityStatus.FEASIBLE 548 )
Returns true only if the problem has been shown to be feasible and bounded.
550 def has_ray(self) -> bool: 551 """Indicates if at least one primal ray is available. 552 553 This is NOT guaranteed to be true when termination.reason is 554 TerminationReason.kUnbounded or TerminationReason.kInfeasibleOrUnbounded. 555 556 Returns: 557 True if at least one primal ray is available. 558 """ 559 return bool(self.primal_rays)
Indicates if at least one primal ray is available.
This is NOT guaranteed to be true when termination.reason is TerminationReason.kUnbounded or TerminationReason.kInfeasibleOrUnbounded.
Returns:
True if at least one primal ray is available.
574 def ray_variable_values(self, variables=None): 575 """The variable values from the first primal ray. 576 577 An error will be raised if there are no primal rays. 578 579 Args: 580 variables: an optional Variable or iterator of Variables indicating what 581 variable values to return. If not provided, variable_values() returns a 582 dictionary with the variable values for all variables. 583 584 Returns: 585 The variable values from the first primal ray. 586 587 Raises: 588 ValueError: There are no primal rays. 589 TypeError: Argument is not None, a Variable or an iterable of Variables. 590 KeyError: Variable values requested for an invalid variable (e.g. is not a 591 Variable or is a variable for another model). 592 """ 593 if not self.has_ray(): 594 raise ValueError("No primal ray available.") 595 if variables is None: 596 return self.primal_rays[0].variable_values 597 if isinstance(variables, model.Variable): 598 return self.primal_rays[0].variable_values[variables] 599 if isinstance(variables, Iterable): 600 return [self.primal_rays[0].variable_values[v] for v in variables] 601 raise TypeError( 602 "unsupported type in argument for " 603 f"ray_variable_values: {type(variables).__name__!r}" 604 )
The variable values from the first primal ray.
An error will be raised if there are no primal rays.
Arguments:
- variables: an optional Variable or iterator of Variables indicating what variable values to return. If not provided, variable_values() returns a dictionary with the variable values for all variables.
Returns:
The variable values from the first primal ray.
Raises:
- ValueError: There are no primal rays.
- TypeError: Argument is not None, a Variable or an iterable of Variables.
- KeyError: Variable values requested for an invalid variable (e.g. is not a Variable or is a variable for another model).
606 def has_dual_feasible_solution(self) -> bool: 607 """Indicates if the best solution has an associated dual feasible solution. 608 609 This is NOT guaranteed to be true when termination.reason is 610 TerminationReason.Optimal. It also may be true even when the best solution 611 does not have an associated primal feasible solution. 612 613 Returns: 614 True if the best solution has an associated dual feasible solution. 615 """ 616 if not self.solutions: 617 return False 618 return ( 619 self.solutions[0].dual_solution is not None 620 and self.solutions[0].dual_solution.feasibility_status 621 == solution.SolutionStatus.FEASIBLE 622 )
Indicates if the best solution has an associated dual feasible solution.
This is NOT guaranteed to be true when termination.reason is TerminationReason.Optimal. It also may be true even when the best solution does not have an associated primal feasible solution.
Returns:
True if the best solution has an associated dual feasible solution.
637 def dual_values(self, linear_constraints=None): 638 """The dual values associated to the best solution. 639 640 If there is at least one primal feasible solution, this corresponds to the 641 dual values associated to the best primal feasible solution. An error will 642 be raised if the best solution does not have an associated dual feasible 643 solution. 644 645 Args: 646 linear_constraints: an optional LinearConstraint or iterator of 647 LinearConstraint indicating what dual values to return. If not provided, 648 dual_values() returns a dictionary with the dual values for all linear 649 constraints. 650 651 Returns: 652 The dual values associated to the best solution. 653 654 Raises: 655 ValueError: The best solution does not have an associated dual feasible 656 solution. 657 TypeError: Argument is not None, a LinearConstraint or an iterable of 658 LinearConstraint. 659 KeyError: LinearConstraint values requested for an invalid 660 linear constraint (e.g. is not a LinearConstraint or is a linear 661 constraint for another model). 662 """ 663 if not self.has_dual_feasible_solution(): 664 raise ValueError(_NO_DUAL_SOLUTION_ERROR) 665 assert self.solutions[0].dual_solution is not None 666 if linear_constraints is None: 667 return self.solutions[0].dual_solution.dual_values 668 if isinstance(linear_constraints, model.LinearConstraint): 669 return self.solutions[0].dual_solution.dual_values[linear_constraints] 670 if isinstance(linear_constraints, Iterable): 671 return [ 672 self.solutions[0].dual_solution.dual_values[c] 673 for c in linear_constraints 674 ] 675 raise TypeError( 676 "unsupported type in argument for " 677 f"dual_values: {type(linear_constraints).__name__!r}" 678 )
The dual values associated to the best solution.
If there is at least one primal feasible solution, this corresponds to the dual values associated to the best primal feasible solution. An error will be raised if the best solution does not have an associated dual feasible solution.
Arguments:
- linear_constraints: an optional LinearConstraint or iterator of LinearConstraint indicating what dual values to return. If not provided, dual_values() returns a dictionary with the dual values for all linear constraints.
Returns:
The dual values associated to the best solution.
Raises:
- ValueError: The best solution does not have an associated dual feasible solution.
- TypeError: Argument is not None, a LinearConstraint or an iterable of LinearConstraint.
- KeyError: LinearConstraint values requested for an invalid linear constraint (e.g. is not a LinearConstraint or is a linear constraint for another model).
689 def reduced_costs(self, variables=None): 690 """The reduced costs associated to the best solution. 691 692 If there is at least one primal feasible solution, this corresponds to the 693 reduced costs associated to the best primal feasible solution. An error will 694 be raised if the best solution does not have an associated dual feasible 695 solution. 696 697 Args: 698 variables: an optional Variable or iterator of Variables indicating what 699 reduced costs to return. If not provided, reduced_costs() returns a 700 dictionary with the reduced costs for all variables. 701 702 Returns: 703 The reduced costs associated to the best solution. 704 705 Raises: 706 ValueError: The best solution does not have an associated dual feasible 707 solution. 708 TypeError: Argument is not None, a Variable or an iterable of Variables. 709 KeyError: Variable values requested for an invalid variable (e.g. is not a 710 Variable or is a variable for another model). 711 """ 712 if not self.has_dual_feasible_solution(): 713 raise ValueError(_NO_DUAL_SOLUTION_ERROR) 714 assert self.solutions[0].dual_solution is not None 715 if variables is None: 716 return self.solutions[0].dual_solution.reduced_costs 717 if isinstance(variables, model.Variable): 718 return self.solutions[0].dual_solution.reduced_costs[variables] 719 if isinstance(variables, Iterable): 720 return [self.solutions[0].dual_solution.reduced_costs[v] for v in variables] 721 raise TypeError( 722 "unsupported type in argument for " 723 f"reduced_costs: {type(variables).__name__!r}" 724 )
The reduced costs associated to the best solution.
If there is at least one primal feasible solution, this corresponds to the reduced costs associated to the best primal feasible solution. An error will be raised if the best solution does not have an associated dual feasible solution.
Arguments:
- variables: an optional Variable or iterator of Variables indicating what reduced costs to return. If not provided, reduced_costs() returns a dictionary with the reduced costs for all variables.
Returns:
The reduced costs associated to the best solution.
Raises:
- ValueError: The best solution does not have an associated dual feasible solution.
- TypeError: Argument is not None, a Variable or an iterable of Variables.
- KeyError: Variable values requested for an invalid variable (e.g. is not a Variable or is a variable for another model).
726 def has_dual_ray(self) -> bool: 727 """Indicates if at least one dual ray is available. 728 729 This is NOT guaranteed to be true when termination.reason is 730 TerminationReason.Infeasible. 731 732 Returns: 733 True if at least one dual ray is available. 734 """ 735 return bool(self.dual_rays)
Indicates if at least one dual ray is available.
This is NOT guaranteed to be true when termination.reason is TerminationReason.Infeasible.
Returns:
True if at least one dual ray is available.
750 def ray_dual_values(self, linear_constraints=None): 751 """The dual values from the first dual ray. 752 753 An error will be raised if there are no dual rays. 754 755 Args: 756 linear_constraints: an optional LinearConstraint or iterator of 757 LinearConstraint indicating what dual values to return. If not provided, 758 ray_dual_values() returns a dictionary with the dual values for all 759 linear constraints. 760 761 Returns: 762 The dual values from the first dual ray. 763 764 Raises: 765 ValueError: There are no dual rays. 766 TypeError: Argument is not None, a LinearConstraint or an iterable of 767 LinearConstraint. 768 KeyError: LinearConstraint values requested for an invalid 769 linear constraint (e.g. is not a LinearConstraint or is a linear 770 constraint for another model). 771 """ 772 if not self.has_dual_ray(): 773 raise ValueError("No dual ray available.") 774 if linear_constraints is None: 775 return self.dual_rays[0].dual_values 776 if isinstance(linear_constraints, model.LinearConstraint): 777 return self.dual_rays[0].dual_values[linear_constraints] 778 if isinstance(linear_constraints, Iterable): 779 return [self.dual_rays[0].dual_values[v] for v in linear_constraints] 780 raise TypeError( 781 "unsupported type in argument for " 782 f"ray_dual_values: {type(linear_constraints).__name__!r}" 783 )
The dual values from the first dual ray.
An error will be raised if there are no dual rays.
Arguments:
- linear_constraints: an optional LinearConstraint or iterator of LinearConstraint indicating what dual values to return. If not provided, ray_dual_values() returns a dictionary with the dual values for all linear constraints.
Returns:
The dual values from the first dual ray.
Raises:
- ValueError: There are no dual rays.
- TypeError: Argument is not None, a LinearConstraint or an iterable of LinearConstraint.
- KeyError: LinearConstraint values requested for an invalid linear constraint (e.g. is not a LinearConstraint or is a linear constraint for another model).
796 def ray_reduced_costs(self, variables=None): 797 """The reduced costs from the first dual ray. 798 799 An error will be raised if there are no dual rays. 800 801 Args: 802 variables: an optional Variable or iterator of Variables indicating what 803 reduced costs to return. If not provided, ray_reduced_costs() returns a 804 dictionary with the reduced costs for all variables. 805 806 Returns: 807 The reduced costs from the first dual ray. 808 809 Raises: 810 ValueError: There are no dual rays. 811 TypeError: Argument is not None, a Variable or an iterable of Variables. 812 KeyError: Variable values requested for an invalid variable (e.g. is not a 813 Variable or is a variable for another model). 814 """ 815 if not self.has_dual_ray(): 816 raise ValueError("No dual ray available.") 817 if variables is None: 818 return self.dual_rays[0].reduced_costs 819 if isinstance(variables, model.Variable): 820 return self.dual_rays[0].reduced_costs[variables] 821 if isinstance(variables, Iterable): 822 return [self.dual_rays[0].reduced_costs[v] for v in variables] 823 raise TypeError( 824 "unsupported type in argument for " 825 f"ray_reduced_costs: {type(variables).__name__!r}" 826 )
The reduced costs from the first dual ray.
An error will be raised if there are no dual rays.
Arguments:
- variables: an optional Variable or iterator of Variables indicating what reduced costs to return. If not provided, ray_reduced_costs() returns a dictionary with the reduced costs for all variables.
Returns:
The reduced costs from the first dual ray.
Raises:
- ValueError: There are no dual rays.
- TypeError: Argument is not None, a Variable or an iterable of Variables.
- KeyError: Variable values requested for an invalid variable (e.g. is not a Variable or is a variable for another model).
828 def has_basis(self) -> bool: 829 """Indicates if the best solution has an associated basis. 830 831 This is NOT guaranteed to be true when termination.reason is 832 TerminationReason.Optimal. It also may be true even when the best solution 833 does not have an associated primal feasible solution. 834 835 Returns: 836 True if the best solution has an associated basis. 837 """ 838 if not self.solutions: 839 return False 840 return self.solutions[0].basis is not None
Indicates if the best solution has an associated basis.
This is NOT guaranteed to be true when termination.reason is TerminationReason.Optimal. It also may be true even when the best solution does not have an associated primal feasible solution.
Returns:
True if the best solution has an associated basis.
857 def constraint_status(self, linear_constraints=None): 858 """The constraint basis status associated to the best solution. 859 860 If there is at least one primal feasible solution, this corresponds to the 861 basis associated to the best primal feasible solution. An error will 862 be raised if the best solution does not have an associated basis. 863 864 865 Args: 866 linear_constraints: an optional LinearConstraint or iterator of 867 LinearConstraint indicating what constraint statuses to return. If not 868 provided, returns a dictionary with the constraint statuses for all 869 linear constraints. 870 871 Returns: 872 The constraint basis status associated to the best solution. 873 874 Raises: 875 ValueError: The best solution does not have an associated basis. 876 TypeError: Argument is not None, a LinearConstraint or an iterable of 877 LinearConstraint. 878 KeyError: LinearConstraint values requested for an invalid 879 linear constraint (e.g. is not a LinearConstraint or is a linear 880 constraint for another model). 881 """ 882 if not self.has_basis(): 883 raise ValueError(_NO_BASIS_ERROR) 884 assert self.solutions[0].basis is not None 885 if linear_constraints is None: 886 return self.solutions[0].basis.constraint_status 887 if isinstance(linear_constraints, model.LinearConstraint): 888 return self.solutions[0].basis.constraint_status[linear_constraints] 889 if isinstance(linear_constraints, Iterable): 890 return [ 891 self.solutions[0].basis.constraint_status[c] for c in linear_constraints 892 ] 893 raise TypeError( 894 "unsupported type in argument for " 895 f"constraint_status: {type(linear_constraints).__name__!r}" 896 )
The constraint basis status associated to the best solution.
If there is at least one primal feasible solution, this corresponds to the basis associated to the best primal feasible solution. An error will be raised if the best solution does not have an associated basis.
Arguments:
- linear_constraints: an optional LinearConstraint or iterator of LinearConstraint indicating what constraint statuses to return. If not provided, returns a dictionary with the constraint statuses for all linear constraints.
Returns:
The constraint basis status associated to the best solution.
Raises:
- ValueError: The best solution does not have an associated basis.
- TypeError: Argument is not None, a LinearConstraint or an iterable of LinearConstraint.
- KeyError: LinearConstraint values requested for an invalid linear constraint (e.g. is not a LinearConstraint or is a linear constraint for another model).
911 def variable_status(self, variables=None): 912 """The variable basis status associated to the best solution. 913 914 If there is at least one primal feasible solution, this corresponds to the 915 basis associated to the best primal feasible solution. An error will 916 be raised if the best solution does not have an associated basis. 917 918 Args: 919 variables: an optional Variable or iterator of Variables indicating what 920 reduced costs to return. If not provided, variable_status() returns a 921 dictionary with the reduced costs for all variables. 922 923 Returns: 924 The variable basis status associated to the best solution. 925 926 Raises: 927 ValueError: The best solution does not have an associated basis. 928 TypeError: Argument is not None, a Variable or an iterable of Variables. 929 KeyError: Variable values requested for an invalid variable (e.g. is not a 930 Variable or is a variable for another model). 931 """ 932 if not self.has_basis(): 933 raise ValueError(_NO_BASIS_ERROR) 934 assert self.solutions[0].basis is not None 935 if variables is None: 936 return self.solutions[0].basis.variable_status 937 if isinstance(variables, model.Variable): 938 return self.solutions[0].basis.variable_status[variables] 939 if isinstance(variables, Iterable): 940 return [self.solutions[0].basis.variable_status[v] for v in variables] 941 raise TypeError( 942 "unsupported type in argument for " 943 f"variable_status: {type(variables).__name__!r}" 944 )
The variable basis status associated to the best solution.
If there is at least one primal feasible solution, this corresponds to the basis associated to the best primal feasible solution. An error will be raised if the best solution does not have an associated basis.
Arguments:
- variables: an optional Variable or iterator of Variables indicating what reduced costs to return. If not provided, variable_status() returns a dictionary with the reduced costs for all variables.
Returns:
The variable basis status associated to the best solution.
Raises:
- ValueError: The best solution does not have an associated basis.
- TypeError: Argument is not None, a Variable or an iterable of Variables.
- KeyError: Variable values requested for an invalid variable (e.g. is not a Variable or is a variable for another model).
946 def to_proto(self) -> result_pb2.SolveResultProto: 947 """Returns an equivalent protocol buffer for a SolveResult.""" 948 proto = result_pb2.SolveResultProto( 949 termination=self.termination.to_proto(), 950 solutions=[s.to_proto() for s in self.solutions], 951 primal_rays=[r.to_proto() for r in self.primal_rays], 952 dual_rays=[r.to_proto() for r in self.dual_rays], 953 solve_stats=self.solve_stats.to_proto(), 954 ) 955 956 # Ensure that at most solver has solver specific output. 957 existing_solver_specific_output = None 958 959 def has_solver_specific_output(solver_name: str) -> None: 960 nonlocal existing_solver_specific_output 961 if existing_solver_specific_output is not None: 962 raise ValueError( 963 "found solver specific output for both" 964 f" {existing_solver_specific_output} and {solver_name}" 965 ) 966 existing_solver_specific_output = solver_name 967 968 if self.gscip_specific_output is not None: 969 has_solver_specific_output("gscip") 970 proto.gscip_output.CopyFrom(self.gscip_specific_output) 971 if self.osqp_specific_output is not None: 972 has_solver_specific_output("osqp") 973 proto.osqp_output.CopyFrom(self.osqp_specific_output) 974 if self.pdlp_specific_output is not None: 975 has_solver_specific_output("pdlp") 976 proto.pdlp_output.CopyFrom(self.pdlp_specific_output) 977 return proto
Returns an equivalent protocol buffer for a SolveResult.
1011def parse_solve_result( 1012 proto: result_pb2.SolveResultProto, mod: model.Model 1013) -> SolveResult: 1014 """Returns a SolveResult equivalent to the input proto.""" 1015 result = SolveResult() 1016 # TODO(b/290091715): change to parse_termination(proto.termination) 1017 # once solve_stats proto no longer has best_primal/dual_bound/problem_status 1018 # and problem_status/objective_bounds are guaranteed to be present in 1019 # termination proto. 1020 result.termination = parse_termination(_upgrade_termination(proto)) 1021 result.solve_stats = parse_solve_stats(proto.solve_stats) 1022 for solution_proto in proto.solutions: 1023 result.solutions.append(solution.parse_solution(solution_proto, mod)) 1024 for primal_ray_proto in proto.primal_rays: 1025 result.primal_rays.append(solution.parse_primal_ray(primal_ray_proto, mod)) 1026 for dual_ray_proto in proto.dual_rays: 1027 result.dual_rays.append(solution.parse_dual_ray(dual_ray_proto, mod)) 1028 if proto.HasField("gscip_output"): 1029 result.gscip_specific_output = proto.gscip_output 1030 elif proto.HasField("osqp_output"): 1031 result.osqp_specific_output = proto.osqp_output 1032 elif proto.HasField("pdlp_output"): 1033 result.pdlp_specific_output = proto.pdlp_output 1034 return result
Returns a SolveResult equivalent to the input proto.