ortools.math_opt.python.result

The output from solving a mathematical optimization problem from model.py.

   1#!/usr/bin/env python3
   2# Copyright 2010-2025 Google LLC
   3# Licensed under the Apache License, Version 2.0 (the "License");
   4# you may not use this file except in compliance with the License.
   5# You may obtain a copy of the License at
   6#
   7#     http://www.apache.org/licenses/LICENSE-2.0
   8#
   9# Unless required by applicable law or agreed to in writing, software
  10# distributed under the License is distributed on an "AS IS" BASIS,
  11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12# See the License for the specific language governing permissions and
  13# limitations under the License.
  14
  15"""The output from solving a mathematical optimization problem from model.py."""
  16import dataclasses
  17import datetime
  18import enum
  19from typing import Dict, Iterable, List, Optional, overload
  20
  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
  27from ortools.math_opt.solvers.gscip import gscip_pb2
  28
  29_NO_DUAL_SOLUTION_ERROR = (
  30    "Best solution does not have an associated dual feasible solution."
  31)
  32_NO_BASIS_ERROR = "Best solution does not have an associated basis."
  33
  34
  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
  50
  51
  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        )
  98
  99
 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    )
 113
 114
 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        )
 161
 162
 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)
 168
 169
 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
 200
 201
 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
 211
 212
 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
 261
 262
 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
 305
 306
 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        )
 342
 343
 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
 371
 372
 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
1001
1002
1003def _get_problem_status(
1004    result_proto: result_pb2.SolveResultProto,
1005) -> result_pb2.ProblemStatusProto:
1006    if result_proto.termination.HasField("problem_status"):
1007        return result_proto.termination.problem_status
1008    return result_proto.solve_stats.problem_status
1009
1010
1011def _get_objective_bounds(
1012    result_proto: result_pb2.SolveResultProto,
1013) -> result_pb2.ObjectiveBoundsProto:
1014    if result_proto.termination.HasField("objective_bounds"):
1015        return result_proto.termination.objective_bounds
1016    return result_pb2.ObjectiveBoundsProto(
1017        primal_bound=result_proto.solve_stats.best_primal_bound,
1018        dual_bound=result_proto.solve_stats.best_dual_bound,
1019    )
1020
1021
1022def _upgrade_termination(
1023    result_proto: result_pb2.SolveResultProto,
1024) -> result_pb2.TerminationProto:
1025    return result_pb2.TerminationProto(
1026        reason=result_proto.termination.reason,
1027        limit=result_proto.termination.limit,
1028        detail=result_proto.termination.detail,
1029        problem_status=_get_problem_status(result_proto),
1030        objective_bounds=_get_objective_bounds(result_proto),
1031    )
1032
1033
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
@enum.unique
class FeasibilityStatus(enum.Enum):
36@enum.unique
37class FeasibilityStatus(enum.Enum):
38    """Problem feasibility status as claimed by the solver.
39
40      (solver is not required to return a certificate for the claim.)
41
42    Attributes:
43      UNDETERMINED: Solver does not claim a status.
44      FEASIBLE: Solver claims the problem is feasible.
45      INFEASIBLE: Solver claims the problem is infeasible.
46    """
47
48    UNDETERMINED = result_pb2.FEASIBILITY_STATUS_UNDETERMINED
49    FEASIBLE = result_pb2.FEASIBILITY_STATUS_FEASIBLE
50    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.
UNDETERMINED = <FeasibilityStatus.UNDETERMINED: 1>
FEASIBLE = <FeasibilityStatus.FEASIBLE: 2>
INFEASIBLE = <FeasibilityStatus.INFEASIBLE: 3>
@dataclasses.dataclass(frozen=True)
class ProblemStatus:
53@dataclasses.dataclass(frozen=True)
54class ProblemStatus:
55    """Feasibility status of the primal problem and its dual (or dual relaxation).
56
57    Statuses are as claimed by the solver and a dual relaxation is the dual of a
58    continuous relaxation for the original problem (e.g. the LP relaxation of a
59    MIP). The solver is not required to return a certificate for the feasibility
60    or infeasibility claims (e.g. the solver may claim primal feasibility without
61    returning a primal feasible solutuion). This combined status gives a
62    comprehensive description of a solver's claims about feasibility and
63    unboundedness of the solved problem. For instance,
64      * a feasible status for primal and dual problems indicates the primal is
65        feasible and bounded and likely has an optimal solution (guaranteed for
66        problems without non-linear constraints).
67      * a primal feasible and a dual infeasible status indicates the primal
68        problem is unbounded (i.e. has arbitrarily good solutions).
69    Note that a dual infeasible status by itself (i.e. accompanied by an
70    undetermined primal status) does not imply the primal problem is unbounded as
71    we could have both problems be infeasible. Also, while a primal and dual
72    feasible status may imply the existence of an optimal solution, it does not
73    guarantee the solver has actually found such optimal solution.
74
75    Attributes:
76      primal_status: Status for the primal problem.
77      dual_status: Status for the dual problem (or for the dual of a continuous
78        relaxation).
79      primal_or_dual_infeasible: If true, the solver claims the primal or dual
80        problem is infeasible, but it does not know which (or if both are
81        infeasible). Can be true only when primal_problem_status =
82        dual_problem_status = kUndetermined. This extra information is often
83        needed when preprocessing determines there is no optimal solution to the
84        problem (but can't determine if it is due to infeasibility, unboundedness,
85        or both).
86    """
87
88    primal_status: FeasibilityStatus = FeasibilityStatus.UNDETERMINED
89    dual_status: FeasibilityStatus = FeasibilityStatus.UNDETERMINED
90    primal_or_dual_infeasible: bool = False
91
92    def to_proto(self) -> result_pb2.ProblemStatusProto:
93        """Returns an equivalent proto for a problem status."""
94        return result_pb2.ProblemStatusProto(
95            primal_status=self.primal_status.value,
96            dual_status=self.dual_status.value,
97            primal_or_dual_infeasible=self.primal_or_dual_infeasible,
98        )

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).
ProblemStatus( primal_status: FeasibilityStatus = <FeasibilityStatus.UNDETERMINED: 1>, dual_status: FeasibilityStatus = <FeasibilityStatus.UNDETERMINED: 1>, primal_or_dual_infeasible: bool = False)
primal_or_dual_infeasible: bool = False
def to_proto(self) -> ortools.math_opt.result_pb2.ProblemStatusProto:
92    def to_proto(self) -> result_pb2.ProblemStatusProto:
93        """Returns an equivalent proto for a problem status."""
94        return result_pb2.ProblemStatusProto(
95            primal_status=self.primal_status.value,
96            dual_status=self.dual_status.value,
97            primal_or_dual_infeasible=self.primal_or_dual_infeasible,
98        )

Returns an equivalent proto for a problem status.

def parse_problem_status( proto: ortools.math_opt.result_pb2.ProblemStatusProto) -> ProblemStatus:
101def parse_problem_status(proto: result_pb2.ProblemStatusProto) -> ProblemStatus:
102    """Returns an equivalent ProblemStatus from the input proto."""
103    primal_status_proto = proto.primal_status
104    if primal_status_proto == result_pb2.FEASIBILITY_STATUS_UNSPECIFIED:
105        raise ValueError("Primal feasibility status should not be UNSPECIFIED")
106    dual_status_proto = proto.dual_status
107    if dual_status_proto == result_pb2.FEASIBILITY_STATUS_UNSPECIFIED:
108        raise ValueError("Dual feasibility status should not be UNSPECIFIED")
109    return ProblemStatus(
110        primal_status=FeasibilityStatus(primal_status_proto),
111        dual_status=FeasibilityStatus(dual_status_proto),
112        primal_or_dual_infeasible=proto.primal_or_dual_infeasible,
113    )

Returns an equivalent ProblemStatus from the input proto.

