15"""The output from solving a mathematical optimization problem from model.py."""
19from typing
import Dict, Iterable, List, Optional, overload
29_NO_DUAL_SOLUTION_ERROR = (
30 "Best solution does not have an associated dual feasible solution."
32_NO_BASIS_ERROR =
"Best solution does not have an associated basis."
37 """Problem feasibility status as claimed by the solver.
39 (solver is not required to return a certificate for the claim.)
42 UNDETERMINED: Solver does not claim a status.
43 FEASIBLE: Solver claims the problem is feasible.
44 INFEASIBLE: Solver claims the problem is infeasible.
47 UNDETERMINED = result_pb2.FEASIBILITY_STATUS_UNDETERMINED
48 FEASIBLE = result_pb2.FEASIBILITY_STATUS_FEASIBLE
49 INFEASIBLE = result_pb2.FEASIBILITY_STATUS_INFEASIBLE
52@dataclasses.dataclass(frozen=True)
54 """Feasibility status of the primal problem and its dual (or dual relaxation).
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.
75 primal_status: Status for the primal problem.
76 dual_status: Status for the dual problem (or for the dual of a continuous
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,
87 primal_status: FeasibilityStatus = FeasibilityStatus.UNDETERMINED
88 dual_status: FeasibilityStatus = FeasibilityStatus.UNDETERMINED
89 primal_or_dual_infeasible: bool =
False
91 def to_proto(self) -> result_pb2.ProblemStatusProto:
92 """Returns an equivalent proto for a problem status."""
93 return result_pb2.ProblemStatusProto(
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")
111 primal_or_dual_infeasible=proto.primal_or_dual_infeasible,
115@dataclasses.dataclass(frozen=True)
117 """Bounds on the optimal objective value.
119 MOE:begin_intracomment_strip
120 See go/mathopt-objective-bounds for more details.
121 MOE:end_intracomment_strip
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.
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
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
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).
148 MOE:begin_intracomment_strip
149 See go/mathopt-objective-bounds for more details.
150 MOE:end_intracomment_strip
153 primal_bound: float = 0.0
154 dual_bound: float = 0.0
156 def to_proto(self) -> result_pb2.ObjectiveBoundsProto:
157 """Returns an equivalent proto for objective bounds."""
158 return result_pb2.ObjectiveBoundsProto(
164 proto: result_pb2.ObjectiveBoundsProto,
166 """Returns an equivalent ObjectiveBounds from the input proto."""
167 return ObjectiveBounds(primal_bound=proto.primal_bound, dual_bound=proto.dual_bound)
170@dataclasses.dataclass
172 """Problem statuses and solve statistics returned by the solver.
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
178 simplex_iterations: Simplex iterations.
179 barrier_iterations: Barrier iterations.
180 first_order_iterations: First order iterations.
181 node_count: Node count.
184 solve_time: datetime.timedelta = datetime.timedelta()
185 simplex_iterations: int = 0
186 barrier_iterations: int = 0
187 first_order_iterations: int = 0
191 """Returns an equivalent proto for a solve stats."""
192 result = result_pb2.SolveStatsProto(
198 result.solve_time.FromTimedelta(self.
solve_time)
203 """Returns an equivalent SolveStats from the input proto."""
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
215 """The reason a solve of a model terminated.
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.
221 * OPTIMAL: A provably optimal solution (up to numerical tolerances) has
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.
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.
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
265 """The optimizer reached a limit, partial solution information may be present.
268 * UNDETERMINED: The underlying solver does not expose which limit was
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
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
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.
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
307@dataclasses.dataclass
309 """An explanation of why the solver stopped.
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
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.
325 reason: TerminationReason = TerminationReason.OPTIMAL
326 limit: Optional[Limit] =
None
332 """Returns an equivalent protocol buffer to this Termination."""
333 return result_pb2.TerminationProto(
336 result_pb2.LIMIT_UNSPECIFIED
if self.
limit is None else self.
limit.value
345 termination_proto: result_pb2.TerminationProto,
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")
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:
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"
364 termination.limit =
Limit(limit_proto)
if limit_set
else None
365 termination.detail = termination_proto.detail
368 termination_proto.objective_bounds
373@dataclasses.dataclass
375 """The result of solving an optimization problem defined by a Model.
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
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
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.
401 Solver specific output is also returned for some solvers (and only information
402 for the solver used will be populated).
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
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.
421 termination: Termination = dataclasses.field(default_factory=Termination)
422 solve_stats: SolveStats = dataclasses.field(default_factory=SolveStats)
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
432 """Shortcut for SolveResult.solve_stats.solve_time."""
436 """Returns a primal bound on the optimal objective value as described in ObjectiveBounds.
438 Will return a valid (possibly infinite) bound even if no primal feasible
439 solutions are available.
441 return self.
termination.objective_bounds.primal_bound
444 """Returns a dual bound on the optimal objective value as described in ObjectiveBounds.
446 Will return a valid (possibly infinite) bound even if no dual feasible
447 solutions are available.
449 return self.
termination.objective_bounds.dual_bound
452 """Indicates if at least one primal feasible solution is available.
454 When termination.reason is TerminationReason.OPTIMAL or
455 TerminationReason.FEASIBLE, this is guaranteed to be true and need not be
459 True if there is at least one primal feasible solution is available,
466 sol.primal_solution
is not None
467 and sol.primal_solution.feasibility_status
468 == solution.SolutionStatus.FEASIBLE
472 """Returns the objective value of the best primal feasible solution.
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.
483 The objective value of the best primal feasible solution.
486 ValueError: There are no primal feasible solutions.
489 raise ValueError(
"No primal feasible solution available.")
491 assert sol.primal_solution
is not None
492 return sol.primal_solution.objective_value
495 """Returns a bound on the best possible objective value.
497 best_objective_bound() is always equal to dual_bound(), so they can be
498 used interchangeably.
500 return self.
termination.objective_bounds.dual_bound
504 self, variables: None = ...
505 ) -> Dict[variables_mod.Variable, float]: ...
512 self, variables: Iterable[variables_mod.Variable]
513 ) -> List[float]: ...
516 """The variable values from the best primal feasible solution.
518 An error will be raised if there are no primal feasible solutions.
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.
526 The variable values from the best primal feasible solution.
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).
535 raise ValueError(
"No primal feasible solution available.")
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]
545 "unsupported type in argument for "
546 f
"variable_values: {type(variables).__name__!r}"
550 """Returns true only if the problem has been shown to be feasible and bounded."""
552 self.
termination.problem_status.primal_status == FeasibilityStatus.FEASIBLE
554 == FeasibilityStatus.FEASIBLE
558 """Indicates if at least one primal ray is available.
560 This is NOT guaranteed to be true when termination.reason is
561 TerminationReason.kUnbounded or TerminationReason.kInfeasibleOrUnbounded.
564 True if at least one primal ray is available.
570 self, variables: None = ...
571 ) -> Dict[variables_mod.Variable, float]: ...
578 self, variables: Iterable[variables_mod.Variable]
579 ) -> List[float]: ...
582 """The variable values from the first primal ray.
584 An error will be raised if there are no primal rays.
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.
592 The variable values from the first primal ray.
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).
601 raise ValueError(
"No primal ray available.")
602 if variables
is None:
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]
609 "unsupported type in argument for "
610 f
"ray_variable_values: {type(variables).__name__!r}"
614 """Indicates if the best solution has an associated dual feasible solution.
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.
621 True if the best solution has an associated dual feasible solution.
627 sol.dual_solution
is not None
628 and sol.dual_solution.feasibility_status == solution.SolutionStatus.FEASIBLE
633 self, linear_constraints: None = ...
634 ) -> Dict[linear_constraints_mod.LinearConstraint, float]: ...
638 self, linear_constraints: linear_constraints_mod.LinearConstraint
644 linear_constraints: Iterable[linear_constraints_mod.LinearConstraint],
645 ) -> List[float]: ...
648 """The dual values associated to the best solution.
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
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
662 The dual values associated to the best solution.
665 ValueError: The best solution does not have an associated dual feasible
667 TypeError: Argument is not None, a LinearConstraint or an iterable of
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).
674 raise ValueError(_NO_DUAL_SOLUTION_ERROR)
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]
684 "unsupported type in argument for "
685 f
"dual_values: {type(linear_constraints).__name__!r}"
690 self, variables: None = ...
691 ) -> Dict[variables_mod.Variable, float]: ...
698 self, variables: Iterable[variables_mod.Variable]
699 ) -> List[float]: ...
702 """The reduced costs associated to the best solution.
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
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.
715 The reduced costs associated to the best solution.
718 ValueError: The best solution does not have an associated dual feasible
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).
725 raise ValueError(_NO_DUAL_SOLUTION_ERROR)
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]
735 "unsupported type in argument for "
736 f
"reduced_costs: {type(variables).__name__!r}"
740 """Indicates if at least one dual ray is available.
742 This is NOT guaranteed to be true when termination.reason is
743 TerminationReason.Infeasible.
746 True if at least one dual ray is available.
752 self, linear_constraints: None = ...
753 ) -> Dict[linear_constraints_mod.LinearConstraint, float]: ...
757 self, linear_constraints: linear_constraints_mod.LinearConstraint
763 linear_constraints: Iterable[linear_constraints_mod.LinearConstraint],
764 ) -> List[float]: ...
767 """The dual values from the first dual ray.
769 An error will be raised if there are no dual rays.
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
778 The dual values from the first dual ray.
781 ValueError: There are no dual rays.
782 TypeError: Argument is not None, a LinearConstraint or an iterable of
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).
789 raise ValueError(
"No dual ray available.")
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]
798 "unsupported type in argument for "
799 f
"ray_dual_values: {type(linear_constraints).__name__!r}"
804 self, variables: None = ...
805 ) -> Dict[variables_mod.Variable, float]: ...
812 self, variables: Iterable[variables_mod.Variable]
813 ) -> List[float]: ...
816 """The reduced costs from the first dual ray.
818 An error will be raised if there are no dual rays.
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.
826 The reduced costs from the first dual ray.
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).
835 raise ValueError(
"No dual ray available.")
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]
844 "unsupported type in argument for "
845 f
"ray_reduced_costs: {type(variables).__name__!r}"
849 """Indicates if the best solution has an associated basis.
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.
856 True if the best solution has an associated basis.
860 return self.
solutions[0].basis
is not None
864 self, linear_constraints: None = ...
869 self, linear_constraints: linear_constraints_mod.LinearConstraint
875 linear_constraints: Iterable[linear_constraints_mod.LinearConstraint],
879 """The constraint basis status associated to the best solution.
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.
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
893 The constraint basis status associated to the best solution.
896 ValueError: The best solution does not have an associated basis.
897 TypeError: Argument is not None, a LinearConstraint or an iterable of
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).
904 raise ValueError(_NO_BASIS_ERROR)
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]
914 "unsupported type in argument for "
915 f
"constraint_status: {type(linear_constraints).__name__!r}"
920 self, variables: None = ...
925 self, variables: variables_mod.Variable
930 self, variables: Iterable[variables_mod.Variable]
934 """The variable basis status associated to the best solution.
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.
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.
946 The variable basis status associated to the best solution.
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).
955 raise ValueError(_NO_BASIS_ERROR)
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]
965 "unsupported type in argument for "
966 f
"variable_status: {type(variables).__name__!r}"
970 """Returns an equivalent protocol buffer for a SolveResult."""
971 proto = result_pb2.SolveResultProto(
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],
980 existing_solver_specific_output =
None
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:
986 "found solver specific output for both"
987 f
" {existing_solver_specific_output} and {solver_name}"
989 existing_solver_specific_output = solver_name
992 has_solver_specific_output(
"gscip")
995 has_solver_specific_output(
"osqp")
998 has_solver_specific_output(
"pdlp")
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
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,
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,
1035 proto: result_pb2.SolveResultProto,
1038 validate: bool =
True,
1040 """Returns a SolveResult equivalent to the input proto."""
1048 for solution_proto
in proto.solutions:
1049 result.solutions.append(
1050 solution.parse_solution(solution_proto, mod, validate=validate)
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)
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)
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