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