@dataclasses.dataclass(frozen=True)
class ObjectiveBounds:
116@dataclasses.dataclass(frozen=True)
117class ObjectiveBounds:
118    """Bounds on the optimal objective value.
119
120  MOE:begin_intracomment_strip
121  See go/mathopt-objective-bounds for more details.
122  MOE:end_intracomment_strip
123
124  Attributes:
125    primal_bound: Solver claims there exists a primal solution that is
126      numerically feasible (i.e. feasible up to the solvers tolerance), and
127      whose objective value is primal_bound.
128
129      The optimal value is equal or better (smaller for min objectives and
130      larger for max objectives) than primal_bound, but only up to
131      solver-tolerances.
132
133      MOE:begin_intracomment_strip
134      See go/mathopt-objective-bounds for more details.
135      MOE:end_intracomment_strip
136    dual_bound: Solver claims there exists a dual solution that is numerically
137      feasible (i.e. feasible up to the solvers tolerance), and whose objective
138      value is dual_bound.
139
140      For MIP solvers, the associated dual problem may be some continuous
141      relaxation (e.g. LP relaxation), but it is often an implicitly defined
142      problem that is a complex consequence of the solvers execution. For both
143      continuous and MIP solvers, the optimal value is equal or worse (larger
144      for min objective and smaller for max objectives) than dual_bound, but
145      only up to solver-tolerances. Some continuous solvers provide a
146      numerically safer dual bound through solver's specific output (e.g. for
147      PDLP, pdlp_output.convergence_information.corrected_dual_objective).
148
149      MOE:begin_intracomment_strip
150      See go/mathopt-objective-bounds for more details.
151      MOE:end_intracomment_strip
152  """  # fmt: skip
153
154    primal_bound: float = 0.0
155    dual_bound: float = 0.0
156
157    def to_proto(self) -> result_pb2.ObjectiveBoundsProto:
158        """Returns an equivalent proto for objective bounds."""
159        return result_pb2.ObjectiveBoundsProto(
160            primal_bound=self.primal_bound, dual_bound=self.dual_bound
161        )

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

ObjectiveBounds(primal_bound: float = 0.0, dual_bound: float = 0.0)
primal_bound: float = 0.0
dual_bound: float = 0.0
def to_proto(self) -> ortools.math_opt.result_pb2.ObjectiveBoundsProto:
157    def to_proto(self) -> result_pb2.ObjectiveBoundsProto:
158        """Returns an equivalent proto for objective bounds."""
159        return result_pb2.ObjectiveBoundsProto(
160            primal_bound=self.primal_bound, dual_bound=self.dual_bound
161        )

Returns an equivalent proto for objective bounds.

def parse_objective_bounds( proto: ortools.math_opt.result_pb2.ObjectiveBoundsProto) -> ObjectiveBounds:
164def parse_objective_bounds(
165    proto: result_pb2.ObjectiveBoundsProto,
166) -> ObjectiveBounds:
167    """Returns an equivalent ObjectiveBounds from the input proto."""
168    return ObjectiveBounds(primal_bound=proto.primal_bound, dual_bound=proto.dual_bound)

Returns an equivalent ObjectiveBounds from the input proto.

@dataclasses.dataclass
class SolveStats:
171@dataclasses.dataclass
172class SolveStats:
173    """Problem statuses and solve statistics returned by the solver.
174
175    Attributes:
176      solve_time: Elapsed wall clock time as measured by math_opt, roughly the
177        time inside solve(). Note: this does not include work done building the
178        model.
179      simplex_iterations: Simplex iterations.
180      barrier_iterations: Barrier iterations.
181      first_order_iterations: First order iterations.
182      node_count: Node count.
183    """
184
185    solve_time: datetime.timedelta = datetime.timedelta()
186    simplex_iterations: int = 0
187    barrier_iterations: int = 0
188    first_order_iterations: int = 0
189    node_count: int = 0
190
191    def to_proto(self) -> result_pb2.SolveStatsProto:
192        """Returns an equivalent proto for a solve stats."""
193        result = result_pb2.SolveStatsProto(
194            simplex_iterations=self.simplex_iterations,
195            barrier_iterations=self.barrier_iterations,
196            first_order_iterations=self.first_order_iterations,
197            node_count=self.node_count,
198        )
199        result.solve_time.FromTimedelta(self.solve_time)
200        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.
SolveStats( solve_time: datetime.timedelta = datetime.timedelta(0), simplex_iterations: int = 0, barrier_iterations: int = 0, first_order_iterations: int = 0, node_count: int = 0)
solve_time: datetime.timedelta = datetime.timedelta(0)
simplex_iterations: int = 0
barrier_iterations: int = 0
first_order_iterations: int = 0
node_count: int = 0
def to_proto(self) -> ortools.math_opt.result_pb2.SolveStatsProto:
191    def to_proto(self) -> result_pb2.SolveStatsProto:
192        """Returns an equivalent proto for a solve stats."""
193        result = result_pb2.SolveStatsProto(
194            simplex_iterations=self.simplex_iterations,
195            barrier_iterations=self.barrier_iterations,
196            first_order_iterations=self.first_order_iterations,
197            node_count=self.node_count,
198        )
199        result.solve_time.FromTimedelta(self.solve_time)
200        return result

Returns an equivalent proto for a solve stats.

def parse_solve_stats( proto: ortools.math_opt.result_pb2.SolveStatsProto) -> SolveStats:
203def parse_solve_stats(proto: result_pb2.SolveStatsProto) -> SolveStats:
204    """Returns an equivalent SolveStats from the input proto."""
205    result = SolveStats()
206    result.solve_time = proto.solve_time.ToTimedelta()
207    result.simplex_iterations = proto.simplex_iterations
208    result.barrier_iterations = proto.barrier_iterations
209    result.first_order_iterations = proto.first_order_iterations
210    result.node_count = proto.node_count
211    return result

Returns an equivalent SolveStats from the input proto.

@enum.unique
class TerminationReason(enum.Enum):
214@enum.unique
215class TerminationReason(enum.Enum):
216    """The reason a solve of a model terminated.
217
218    These reasons are typically as reported by the underlying solver, e.g. we do
219    not attempt to verify the precision of the solution returned.
220
221    The values are:
222       * OPTIMAL: A provably optimal solution (up to numerical tolerances) has
223           been found.
224       * INFEASIBLE: The primal problem has no feasible solutions.
225       * UNBOUNDED: The primal problem is feasible and arbitrarily good solutions
226           can be found along a primal ray.
227       * INFEASIBLE_OR_UNBOUNDED: The primal problem is either infeasible or
228           unbounded. More details on the problem status may be available in
229           solve_stats.problem_status. Note that Gurobi's unbounded status may be
230           mapped here as explained in
231           go/mathopt-solver-specific#gurobi-inf-or-unb.
232       * IMPRECISE: The problem was solved to one of the criteria above (Optimal,
233           Infeasible, Unbounded, or InfeasibleOrUnbounded), but one or more
234           tolerances was not met. Some primal/dual solutions/rays may be present,
235           but either they will be slightly infeasible, or (if the problem was
236           nearly optimal) their may be a gap between the best solution objective
237           and best objective bound.
238
239           Users can still query primal/dual solutions/rays and solution stats,
240           but they are responsible for dealing with the numerical imprecision.
241       * FEASIBLE: The optimizer reached some kind of limit and a primal feasible
242           solution is returned. See SolveResultProto.limit_detail for detailed
243           description of the kind of limit that was reached.
244       * NO_SOLUTION_FOUND: The optimizer reached some kind of limit and it did
245           not find a primal feasible solution. See SolveResultProto.limit_detail
246           for detailed description of the kind of limit that was reached.
247       * NUMERICAL_ERROR: The algorithm stopped because it encountered
248           unrecoverable numerical error. No solution information is present.
249       * OTHER_ERROR: The algorithm stopped because of an error not covered by one
250           of the statuses defined above. No solution information is present.
251    """
252
253    OPTIMAL = result_pb2.TERMINATION_REASON_OPTIMAL
254    INFEASIBLE = result_pb2.TERMINATION_REASON_INFEASIBLE
255    UNBOUNDED = result_pb2.TERMINATION_REASON_UNBOUNDED
256    INFEASIBLE_OR_UNBOUNDED = result_pb2.TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED
257    IMPRECISE = result_pb2.TERMINATION_REASON_IMPRECISE
258    FEASIBLE = result_pb2.TERMINATION_REASON_FEASIBLE
259    NO_SOLUTION_FOUND = result_pb2.TERMINATION_REASON_NO_SOLUTION_FOUND
260    NUMERICAL_ERROR = result_pb2.TERMINATION_REASON_NUMERICAL_ERROR
261    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.
OPTIMAL = <TerminationReason.OPTIMAL: 1>
INFEASIBLE = <TerminationReason.INFEASIBLE: 2>
UNBOUNDED = <TerminationReason.UNBOUNDED: 3>
INFEASIBLE_OR_UNBOUNDED = <TerminationReason.INFEASIBLE_OR_UNBOUNDED: 4>
IMPRECISE = <TerminationReason.IMPRECISE: 5>
FEASIBLE = <TerminationReason.FEASIBLE: 9>
NO_SOLUTION_FOUND = <TerminationReason.NO_SOLUTION_FOUND: 6>
NUMERICAL_ERROR = <TerminationReason.NUMERICAL_ERROR: 7>
OTHER_ERROR = <TerminationReason.OTHER_ERROR: 8>
@enum.unique
class Limit(enum.Enum):
264@enum.unique
265class Limit(enum.Enum):
266    """The optimizer reached a limit, partial solution information may be present.
267
268    Values are:
269       * UNDETERMINED: The underlying solver does not expose which limit was
270           reached.
271       * ITERATION: An iterative algorithm stopped after conducting the
272           maximum number of iterations (e.g. simplex or barrier iterations).
273       * TIME: The algorithm stopped after a user-specified amount of
274           computation time.
275       * NODE: A branch-and-bound algorithm stopped because it explored a
276           maximum number of nodes in the branch-and-bound tree.
277       * SOLUTION: The algorithm stopped because it found the required
278           number of solutions. This is often used in MIPs to get the solver to
279           return the first feasible solution it encounters.
280       * MEMORY: The algorithm stopped because it ran out of memory.
281       * OBJECTIVE: The algorithm stopped because it found a solution better
282           than a minimum limit set by the user.
283       * NORM: The algorithm stopped because the norm of an iterate became
284           too large.
285       * INTERRUPTED: The algorithm stopped because of an interrupt signal or a
286           user interrupt request.
287       * SLOW_PROGRESS: The algorithm stopped because it was unable to continue
288           making progress towards the solution.
289       * OTHER: The algorithm stopped due to a limit not covered by one of the
290           above. Note that UNDETERMINED is used when the reason cannot be
291           determined, and OTHER is used when the reason is known but does not fit
292           into any of the above alternatives.
293    """
294
295    UNDETERMINED = result_pb2.LIMIT_UNDETERMINED
296    ITERATION = result_pb2.LIMIT_ITERATION
297    TIME = result_pb2.LIMIT_TIME
298    NODE = result_pb2.LIMIT_NODE
299    SOLUTION = result_pb2.LIMIT_SOLUTION
300    MEMORY = result_pb2.LIMIT_MEMORY
301    OBJECTIVE = result_pb2.LIMIT_OBJECTIVE
302    NORM = result_pb2.LIMIT_NORM
303    INTERRUPTED = result_pb2.LIMIT_INTERRUPTED
304    SLOW_PROGRESS = result_pb2.LIMIT_SLOW_PROGRESS
305    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.
UNDETERMINED = <Limit.UNDETERMINED: 1>
ITERATION = <Limit.ITERATION: 2>
TIME = <Limit.TIME: 3>
NODE = <Limit.NODE: 4>
SOLUTION = <Limit.SOLUTION: 5>
MEMORY = <Limit.MEMORY: 6>
OBJECTIVE = <Limit.OBJECTIVE: 7>
NORM = <Limit.NORM: 8>
INTERRUPTED = <Limit.INTERRUPTED: 9>
SLOW_PROGRESS = <Limit.SLOW_PROGRESS: 10>
OTHER = <Limit.OTHER: 11>
@dataclasses.dataclass
class Termination:
308@dataclasses.dataclass
309class Termination:
310    """An explanation of why the solver stopped.
311
312    Attributes:
313      reason: Why the solver stopped, e.g. it found a provably optimal solution.
314        Additional information in `limit` when value is FEASIBLE or
315        NO_SOLUTION_FOUND, see `limit` for details.
316      limit: If the solver stopped early, what caused it to stop. Have value
317        UNSPECIFIED when reason is not NO_SOLUTION_FOUND or FEASIBLE. May still be
318        UNSPECIFIED when reason is NO_SOLUTION_FOUND or FEASIBLE, some solvers
319        cannot fill this in.
320      detail: Additional, information beyond reason about why the solver stopped,
321        typically solver specific.
322      problem_status: Feasibility statuses for primal and dual problems.
323      objective_bounds: Bounds on the optimal objective value.
324    """
325
326    reason: TerminationReason = TerminationReason.OPTIMAL
327    limit: Optional[Limit] = None
328    detail: str = ""
329    problem_status: ProblemStatus = ProblemStatus()
330    objective_bounds: ObjectiveBounds = ObjectiveBounds()
331
332    def to_proto(self) -> result_pb2.TerminationProto:
333        """Returns an equivalent protocol buffer to this Termination."""
334        return result_pb2.TerminationProto(
335            reason=self.reason.value,
336            limit=(
337                result_pb2.LIMIT_UNSPECIFIED if self.limit is None else self.limit.value
338            ),
339            detail=self.detail,
340            problem_status=self.problem_status.to_proto(),
341            objective_bounds=self.objective_bounds.to_proto(),
342        )

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, see limit 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.
Termination( reason: TerminationReason = <TerminationReason.OPTIMAL: 1>, limit: Limit | None = None, detail: str = '', problem_status: ProblemStatus = ProblemStatus(primal_status=<FeasibilityStatus.UNDETERMINED: 1>, dual_status=<FeasibilityStatus.UNDETERMINED: 1>, primal_or_dual_infeasible=False), objective_bounds: ObjectiveBounds = ObjectiveBounds(primal_bound=0.0, dual_bound=0.0))
limit: Limit | None = None
detail: str = ''
problem_status: ProblemStatus = ProblemStatus(primal_status=<FeasibilityStatus.UNDETERMINED: 1>, dual_status=<FeasibilityStatus.UNDETERMINED: 1>, primal_or_dual_infeasible=False)
objective_bounds: ObjectiveBounds = ObjectiveBounds(primal_bound=0.0, dual_bound=0.0)
def to_proto(self) -> ortools.math_opt.result_pb2.TerminationProto:
332    def to_proto(self) -> result_pb2.TerminationProto:
333        """Returns an equivalent protocol buffer to this Termination."""
334        return result_pb2.TerminationProto(
335            reason=self.reason.value,
336            limit=(
337                result_pb2.LIMIT_UNSPECIFIED if self.limit is None else self.limit.value
338            ),
339            detail=self.detail,
340            problem_status=self.problem_status.to_proto(),
341            objective_bounds=self.objective_bounds.to_proto(),
342        )

Returns an equivalent protocol buffer to this Termination.

def parse_termination( termination_proto: ortools.math_opt.result_pb2.TerminationProto) -> Termination:
345def parse_termination(
346    termination_proto: result_pb2.TerminationProto,
347) -> Termination:
348    """Returns a Termination that is equivalent to termination_proto."""
349    reason_proto = termination_proto.reason
350    limit_proto = termination_proto.limit
351    if reason_proto == result_pb2.TERMINATION_REASON_UNSPECIFIED:
352        raise ValueError("Termination reason should not be UNSPECIFIED")
353    reason_is_limit = (
354        reason_proto == result_pb2.TERMINATION_REASON_NO_SOLUTION_FOUND
355    ) or (reason_proto == result_pb2.TERMINATION_REASON_FEASIBLE)
356    limit_set = limit_proto != result_pb2.LIMIT_UNSPECIFIED
357    if reason_is_limit != limit_set:
358        raise ValueError(
359            f"Termination limit (={limit_proto})) should take value other than "
360            f"UNSPECIFIED if and only if termination reason (={reason_proto}) is "
361            "FEASIBLE or NO_SOLUTION_FOUND"
362        )
363    termination = Termination()
364    termination.reason = TerminationReason(reason_proto)
365    termination.limit = Limit(limit_proto) if limit_set else None
366    termination.detail = termination_proto.detail
367    termination.problem_status = parse_problem_status(termination_proto.problem_status)
368    termination.objective_bounds = parse_objective_bounds(
369        termination_proto.objective_bounds
370    )
371    return termination

Returns a Termination that is equivalent to termination_proto.

@dataclasses.dataclass
class SolveResult:
 374@dataclasses.dataclass
 375class SolveResult:
 376    """The result of solving an optimization problem defined by a Model.
 377
 378    We attempt to return as much solution information (primal_solutions,
 379    primal_rays, dual_solutions, dual_rays) as each underlying solver will provide
 380    given its return status. Differences in the underlying solvers result in a
 381    weak contract on what fields will be populated for a given termination
 382    reason. This is discussed in detail in termination_reasons.md, and the most
 383    important points are summarized below:
 384      * When the termination reason is optimal, there will be at least one primal
 385        solution provided that will be feasible up to the underlying solver's
 386        tolerances.
 387      * Dual solutions are only given for convex optimization problems (e.g.
 388        linear programs, not integer programs).
 389      * A basis is only given for linear programs when solved by the simplex
 390        method (e.g., not with PDLP).
 391      * Solvers have widely varying support for returning primal and dual rays.
 392        E.g. a termination_reason of unbounded does not ensure that a feasible
 393        solution or a primal ray is returned, check termination_reasons.md for
 394        solver specific guarantees if this is needed. Further, many solvers will
 395        provide the ray but not the feasible solution when returning an unbounded
 396        status.
 397      * When the termination reason is that a limit was reached or that the result
 398        is imprecise, a solution may or may not be present. Further, for some
 399        solvers (generally, convex optimization solvers, not MIP solvers), the
 400        primal or dual solution may not be feasible.
 401
 402    Solver specific output is also returned for some solvers (and only information
 403    for the solver used will be populated).
 404
 405    Attributes:
 406      termination: The reason the solver stopped.
 407      solve_stats: Statistics on the solve process, e.g. running time, iterations.
 408      solutions: Lexicographically by primal feasibility status, dual feasibility
 409        status, (basic dual feasibility for simplex solvers), primal objective
 410        value and dual objective value.
 411      primal_rays: Directions of unbounded primal improvement, or equivalently,
 412        dual infeasibility certificates. Typically provided for terminal reasons
 413        UNBOUNDED and DUAL_INFEASIBLE.
 414      dual_rays: Directions of unbounded dual improvement, or equivalently, primal
 415        infeasibility certificates. Typically provided for termination reason
 416        INFEASIBLE.
 417      gscip_specific_output: statistics returned by the gSCIP solver, if used.
 418      osqp_specific_output: statistics returned by the OSQP solver, if used.
 419      pdlp_specific_output: statistics returned by the PDLP solver, if used.
 420    """
 421
 422    termination: Termination = dataclasses.field(default_factory=Termination)
 423    solve_stats: SolveStats = dataclasses.field(default_factory=SolveStats)
 424    solutions: List[solution.Solution] = dataclasses.field(default_factory=list)
 425    primal_rays: List[solution.PrimalRay] = dataclasses.field(default_factory=list)
 426    dual_rays: List[solution.DualRay] = dataclasses.field(default_factory=list)
 427    # At most one of the below will be set
 428    gscip_specific_output: Optional[gscip_pb2.GScipOutput] = None
 429    osqp_specific_output: Optional[osqp_pb2.OsqpOutput] = None
 430    pdlp_specific_output: Optional[result_pb2.SolveResultProto.PdlpOutput] = None
 431
 432    def solve_time(self) -> datetime.timedelta:
 433        """Shortcut for SolveResult.solve_stats.solve_time."""
 434        return self.solve_stats.solve_time
 435
 436    def primal_bound(self) -> float:
 437        """Returns a primal bound on the optimal objective value as described in ObjectiveBounds.
 438
 439        Will return a valid (possibly infinite) bound even if no primal feasible
 440        solutions are available.
 441        """
 442        return self.termination.objective_bounds.primal_bound
 443
 444    def dual_bound(self) -> float:
 445        """Returns a dual bound on the optimal objective value as described in ObjectiveBounds.
 446
 447        Will return a valid (possibly infinite) bound even if no dual feasible
 448        solutions are available.
 449        """
 450        return self.termination.objective_bounds.dual_bound
 451
 452    def has_primal_feasible_solution(self) -> bool:
 453        """Indicates if at least one primal feasible solution is available.
 454
 455        When termination.reason is TerminationReason.OPTIMAL or
 456        TerminationReason.FEASIBLE, this is guaranteed to be true and need not be
 457        checked.
 458
 459        Returns:
 460          True if there is at least one primal feasible solution is available,
 461          False, otherwise.
 462        """
 463        if not self.solutions:
 464            return False
 465        sol = self.solutions[0]
 466        return (
 467            sol.primal_solution is not None
 468            and sol.primal_solution.feasibility_status
 469            == solution.SolutionStatus.FEASIBLE
 470        )
 471
 472    def objective_value(self) -> float:
 473        """Returns the objective value of the best primal feasible solution.
 474
 475        An error will be raised if there are no primal feasible solutions.
 476        primal_bound() above is guaranteed to be at least as good (larger or equal
 477        for max problems and smaller or equal for min problems) as objective_value()
 478        and will never raise an error, so it may be preferable in some cases. Note
 479        that primal_bound() could be better than objective_value() even for optimal
 480        terminations, but on such optimal termination, both should satisfy the
 481        optimality tolerances.
 482
 483         Returns:
 484           The objective value of the best primal feasible solution.
 485
 486         Raises:
 487           ValueError: There are no primal feasible solutions.
 488        """
 489        if not self.has_primal_feasible_solution():
 490            raise ValueError("No primal feasible solution available.")
 491        sol = self.solutions[0]
 492        assert sol.primal_solution is not None
 493        return sol.primal_solution.objective_value
 494
 495    def best_objective_bound(self) -> float:
 496        """Returns a bound on the best possible objective value.
 497
 498        best_objective_bound() is always equal to dual_bound(), so they can be
 499        used interchangeably.
 500        """
 501        return self.termination.objective_bounds.dual_bound
 502
 503    @overload
 504    def variable_values(
 505        self, variables: None = ...
 506    ) -> Dict[variables_mod.Variable, float]: ...
 507
 508    @overload
 509    def variable_values(self, variables: variables_mod.Variable) -> float: ...
 510
 511    @overload
 512    def variable_values(
 513        self, variables: Iterable[variables_mod.Variable]
 514    ) -> List[float]: ...
 515
 516    def variable_values(self, variables=None):
 517        """The variable values from the best primal feasible solution.
 518
 519        An error will be raised if there are no primal feasible solutions.
 520
 521        Args:
 522          variables: an optional Variable or iterator of Variables indicating what
 523            variable values to return. If not provided, variable_values returns a
 524            dictionary with all the variable values for all variables.
 525
 526        Returns:
 527          The variable values from the best primal feasible solution.
 528
 529        Raises:
 530          ValueError: There are no primal feasible solutions.
 531          TypeError: Argument is not None, a Variable or an iterable of Variables.
 532          KeyError: Variable values requested for an invalid variable (e.g. is not a
 533            Variable or is a variable for another model).
 534        """
 535        if not self.has_primal_feasible_solution():
 536            raise ValueError("No primal feasible solution available.")
 537        sol = self.solutions[0]
 538        assert sol.primal_solution is not None
 539        if variables is None:
 540            return sol.primal_solution.variable_values
 541        if isinstance(variables, variables_mod.Variable):
 542            return sol.primal_solution.variable_values[variables]
 543        if isinstance(variables, Iterable):
 544            return [sol.primal_solution.variable_values[v] for v in variables]
 545        raise TypeError(
 546            "unsupported type in argument for "
 547            f"variable_values: {type(variables).__name__!r}"
 548        )
 549
 550    def bounded(self) -> bool:
 551        """Returns true only if the problem has been shown to be feasible and bounded."""
 552        return (
 553            self.termination.problem_status.primal_status == FeasibilityStatus.FEASIBLE
 554            and self.termination.problem_status.dual_status
 555            == FeasibilityStatus.FEASIBLE
 556        )
 557
 558    def has_ray(self) -> bool:
 559        """Indicates if at least one primal ray is available.
 560
 561        This is NOT guaranteed to be true when termination.reason is
 562        TerminationReason.kUnbounded or TerminationReason.kInfeasibleOrUnbounded.
 563
 564        Returns:
 565          True if at least one primal ray is available.
 566        """
 567        return bool(self.primal_rays)
 568
 569    @overload
 570    def ray_variable_values(
 571        self, variables: None = ...
 572    ) -> Dict[variables_mod.Variable, float]: ...
 573
 574    @overload
 575    def ray_variable_values(self, variables: variables_mod.Variable) -> float: ...
 576
 577    @overload
 578    def ray_variable_values(
 579        self, variables: Iterable[variables_mod.Variable]
 580    ) -> List[float]: ...
 581
 582    def ray_variable_values(self, variables=None):
 583        """The variable values from the first primal ray.
 584
 585        An error will be raised if there are no primal rays.
 586
 587        Args:
 588          variables: an optional Variable or iterator of Variables indicating what
 589            variable values to return. If not provided, variable_values() returns a
 590            dictionary with the variable values for all variables.
 591
 592        Returns:
 593          The variable values from the first primal ray.
 594
 595        Raises:
 596          ValueError: There are no primal rays.
 597          TypeError: Argument is not None, a Variable or an iterable of Variables.
 598          KeyError: Variable values requested for an invalid variable (e.g. is not a
 599            Variable or is a variable for another model).
 600        """
 601        if not self.has_ray():
 602            raise ValueError("No primal ray available.")
 603        if variables is None:
 604            return self.primal_rays[0].variable_values
 605        if isinstance(variables, variables_mod.Variable):
 606            return self.primal_rays[0].variable_values[variables]
 607        if isinstance(variables, Iterable):
 608            return [self.primal_rays[0].variable_values[v] for v in variables]
 609        raise TypeError(
 610            "unsupported type in argument for "
 611            f"ray_variable_values: {type(variables).__name__!r}"
 612        )
 613
 614    def has_dual_feasible_solution(self) -> bool:
 615        """Indicates if the best solution has an associated dual feasible solution.
 616
 617        This is NOT guaranteed to be true when termination.reason is
 618        TerminationReason.Optimal. It also may be true even when the best solution
 619        does not have an associated primal feasible solution.
 620
 621        Returns:
 622          True if the best solution has an associated dual feasible solution.
 623        """
 624        if not self.solutions:
 625            return False
 626        sol = self.solutions[0]
 627        return (
 628            sol.dual_solution is not None
 629            and sol.dual_solution.feasibility_status == solution.SolutionStatus.FEASIBLE
 630        )
 631
 632    @overload
 633    def dual_values(
 634        self, linear_constraints: None = ...
 635    ) -> Dict[linear_constraints_mod.LinearConstraint, float]: ...
 636
 637    @overload
 638    def dual_values(
 639        self, linear_constraints: linear_constraints_mod.LinearConstraint
 640    ) -> float: ...
 641
 642    @overload
 643    def dual_values(
 644        self,
 645        linear_constraints: Iterable[linear_constraints_mod.LinearConstraint],
 646    ) -> List[float]: ...
 647
 648    def dual_values(self, linear_constraints=None):
 649        """The dual values associated to the best solution.
 650
 651        If there is at least one primal feasible solution, this corresponds to the
 652        dual values associated to the best primal feasible solution. An error will
 653        be raised if the best solution does not have an associated dual feasible
 654        solution.
 655
 656        Args:
 657          linear_constraints: an optional LinearConstraint or iterator of
 658            LinearConstraint indicating what dual values to return. If not provided,
 659            dual_values() returns a dictionary with the dual values for all linear
 660            constraints.
 661
 662        Returns:
 663          The dual values associated to the best solution.
 664
 665        Raises:
 666          ValueError: The best solution does not have an associated dual feasible
 667            solution.
 668          TypeError: Argument is not None, a LinearConstraint or an iterable of
 669            LinearConstraint.
 670          KeyError: LinearConstraint values requested for an invalid
 671            linear constraint (e.g. is not a LinearConstraint or is a linear
 672            constraint for another model).
 673        """
 674        if not self.has_dual_feasible_solution():
 675            raise ValueError(_NO_DUAL_SOLUTION_ERROR)
 676        sol = self.solutions[0]
 677        assert sol.dual_solution is not None
 678        if linear_constraints is None:
 679            return sol.dual_solution.dual_values
 680        if isinstance(linear_constraints, linear_constraints_mod.LinearConstraint):
 681            return sol.dual_solution.dual_values[linear_constraints]
 682        if isinstance(linear_constraints, Iterable):
 683            return [sol.dual_solution.dual_values[c] for c in linear_constraints]
 684        raise TypeError(
 685            "unsupported type in argument for "
 686            f"dual_values: {type(linear_constraints).__name__!r}"
 687        )
 688
 689    @overload
 690    def reduced_costs(
 691        self, variables: None = ...
 692    ) -> Dict[variables_mod.Variable, float]: ...
 693
 694    @overload
 695    def reduced_costs(self, variables: variables_mod.Variable) -> float: ...
 696
 697    @overload
 698    def reduced_costs(
 699        self, variables: Iterable[variables_mod.Variable]
 700    ) -> List[float]: ...
 701
 702    def reduced_costs(self, variables=None):
 703        """The reduced costs associated to the best solution.
 704
 705        If there is at least one primal feasible solution, this corresponds to the
 706        reduced costs associated to the best primal feasible solution. An error will
 707        be raised if the best solution does not have an associated dual feasible
 708        solution.
 709
 710        Args:
 711          variables: an optional Variable or iterator of Variables indicating what
 712            reduced costs to return. If not provided, reduced_costs() returns a
 713            dictionary with the reduced costs for all variables.
 714
 715        Returns:
 716          The reduced costs associated to the best solution.
 717
 718        Raises:
 719          ValueError: The best solution does not have an associated dual feasible
 720            solution.
 721          TypeError: Argument is not None, a Variable or an iterable of Variables.
 722          KeyError: Variable values requested for an invalid variable (e.g. is not a
 723            Variable or is a variable for another model).
 724        """
 725        if not self.has_dual_feasible_solution():
 726            raise ValueError(_NO_DUAL_SOLUTION_ERROR)
 727        sol = self.solutions[0]
 728        assert sol.dual_solution is not None
 729        if variables is None:
 730            return sol.dual_solution.reduced_costs
 731        if isinstance(variables, variables_mod.Variable):
 732            return sol.dual_solution.reduced_costs[variables]
 733        if isinstance(variables, Iterable):
 734            return [sol.dual_solution.reduced_costs[v] for v in variables]
 735        raise TypeError(
 736            "unsupported type in argument for "
 737            f"reduced_costs: {type(variables).__name__!r}"
 738        )
 739
 740    def has_dual_ray(self) -> bool:
 741        """Indicates if at least one dual ray is available.
 742
 743        This is NOT guaranteed to be true when termination.reason is
 744        TerminationReason.Infeasible.
 745
 746        Returns:
 747          True if at least one dual ray is available.
 748        """
 749        return bool(self.dual_rays)
 750
 751    @overload
 752    def ray_dual_values(
 753        self, linear_constraints: None = ...
 754    ) -> Dict[linear_constraints_mod.LinearConstraint, float]: ...
 755
 756    @overload
 757    def ray_dual_values(
 758        self, linear_constraints: linear_constraints_mod.LinearConstraint
 759    ) -> float: ...
 760
 761    @overload
 762    def ray_dual_values(
 763        self,
 764        linear_constraints: Iterable[linear_constraints_mod.LinearConstraint],
 765    ) -> List[float]: ...
 766
 767    def ray_dual_values(self, linear_constraints=None):
 768        """The dual values from the first dual ray.
 769
 770        An error will be raised if there are no dual rays.
 771
 772        Args:
 773          linear_constraints: an optional LinearConstraint or iterator of
 774            LinearConstraint indicating what dual values to return. If not provided,
 775            ray_dual_values() returns a dictionary with the dual values for all
 776            linear constraints.
 777
 778        Returns:
 779          The dual values from the first dual ray.
 780
 781        Raises:
 782          ValueError: There are no dual rays.
 783          TypeError: Argument is not None, a LinearConstraint or an iterable of
 784            LinearConstraint.
 785          KeyError: LinearConstraint values requested for an invalid
 786            linear constraint (e.g. is not a LinearConstraint or is a linear
 787            constraint for another model).
 788        """
 789        if not self.has_dual_ray():
 790            raise ValueError("No dual ray available.")
 791        ray = self.dual_rays[0]
 792        if linear_constraints is None:
 793            return ray.dual_values
 794        if isinstance(linear_constraints, linear_constraints_mod.LinearConstraint):
 795            return ray.dual_values[linear_constraints]
 796        if isinstance(linear_constraints, Iterable):
 797            return [ray.dual_values[v] for v in linear_constraints]
 798        raise TypeError(
 799            "unsupported type in argument for "
 800            f"ray_dual_values: {type(linear_constraints).__name__!r}"
 801        )
 802
 803    @overload
 804    def ray_reduced_costs(
 805        self, variables: None = ...
 806    ) -> Dict[variables_mod.Variable, float]: ...
 807
 808    @overload
 809    def ray_reduced_costs(self, variables: variables_mod.Variable) -> float: ...
 810
 811    @overload
 812    def ray_reduced_costs(
 813        self, variables: Iterable[variables_mod.Variable]
 814    ) -> List[float]: ...
 815
 816    def ray_reduced_costs(self, variables=None):
 817        """The reduced costs from the first dual ray.
 818
 819        An error will be raised if there are no dual rays.
 820
 821        Args:
 822          variables: an optional Variable or iterator of Variables indicating what
 823            reduced costs to return. If not provided, ray_reduced_costs() returns a
 824            dictionary with the reduced costs for all variables.
 825
 826        Returns:
 827          The reduced costs from the first dual ray.
 828
 829        Raises:
 830          ValueError: There are no dual rays.
 831          TypeError: Argument is not None, a Variable or an iterable of Variables.
 832          KeyError: Variable values requested for an invalid variable (e.g. is not a
 833            Variable or is a variable for another model).
 834        """
 835        if not self.has_dual_ray():
 836            raise ValueError("No dual ray available.")
 837        ray = self.dual_rays[0]
 838        if variables is None:
 839            return ray.reduced_costs
 840        if isinstance(variables, variables_mod.Variable):
 841            return ray.reduced_costs[variables]
 842        if isinstance(variables, Iterable):
 843            return [ray.reduced_costs[v] for v in variables]
 844        raise TypeError(
 845            "unsupported type in argument for "
 846            f"ray_reduced_costs: {type(variables).__name__!r}"
 847        )
 848
 849    def has_basis(self) -> bool:
 850        """Indicates if the best solution has an associated basis.
 851
 852        This is NOT guaranteed to be true when termination.reason is
 853        TerminationReason.Optimal. It also may be true even when the best solution
 854        does not have an associated primal feasible solution.
 855
 856        Returns:
 857          True if the best solution has an associated basis.
 858        """
 859        if not self.solutions:
 860            return False
 861        return self.solutions[0].basis is not None
 862
 863    @overload
 864    def constraint_status(
 865        self, linear_constraints: None = ...
 866    ) -> Dict[linear_constraints_mod.LinearConstraint, solution.BasisStatus]: ...
 867
 868    @overload
 869    def constraint_status(
 870        self, linear_constraints: linear_constraints_mod.LinearConstraint
 871    ) -> solution.BasisStatus: ...
 872
 873    @overload
 874    def constraint_status(
 875        self,
 876        linear_constraints: Iterable[linear_constraints_mod.LinearConstraint],
 877    ) -> List[solution.BasisStatus]: ...
 878
 879    def constraint_status(self, linear_constraints=None):
 880        """The constraint basis status associated to the best solution.
 881
 882        If there is at least one primal feasible solution, this corresponds to the
 883        basis associated to the best primal feasible solution. An error will
 884        be raised if the best solution does not have an associated basis.
 885
 886
 887        Args:
 888          linear_constraints: an optional LinearConstraint or iterator of
 889            LinearConstraint indicating what constraint statuses to return. If not
 890            provided, returns a dictionary with the constraint statuses for all
 891            linear constraints.
 892
 893        Returns:
 894          The constraint basis status associated to the best solution.
 895
 896        Raises:
 897          ValueError: The best solution does not have an associated basis.
 898          TypeError: Argument is not None, a LinearConstraint or an iterable of
 899            LinearConstraint.
 900          KeyError: LinearConstraint values requested for an invalid
 901            linear constraint (e.g. is not a LinearConstraint or is a linear
 902            constraint for another model).
 903        """
 904        if not self.has_basis():
 905            raise ValueError(_NO_BASIS_ERROR)
 906        basis = self.solutions[0].basis
 907        assert basis is not None
 908        if linear_constraints is None:
 909            return basis.constraint_status
 910        if isinstance(linear_constraints, linear_constraints_mod.LinearConstraint):
 911            return basis.constraint_status[linear_constraints]
 912        if isinstance(linear_constraints, Iterable):
 913            return [basis.constraint_status[c] for c in linear_constraints]
 914        raise TypeError(
 915            "unsupported type in argument for "
 916            f"constraint_status: {type(linear_constraints).__name__!r}"
 917        )
 918
 919    @overload
 920    def variable_status(
 921        self, variables: None = ...
 922    ) -> Dict[variables_mod.Variable, solution.BasisStatus]: ...
 923
 924    @overload
 925    def variable_status(
 926        self, variables: variables_mod.Variable
 927    ) -> solution.BasisStatus: ...
 928
 929    @overload
 930    def variable_status(
 931        self, variables: Iterable[variables_mod.Variable]
 932    ) -> List[solution.BasisStatus]: ...
 933
 934    def variable_status(self, variables=None):
 935        """The variable basis status associated to the best solution.
 936
 937        If there is at least one primal feasible solution, this corresponds to the
 938        basis associated to the best primal feasible solution. An error will
 939        be raised if the best solution does not have an associated basis.
 940
 941        Args:
 942          variables: an optional Variable or iterator of Variables indicating what
 943            reduced costs to return. If not provided, variable_status() returns a
 944            dictionary with the reduced costs for all variables.
 945
 946        Returns:
 947          The variable basis status associated to the best solution.
 948
 949        Raises:
 950          ValueError: The best solution does not have an associated basis.
 951          TypeError: Argument is not None, a Variable or an iterable of Variables.
 952          KeyError: Variable values requested for an invalid variable (e.g. is not a
 953            Variable or is a variable for another model).
 954        """
 955        if not self.has_basis():
 956            raise ValueError(_NO_BASIS_ERROR)
 957        basis = self.solutions[0].basis
 958        assert basis is not None
 959        if variables is None:
 960            return basis.variable_status
 961        if isinstance(variables, variables_mod.Variable):
 962            return basis.variable_status[variables]
 963        if isinstance(variables, Iterable):
 964            return [basis.variable_status[v] for v in variables]
 965        raise TypeError(
 966            "unsupported type in argument for "
 967            f"variable_status: {type(variables).__name__!r}"
 968        )
 969
 970    def to_proto(self) -> result_pb2.SolveResultProto:
 971        """Returns an equivalent protocol buffer for a SolveResult."""
 972        proto = result_pb2.SolveResultProto(
 973            termination=self.termination.to_proto(),
 974            solutions=[s.to_proto() for s in self.solutions],
 975            primal_rays=[r.to_proto() for r in self.primal_rays],
 976            dual_rays=[r.to_proto() for r in self.dual_rays],
 977            solve_stats=self.solve_stats.to_proto(),
 978        )
 979
 980        # Ensure that at most solver has solver specific output.
 981        existing_solver_specific_output = None
 982
 983        def has_solver_specific_output(solver_name: str) -> None:
 984            nonlocal existing_solver_specific_output
 985            if existing_solver_specific_output is not None:
 986                raise ValueError(
 987                    "found solver specific output for both"
 988                    f" {existing_solver_specific_output} and {solver_name}"
 989                )
 990            existing_solver_specific_output = solver_name
 991
 992        if self.gscip_specific_output is not None:
 993            has_solver_specific_output("gscip")
 994            proto.gscip_output.CopyFrom(self.gscip_specific_output)
 995        if self.osqp_specific_output is not None:
 996            has_solver_specific_output("osqp")
 997            proto.osqp_output.CopyFrom(self.osqp_specific_output)
 998        if self.pdlp_specific_output is not None:
 999            has_solver_specific_output("pdlp")
1000            proto.pdlp_output.CopyFrom(self.pdlp_specific_output)
1001        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.
SolveResult( termination: Termination = <factory>, solve_stats: SolveStats = <factory>, solutions: List[ortools.math_opt.python.solution.Solution] = <factory>, primal_rays: List[ortools.math_opt.python.solution.PrimalRay] = <factory>, dual_rays: List[ortools.math_opt.python.solution.DualRay] = <factory>, gscip_specific_output: ortools.math_opt.solvers.gscip.gscip_pb2.GScipOutput | None = None, osqp_specific_output: ortools.math_opt.solvers.osqp_pb2.OsqpOutput | None = None, pdlp_specific_output: ortools.math_opt.result_pb2.SolveResultProto.PdlpOutput | None = None)
termination: Termination
solve_stats: SolveStats
gscip_specific_output: ortools.math_opt.solvers.gscip.gscip_pb2.GScipOutput | None = None
osqp_specific_output: ortools.math_opt.solvers.osqp_pb2.OsqpOutput | None = None
pdlp_specific_output: ortools.math_opt.result_pb2.SolveResultProto.PdlpOutput | None = None
def solve_time(self) -> datetime.timedelta:
432    def solve_time(self) -> datetime.timedelta:
433        """Shortcut for SolveResult.solve_stats.solve_time."""
434        return self.solve_stats.solve_time

Shortcut for SolveResult.solve_stats.solve_time.

def primal_bound(self) -> float:
436    def primal_bound(self) -> float:
437        """Returns a primal bound on the optimal objective value as described in ObjectiveBounds.
438
439        Will return a valid (possibly infinite) bound even if no primal feasible
440        solutions are available.
441        """
442        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.

def dual_bound(self) -> float:
444    def dual_bound(self) -> float:
445        """Returns a dual bound on the optimal objective value as described in ObjectiveBounds.
446
447        Will return a valid (possibly infinite) bound even if no dual feasible
448        solutions are available.
449        """
450        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.

def has_primal_feasible_solution(self) -> bool:
452    def has_primal_feasible_solution(self) -> bool:
453        """Indicates if at least one primal feasible solution is available.
454
455        When termination.reason is TerminationReason.OPTIMAL or
456        TerminationReason.FEASIBLE, this is guaranteed to be true and need not be
457        checked.
458
459        Returns:
460          True if there is at least one primal feasible solution is available,
461          False, otherwise.
462        """
463        if not self.solutions:
464            return False
465        sol = self.solutions[0]
466        return (
467            sol.primal_solution is not None
468            and sol.primal_solution.feasibility_status
469            == solution.SolutionStatus.FEASIBLE
470        )

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.

def objective_value(self) -> float:
472    def objective_value(self) -> float:
473        """Returns the objective value of the best primal feasible solution.
474
475        An error will be raised if there are no primal feasible solutions.
476        primal_bound() above is guaranteed to be at least as good (larger or equal
477        for max problems and smaller or equal for min problems) as objective_value()
478        and will never raise an error, so it may be preferable in some cases. Note
479        that primal_bound() could be better than objective_value() even for optimal
480        terminations, but on such optimal termination, both should satisfy the
481        optimality tolerances.
482
483         Returns:
484           The objective value of the best primal feasible solution.
485
486         Raises:
487           ValueError: There are no primal feasible solutions.
488        """
489        if not self.has_primal_feasible_solution():
490            raise ValueError("No primal feasible solution available.")
491        sol = self.solutions[0]
492        assert sol.primal_solution is not None
493        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.

def best_objective_bound(self) -> float:
495    def best_objective_bound(self) -> float:
496        """Returns a bound on the best possible objective value.
497
498        best_objective_bound() is always equal to dual_bound(), so they can be
499        used interchangeably.
500        """
501        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.

def variable_values(self, variables=None):
516    def variable_values(self, variables=None):
517        """The variable values from the best primal feasible solution.
518
519        An error will be raised if there are no primal feasible solutions.
520
521        Args:
522          variables: an optional Variable or iterator of Variables indicating what
523            variable values to return. If not provided, variable_values returns a
524            dictionary with all the variable values for all variables.
525
526        Returns:
527          The variable values from the best primal feasible solution.
528
529        Raises:
530          ValueError: There are no primal feasible solutions.
531          TypeError: Argument is not None, a Variable or an iterable of Variables.
532          KeyError: Variable values requested for an invalid variable (e.g. is not a
533            Variable or is a variable for another model).
534        """
535        if not self.has_primal_feasible_solution():
536            raise ValueError("No primal feasible solution available.")
537        sol = self.solutions[0]
538        assert sol.primal_solution is not None
539        if variables is None:
540            return sol.primal_solution.variable_values
541        if isinstance(variables, variables_mod.Variable):
542            return sol.primal_solution.variable_values[variables]
543        if isinstance(variables, Iterable):
544            return [sol.primal_solution.variable_values[v] for v in variables]
545        raise TypeError(
546            "unsupported type in argument for "
547            f"variable_values: {type(variables).__name__!r}"
548        )

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).
def bounded(self) -> bool:
550    def bounded(self) -> bool:
551        """Returns true only if the problem has been shown to be feasible and bounded."""
552        return (
553            self.termination.problem_status.primal_status == FeasibilityStatus.FEASIBLE
554            and self.termination.problem_status.dual_status
555            == FeasibilityStatus.FEASIBLE
556        )

Returns true only if the problem has been shown to be feasible and bounded.

def has_ray(self) -> bool:
558    def has_ray(self) -> bool:
559        """Indicates if at least one primal ray is available.
560
561        This is NOT guaranteed to be true when termination.reason is
562        TerminationReason.kUnbounded or TerminationReason.kInfeasibleOrUnbounded.
563
564        Returns:
565          True if at least one primal ray is available.
566        """
567        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.

def ray_variable_values(self, variables=None):
582    def ray_variable_values(self, variables=None):
583        """The variable values from the first primal ray.
584
585        An error will be raised if there are no primal rays.
586
587        Args:
588          variables: an optional Variable or iterator of Variables indicating what
589            variable values to return. If not provided, variable_values() returns a
590            dictionary with the variable values for all variables.
591
592        Returns:
593          The variable values from the first primal ray.
594
595        Raises:
596          ValueError: There are no primal rays.
597          TypeError: Argument is not None, a Variable or an iterable of Variables.
598          KeyError: Variable values requested for an invalid variable (e.g. is not a
599            Variable or is a variable for another model).
600        """
601        if not self.has_ray():
602            raise ValueError("No primal ray available.")
603        if variables is None:
604            return self.primal_rays[0].variable_values
605        if isinstance(variables, variables_mod.Variable):
606            return self.primal_rays[0].variable_values[variables]
607        if isinstance(variables, Iterable):
608            return [self.primal_rays[0].variable_values[v] for v in variables]
609        raise TypeError(
610            "unsupported type in argument for "
611            f"ray_variable_values: {type(variables).__name__!r}"
612        )

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).
def has_dual_feasible_solution(self) -> bool:
614    def has_dual_feasible_solution(self) -> bool:
615        """Indicates if the best solution has an associated dual feasible solution.
616
617        This is NOT guaranteed to be true when termination.reason is
618        TerminationReason.Optimal. It also may be true even when the best solution
619        does not have an associated primal feasible solution.
620
621        Returns:
622          True if the best solution has an associated dual feasible solution.
623        """
624        if not self.solutions:
625            return False
626        sol = self.solutions[0]
627        return (
628            sol.dual_solution is not None
629            and sol.dual_solution.feasibility_status == solution.SolutionStatus.FEASIBLE
630        )

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.

def dual_values(self, linear_constraints=None):
648    def dual_values(self, linear_constraints=None):
649        """The dual values associated to the best solution.
650
651        If there is at least one primal feasible solution, this corresponds to the
652        dual values associated to the best primal feasible solution. An error will
653        be raised if the best solution does not have an associated dual feasible
654        solution.
655
656        Args:
657          linear_constraints: an optional LinearConstraint or iterator of
658            LinearConstraint indicating what dual values to return. If not provided,
659            dual_values() returns a dictionary with the dual values for all linear
660            constraints.
661
662        Returns:
663          The dual values associated to the best solution.
664
665        Raises:
666          ValueError: The best solution does not have an associated dual feasible
667            solution.
668          TypeError: Argument is not None, a LinearConstraint or an iterable of
669            LinearConstraint.
670          KeyError: LinearConstraint values requested for an invalid
671            linear constraint (e.g. is not a LinearConstraint or is a linear
672            constraint for another model).
673        """
674        if not self.has_dual_feasible_solution():
675            raise ValueError(_NO_DUAL_SOLUTION_ERROR)
676        sol = self.solutions[0]
677        assert sol.dual_solution is not None
678        if linear_constraints is None:
679            return sol.dual_solution.dual_values
680        if isinstance(linear_constraints, linear_constraints_mod.LinearConstraint):
681            return sol.dual_solution.dual_values[linear_constraints]
682        if isinstance(linear_constraints, Iterable):
683            return [sol.dual_solution.dual_values[c] for c in linear_constraints]
684        raise TypeError(
685            "unsupported type in argument for "
686            f"dual_values: {type(linear_constraints).__name__!r}"
687        )

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).
def reduced_costs(self, variables=None):
702    def reduced_costs(self, variables=None):
703        """The reduced costs associated to the best solution.
704
705        If there is at least one primal feasible solution, this corresponds to the
706        reduced costs associated to the best primal feasible solution. An error will
707        be raised if the best solution does not have an associated dual feasible
708        solution.
709
710        Args:
711          variables: an optional Variable or iterator of Variables indicating what
712            reduced costs to return. If not provided, reduced_costs() returns a
713            dictionary with the reduced costs for all variables.
714
715        Returns:
716          The reduced costs associated to the best solution.
717
718        Raises:
719          ValueError: The best solution does not have an associated dual feasible
720            solution.
721          TypeError: Argument is not None, a Variable or an iterable of Variables.
722          KeyError: Variable values requested for an invalid variable (e.g. is not a
723            Variable or is a variable for another model).
724        """
725        if not self.has_dual_feasible_solution():
726            raise ValueError(_NO_DUAL_SOLUTION_ERROR)
727        sol = self.solutions[0]
728        assert sol.dual_solution is not None
729        if variables is None:
730            return sol.dual_solution.reduced_costs
731        if isinstance(variables, variables_mod.Variable):
732            return sol.dual_solution.reduced_costs[variables]
733        if isinstance(variables, Iterable):
734            return [sol.dual_solution.reduced_costs[v] for v in variables]
735        raise TypeError(
736            "unsupported type in argument for "
737            f"reduced_costs: {type(variables).__name__!r}"
738        )

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).
def has_dual_ray(self) -> bool:
740    def has_dual_ray(self) -> bool:
741        """Indicates if at least one dual ray is available.
742
743        This is NOT guaranteed to be true when termination.reason is
744        TerminationReason.Infeasible.
745
746        Returns:
747          True if at least one dual ray is available.
748        """
749        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.

def ray_dual_values(self, linear_constraints=None):
767    def ray_dual_values(self, linear_constraints=None):
768        """The dual values from the first dual ray.
769
770        An error will be raised if there are no dual rays.
771
772        Args:
773          linear_constraints: an optional LinearConstraint or iterator of
774            LinearConstraint indicating what dual values to return. If not provided,
775            ray_dual_values() returns a dictionary with the dual values for all
776            linear constraints.
777
778        Returns:
779          The dual values from the first dual ray.
780
781        Raises:
782          ValueError: There are no dual rays.
783          TypeError: Argument is not None, a LinearConstraint or an iterable of
784            LinearConstraint.
785          KeyError: LinearConstraint values requested for an invalid
786            linear constraint (e.g. is not a LinearConstraint or is a linear
787            constraint for another model).
788        """
789        if not self.has_dual_ray():
790            raise ValueError("No dual ray available.")
791        ray = self.dual_rays[0]
792        if linear_constraints is None:
793            return ray.dual_values
794        if isinstance(linear_constraints, linear_constraints_mod.LinearConstraint):
795            return ray.dual_values[linear_constraints]
796        if isinstance(linear_constraints, Iterable):
797            return [ray.dual_values[v] for v in linear_constraints]
798        raise TypeError(
799            "unsupported type in argument for "
800            f"ray_dual_values: {type(linear_constraints).__name__!r}"
801        )

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).
def ray_reduced_costs(self, variables=None):
816    def ray_reduced_costs(self, variables=None):
817        """The reduced costs from the first dual ray.
818
819        An error will be raised if there are no dual rays.
820
821        Args:
822          variables: an optional Variable or iterator of Variables indicating what
823            reduced costs to return. If not provided, ray_reduced_costs() returns a
824            dictionary with the reduced costs for all variables.
825
826        Returns:
827          The reduced costs from the first dual ray.
828
829        Raises:
830          ValueError: There are no dual rays.
831          TypeError: Argument is not None, a Variable or an iterable of Variables.
832          KeyError: Variable values requested for an invalid variable (e.g. is not a
833            Variable or is a variable for another model).
834        """
835        if not self.has_dual_ray():
836            raise ValueError("No dual ray available.")
837        ray = self.dual_rays[0]
838        if variables is None:
839            return ray.reduced_costs
840        if isinstance(variables, variables_mod.Variable):
841            return ray.reduced_costs[variables]
842        if isinstance(variables, Iterable):
843            return [ray.reduced_costs[v] for v in variables]
844        raise TypeError(
845            "unsupported type in argument for "
846            f"ray_reduced_costs: {type(variables).__name__!r}"
847        )

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).
def has_basis(self) -> bool:
849    def has_basis(self) -> bool:
850        """Indicates if the best solution has an associated basis.
851
852        This is NOT guaranteed to be true when termination.reason is
853        TerminationReason.Optimal. It also may be true even when the best solution
854        does not have an associated primal feasible solution.
855
856        Returns:
857          True if the best solution has an associated basis.
858        """
859        if not self.solutions:
860            return False
861        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.

def constraint_status(self, linear_constraints=None):
879    def constraint_status(self, linear_constraints=None):
880        """The constraint basis status associated to the best solution.
881
882        If there is at least one primal feasible solution, this corresponds to the
883        basis associated to the best primal feasible solution. An error will
884        be raised if the best solution does not have an associated basis.
885
886
887        Args:
888          linear_constraints: an optional LinearConstraint or iterator of
889            LinearConstraint indicating what constraint statuses to return. If not
890            provided, returns a dictionary with the constraint statuses for all
891            linear constraints.
892
893        Returns:
894          The constraint basis status associated to the best solution.
895
896        Raises:
897          ValueError: The best solution does not have an associated basis.
898          TypeError: Argument is not None, a LinearConstraint or an iterable of
899            LinearConstraint.
900          KeyError: LinearConstraint values requested for an invalid
901            linear constraint (e.g. is not a LinearConstraint or is a linear
902            constraint for another model).
903        """
904        if not self.has_basis():
905            raise ValueError(_NO_BASIS_ERROR)
906        basis = self.solutions[0].basis
907        assert basis is not None
908        if linear_constraints is None:
909            return basis.constraint_status
910        if isinstance(linear_constraints, linear_constraints_mod.LinearConstraint):
911            return basis.constraint_status[linear_constraints]
912        if isinstance(linear_constraints, Iterable):
913            return [basis.constraint_status[c] for c in linear_constraints]
914        raise TypeError(
915            "unsupported type in argument for "
916            f"constraint_status: {type(linear_constraints).__name__!r}"
917        )

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).
def variable_status(self, variables=None):
934    def variable_status(self, variables=None):
935        """The variable basis status associated to the best solution.
936
937        If there is at least one primal feasible solution, this corresponds to the
938        basis associated to the best primal feasible solution. An error will
939        be raised if the best solution does not have an associated basis.
940
941        Args:
942          variables: an optional Variable or iterator of Variables indicating what
943            reduced costs to return. If not provided, variable_status() returns a
944            dictionary with the reduced costs for all variables.
945
946        Returns:
947          The variable basis status associated to the best solution.
948
949        Raises:
950          ValueError: The best solution does not have an associated basis.
951          TypeError: Argument is not None, a Variable or an iterable of Variables.
952          KeyError: Variable values requested for an invalid variable (e.g. is not a
953            Variable or is a variable for another model).
954        """
955        if not self.has_basis():
956            raise ValueError(_NO_BASIS_ERROR)
957        basis = self.solutions[0].basis
958        assert basis is not None
959        if variables is None:
960            return basis.variable_status
961        if isinstance(variables, variables_mod.Variable):
962            return basis.variable_status[variables]
963        if isinstance(variables, Iterable):
964            return [basis.variable_status[v] for v in variables]
965        raise TypeError(
966            "unsupported type in argument for "
967            f"variable_status: {type(variables).__name__!r}"
968        )

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).
def to_proto(self) -> ortools.math_opt.result_pb2.SolveResultProto:
 970    def to_proto(self) -> result_pb2.SolveResultProto:
 971        """Returns an equivalent protocol buffer for a SolveResult."""
 972        proto = result_pb2.SolveResultProto(
 973            termination=self.termination.to_proto(),
 974            solutions=[s.to_proto() for s in self.solutions],
 975            primal_rays=[r.to_proto() for r in self.primal_rays],
 976            dual_rays=[r.to_proto() for r in self.dual_rays],
 977            solve_stats=self.solve_stats.to_proto(),
 978        )
 979
 980        # Ensure that at most solver has solver specific output.
 981        existing_solver_specific_output = None
 982
 983        def has_solver_specific_output(solver_name: str) -> None:
 984            nonlocal existing_solver_specific_output
 985            if existing_solver_specific_output is not None:
 986                raise ValueError(
 987                    "found solver specific output for both"
 988                    f" {existing_solver_specific_output} and {solver_name}"
 989                )
 990            existing_solver_specific_output = solver_name
 991
 992        if self.gscip_specific_output is not None:
 993            has_solver_specific_output("gscip")
 994            proto.gscip_output.CopyFrom(self.gscip_specific_output)
 995        if self.osqp_specific_output is not None:
 996            has_solver_specific_output("osqp")
 997            proto.osqp_output.CopyFrom(self.osqp_specific_output)
 998        if self.pdlp_specific_output is not None:
 999            has_solver_specific_output("pdlp")
1000            proto.pdlp_output.CopyFrom(self.pdlp_specific_output)
1001        return proto

Returns an equivalent protocol buffer for a SolveResult.

def parse_solve_result( proto: ortools.math_opt.result_pb2.SolveResultProto, mod: ortools.math_opt.python.model.Model, *, validate: bool = True) -> SolveResult:
1035def parse_solve_result(
1036    proto: result_pb2.SolveResultProto,
1037    mod: model.Model,
1038    *,
1039    validate: bool = True,
1040) -> SolveResult:
1041    """Returns a SolveResult equivalent to the input proto."""
1042    result = SolveResult()
1043    # TODO(b/290091715): change to parse_termination(proto.termination)
1044    # once solve_stats proto no longer has best_primal/dual_bound/problem_status
1045    # and problem_status/objective_bounds are guaranteed to be present in
1046    # termination proto.
1047    result.termination = parse_termination(_upgrade_termination(proto))
1048    result.solve_stats = parse_solve_stats(proto.solve_stats)
1049    for solution_proto in proto.solutions:
1050        result.solutions.append(
1051            solution.parse_solution(solution_proto, mod, validate=validate)
1052        )
1053    for primal_ray_proto in proto.primal_rays:
1054        result.primal_rays.append(
1055            solution.parse_primal_ray(primal_ray_proto, mod, validate=validate)
1056        )
1057    for dual_ray_proto in proto.dual_rays:
1058        result.dual_rays.append(
1059            solution.parse_dual_ray(dual_ray_proto, mod, validate=validate)
1060        )
1061    if proto.HasField("gscip_output"):
1062        result.gscip_specific_output = proto.gscip_output
1063    elif proto.HasField("osqp_output"):
1064        result.osqp_specific_output = proto.osqp_output
1065    elif proto.HasField("pdlp_output"):
1066        result.pdlp_specific_output = proto.pdlp_output
1067    return result

Returns a SolveResult equivalent to the input proto.