14"""The output from solving a mathematical optimization problem from model.py."""
18from typing
import Dict, Iterable, List, Optional, overload
28_NO_DUAL_SOLUTION_ERROR = (
29 "Best solution does not have an associated dual feasible solution."
31_NO_BASIS_ERROR =
"Best solution does not have an associated basis."
36 """Problem feasibility status as claimed by the solver.
38 (solver is not required to return a certificate for the claim.)
41 UNDETERMINED: Solver does not claim a status.
42 FEASIBLE: Solver claims the problem is feasible.
43 INFEASIBLE: Solver claims the problem is infeasible.
46 UNDETERMINED = result_pb2.FEASIBILITY_STATUS_UNDETERMINED
47 FEASIBLE = result_pb2.FEASIBILITY_STATUS_FEASIBLE
48 INFEASIBLE = result_pb2.FEASIBILITY_STATUS_INFEASIBLE
51@dataclasses.dataclass(frozen=True)
53 """Feasibility status of the primal problem and its dual (or dual relaxation).
55 Statuses are as claimed by the solver and a dual relaxation is the dual of a
56 continuous relaxation for the original problem (e.g. the LP relaxation of a
57 MIP). The solver is not required to return a certificate for the feasibility
58 or infeasibility claims (e.g. the solver may claim primal feasibility without
59 returning a primal feasible solutuion). This combined status gives a
60 comprehensive description of a solver's claims about feasibility and
61 unboundedness of the solved problem. For instance,
62 * a feasible status for primal and dual problems indicates the primal is
63 feasible and bounded and likely has an optimal solution (guaranteed for
64 problems without non-linear constraints).
65 * a primal feasible and a dual infeasible status indicates the primal
66 problem is unbounded (i.e. has arbitrarily good solutions).
67 Note that a dual infeasible status by itself (i.e. accompanied by an
68 undetermined primal status) does not imply the primal problem is unbounded as
69 we could have both problems be infeasible. Also, while a primal and dual
70 feasible status may imply the existence of an optimal solution, it does not
71 guarantee the solver has actually found such optimal solution.
74 primal_status: Status for the primal problem.
75 dual_status: Status for the dual problem (or for the dual of a continuous
77 primal_or_dual_infeasible: If true, the solver claims the primal or dual
78 problem is infeasible, but it does not know which (or if both are
79 infeasible). Can be true only when primal_problem_status =
80 dual_problem_status = kUndetermined. This extra information is often
81 needed when preprocessing determines there is no optimal solution to the
82 problem (but can't determine if it is due to infeasibility, unboundedness,
86 primal_status: FeasibilityStatus = FeasibilityStatus.UNDETERMINED
87 dual_status: FeasibilityStatus = FeasibilityStatus.UNDETERMINED
88 primal_or_dual_infeasible: bool =
False
90 def to_proto(self) -> result_pb2.ProblemStatusProto:
91 """Returns an equivalent proto for a problem status."""
92 return result_pb2.ProblemStatusProto(
100 """Returns an equivalent ProblemStatus from the input proto."""
101 primal_status_proto = proto.primal_status
102 if primal_status_proto == result_pb2.FEASIBILITY_STATUS_UNSPECIFIED:
103 raise ValueError(
"Primal feasibility status should not be UNSPECIFIED")
104 dual_status_proto = proto.dual_status
105 if dual_status_proto == result_pb2.FEASIBILITY_STATUS_UNSPECIFIED:
106 raise ValueError(
"Dual feasibility status should not be UNSPECIFIED")
110 primal_or_dual_infeasible=proto.primal_or_dual_infeasible,
114@dataclasses.dataclass(frozen=True)
116 """Bounds on the optimal objective value.
118 MOE:begin_intracomment_strip
119 See go/mathopt-objective-bounds for more details.
120 MOE:end_intracomment_strip
123 primal_bound: Solver claims there exists a primal solution that is
124 numerically feasible (i.e. feasible up to the solvers tolerance), and
125 whose objective value is primal_bound.
127 The optimal value is equal or better (smaller for min objectives and
128 larger for max objectives) than primal_bound, but only up to
131 MOE:begin_intracomment_strip
132 See go/mathopt-objective-bounds for more details.
133 MOE:end_intracomment_strip
134 dual_bound: Solver claims there exists a dual solution that is numerically
135 feasible (i.e. feasible up to the solvers tolerance), and whose objective
138 For MIP solvers, the associated dual problem may be some continuous
139 relaxation (e.g. LP relaxation), but it is often an implicitly defined
140 problem that is a complex consequence of the solvers execution. For both
141 continuous and MIP solvers, the optimal value is equal or worse (larger
142 for min objective and smaller for max objectives) than dual_bound, but
143 only up to solver-tolerances. Some continuous solvers provide a
144 numerically safer dual bound through solver's specific output (e.g. for
145 PDLP, pdlp_output.convergence_information.corrected_dual_objective).
147 MOE:begin_intracomment_strip
148 See go/mathopt-objective-bounds for more details.
149 MOE:end_intracomment_strip
152 primal_bound: float = 0.0
153 dual_bound: float = 0.0
155 def to_proto(self) -> result_pb2.ObjectiveBoundsProto:
156 """Returns an equivalent proto for objective bounds."""
157 return result_pb2.ObjectiveBoundsProto(
163 proto: result_pb2.ObjectiveBoundsProto,
165 """Returns an equivalent ObjectiveBounds from the input proto."""
166 return ObjectiveBounds(primal_bound=proto.primal_bound, dual_bound=proto.dual_bound)
169@dataclasses.dataclass
171 """Problem statuses and solve statistics returned by the solver.
174 solve_time: Elapsed wall clock time as measured by math_opt, roughly the
175 time inside solve(). Note: this does not include work done building the
177 simplex_iterations: Simplex iterations.
178 barrier_iterations: Barrier iterations.
179 first_order_iterations: First order iterations.
180 node_count: Node count.
183 solve_time: datetime.timedelta = datetime.timedelta()
184 simplex_iterations: int = 0
185 barrier_iterations: int = 0
186 first_order_iterations: int = 0
190 """Returns an equivalent proto for a solve stats."""
191 result = result_pb2.SolveStatsProto(
197 result.solve_time.FromTimedelta(self.
solve_time)
202 """Returns an equivalent SolveStats from the input proto."""
204 result.solve_time = proto.solve_time.ToTimedelta()
205 result.simplex_iterations = proto.simplex_iterations
206 result.barrier_iterations = proto.barrier_iterations
207 result.first_order_iterations = proto.first_order_iterations
208 result.node_count = proto.node_count
214 """The reason a solve of a model terminated.
216 These reasons are typically as reported by the underlying solver, e.g. we do
217 not attempt to verify the precision of the solution returned.
220 * OPTIMAL: A provably optimal solution (up to numerical tolerances) has
222 * INFEASIBLE: The primal problem has no feasible solutions.
223 * UNBOUNDED: The primal problem is feasible and arbitrarily good solutions
224 can be found along a primal ray.
225 * INFEASIBLE_OR_UNBOUNDED: The primal problem is either infeasible or
226 unbounded. More details on the problem status may be available in
227 solve_stats.problem_status. Note that Gurobi's unbounded status may be
228 mapped here as explained in
229 go/mathopt-solver-specific#gurobi-inf-or-unb.
230 * IMPRECISE: The problem was solved to one of the criteria above (Optimal,
231 Infeasible, Unbounded, or InfeasibleOrUnbounded), but one or more
232 tolerances was not met. Some primal/dual solutions/rays may be present,
233 but either they will be slightly infeasible, or (if the problem was
234 nearly optimal) their may be a gap between the best solution objective
235 and best objective bound.
237 Users can still query primal/dual solutions/rays and solution stats,
238 but they are responsible for dealing with the numerical imprecision.
239 * FEASIBLE: The optimizer reached some kind of limit and a primal feasible
240 solution is returned. See SolveResultProto.limit_detail for detailed
241 description of the kind of limit that was reached.
242 * NO_SOLUTION_FOUND: The optimizer reached some kind of limit and it did
243 not find a primal feasible solution. See SolveResultProto.limit_detail
244 for detailed description of the kind of limit that was reached.
245 * NUMERICAL_ERROR: The algorithm stopped because it encountered
246 unrecoverable numerical error. No solution information is present.
247 * OTHER_ERROR: The algorithm stopped because of an error not covered by one
248 of the statuses defined above. No solution information is present.
251 OPTIMAL = result_pb2.TERMINATION_REASON_OPTIMAL
252 INFEASIBLE = result_pb2.TERMINATION_REASON_INFEASIBLE
253 UNBOUNDED = result_pb2.TERMINATION_REASON_UNBOUNDED
254 INFEASIBLE_OR_UNBOUNDED = result_pb2.TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED
255 IMPRECISE = result_pb2.TERMINATION_REASON_IMPRECISE
256 FEASIBLE = result_pb2.TERMINATION_REASON_FEASIBLE
257 NO_SOLUTION_FOUND = result_pb2.TERMINATION_REASON_NO_SOLUTION_FOUND
258 NUMERICAL_ERROR = result_pb2.TERMINATION_REASON_NUMERICAL_ERROR
259 OTHER_ERROR = result_pb2.TERMINATION_REASON_OTHER_ERROR
264 """The optimizer reached a limit, partial solution information may be present.
267 * UNDETERMINED: The underlying solver does not expose which limit was
269 * ITERATION: An iterative algorithm stopped after conducting the
270 maximum number of iterations (e.g. simplex or barrier iterations).
271 * TIME: The algorithm stopped after a user-specified amount of
273 * NODE: A branch-and-bound algorithm stopped because it explored a
274 maximum number of nodes in the branch-and-bound tree.
275 * SOLUTION: The algorithm stopped because it found the required
276 number of solutions. This is often used in MIPs to get the solver to
277 return the first feasible solution it encounters.
278 * MEMORY: The algorithm stopped because it ran out of memory.
279 * OBJECTIVE: The algorithm stopped because it found a solution better
280 than a minimum limit set by the user.
281 * NORM: The algorithm stopped because the norm of an iterate became
283 * INTERRUPTED: The algorithm stopped because of an interrupt signal or a
284 user interrupt request.
285 * SLOW_PROGRESS: The algorithm stopped because it was unable to continue
286 making progress towards the solution.
287 * OTHER: The algorithm stopped due to a limit not covered by one of the
288 above. Note that UNDETERMINED is used when the reason cannot be
289 determined, and OTHER is used when the reason is known but does not fit
290 into any of the above alternatives.
293 UNDETERMINED = result_pb2.LIMIT_UNDETERMINED
294 ITERATION = result_pb2.LIMIT_ITERATION
295 TIME = result_pb2.LIMIT_TIME
296 NODE = result_pb2.LIMIT_NODE
297 SOLUTION = result_pb2.LIMIT_SOLUTION
298 MEMORY = result_pb2.LIMIT_MEMORY
299 OBJECTIVE = result_pb2.LIMIT_OBJECTIVE
300 NORM = result_pb2.LIMIT_NORM
301 INTERRUPTED = result_pb2.LIMIT_INTERRUPTED
302 SLOW_PROGRESS = result_pb2.LIMIT_SLOW_PROGRESS
303 OTHER = result_pb2.LIMIT_OTHER
306@dataclasses.dataclass
308 """An explanation of why the solver stopped.
311 reason: Why the solver stopped, e.g. it found a provably optimal solution.
312 Additional information in `limit` when value is FEASIBLE or
313 NO_SOLUTION_FOUND, see `limit` for details.
314 limit: If the solver stopped early, what caused it to stop. Have value
315 UNSPECIFIED when reason is not NO_SOLUTION_FOUND or FEASIBLE. May still be
316 UNSPECIFIED when reason is NO_SOLUTION_FOUND or FEASIBLE, some solvers
318 detail: Additional, information beyond reason about why the solver stopped,
319 typically solver specific.
320 problem_status: Feasibility statuses for primal and dual problems.
321 objective_bounds: Bounds on the optimal objective value.
324 reason: TerminationReason = TerminationReason.OPTIMAL
325 limit: Optional[Limit] =
None
331 """Returns an equivalent protocol buffer to this Termination."""
332 return result_pb2.TerminationProto(
335 result_pb2.LIMIT_UNSPECIFIED
if self.
limit is None else self.
limit.value
344 termination_proto: result_pb2.TerminationProto,
346 """Returns a Termination that is equivalent to termination_proto."""
347 reason_proto = termination_proto.reason
348 limit_proto = termination_proto.limit
349 if reason_proto == result_pb2.TERMINATION_REASON_UNSPECIFIED:
350 raise ValueError(
"Termination reason should not be UNSPECIFIED")
352 reason_proto == result_pb2.TERMINATION_REASON_NO_SOLUTION_FOUND
353 )
or (reason_proto == result_pb2.TERMINATION_REASON_FEASIBLE)
354 limit_set = limit_proto != result_pb2.LIMIT_UNSPECIFIED
355 if reason_is_limit != limit_set:
357 f
"Termination limit (={limit_proto})) should take value other than "
358 f
"UNSPECIFIED if and only if termination reason (={reason_proto}) is "
359 "FEASIBLE or NO_SOLUTION_FOUND"
363 termination.limit =
Limit(limit_proto)
if limit_set
else None
364 termination.detail = termination_proto.detail
367 termination_proto.objective_bounds
372@dataclasses.dataclass
374 """The result of solving an optimization problem defined by a Model.
376 We attempt to return as much solution information (primal_solutions,
377 primal_rays, dual_solutions, dual_rays) as each underlying solver will provide
378 given its return status. Differences in the underlying solvers result in a
379 weak contract on what fields will be populated for a given termination
380 reason. This is discussed in detail in termination_reasons.md, and the most
381 important points are summarized below:
382 * When the termination reason is optimal, there will be at least one primal
383 solution provided that will be feasible up to the underlying solver's
385 * Dual solutions are only given for convex optimization problems (e.g.
386 linear programs, not integer programs).
387 * A basis is only given for linear programs when solved by the simplex
388 method (e.g., not with PDLP).
389 * Solvers have widely varying support for returning primal and dual rays.
390 E.g. a termination_reason of unbounded does not ensure that a feasible
391 solution or a primal ray is returned, check termination_reasons.md for
392 solver specific guarantees if this is needed. Further, many solvers will
393 provide the ray but not the feasible solution when returning an unbounded
395 * When the termination reason is that a limit was reached or that the result
396 is imprecise, a solution may or may not be present. Further, for some
397 solvers (generally, convex optimization solvers, not MIP solvers), the
398 primal or dual solution may not be feasible.
400 Solver specific output is also returned for some solvers (and only information
401 for the solver used will be populated).
404 termination: The reason the solver stopped.
405 solve_stats: Statistics on the solve process, e.g. running time, iterations.
406 solutions: Lexicographically by primal feasibility status, dual feasibility
407 status, (basic dual feasibility for simplex solvers), primal objective
408 value and dual objective value.
409 primal_rays: Directions of unbounded primal improvement, or equivalently,
410 dual infeasibility certificates. Typically provided for terminal reasons
411 UNBOUNDED and DUAL_INFEASIBLE.
412 dual_rays: Directions of unbounded dual improvement, or equivalently, primal
413 infeasibility certificates. Typically provided for termination reason
415 gscip_specific_output: statistics returned by the gSCIP solver, if used.
416 osqp_specific_output: statistics returned by the OSQP solver, if used.
417 pdlp_specific_output: statistics returned by the PDLP solver, if used.
420 termination: Termination = dataclasses.field(default_factory=Termination)
421 solve_stats: SolveStats = dataclasses.field(default_factory=SolveStats)
426 gscip_specific_output: Optional[gscip_pb2.GScipOutput] =
None
427 osqp_specific_output: Optional[osqp_pb2.OsqpOutput] =
None
428 pdlp_specific_output: Optional[result_pb2.SolveResultProto.PdlpOutput] =
None
431 """Shortcut for SolveResult.solve_stats.solve_time."""
435 """Returns a primal bound on the optimal objective value as described in ObjectiveBounds.
437 Will return a valid (possibly infinite) bound even if no primal feasible
438 solutions are available.
440 return self.
termination.objective_bounds.primal_bound
443 """Returns a dual bound on the optimal objective value as described in ObjectiveBounds.
445 Will return a valid (possibly infinite) bound even if no dual feasible
446 solutions are available.
448 return self.
termination.objective_bounds.dual_bound
451 """Indicates if at least one primal feasible solution is available.
453 When termination.reason is TerminationReason.OPTIMAL or
454 TerminationReason.FEASIBLE, this is guaranteed to be true and need not be
458 True if there is at least one primal feasible solution is available,
465 sol.primal_solution
is not None
466 and sol.primal_solution.feasibility_status
467 == solution.SolutionStatus.FEASIBLE
471 """Returns the objective value of the best primal feasible solution.
473 An error will be raised if there are no primal feasible solutions.
474 primal_bound() above is guaranteed to be at least as good (larger or equal
475 for max problems and smaller or equal for min problems) as objective_value()
476 and will never raise an error, so it may be preferable in some cases. Note
477 that primal_bound() could be better than objective_value() even for optimal
478 terminations, but on such optimal termination, both should satisfy the
479 optimality tolerances.
482 The objective value of the best primal feasible solution.
485 ValueError: There are no primal feasible solutions.
488 raise ValueError(
"No primal feasible solution available.")
490 assert sol.primal_solution
is not None
491 return sol.primal_solution.objective_value
494 """Returns a bound on the best possible objective value.
496 best_objective_bound() is always equal to dual_bound(), so they can be
497 used interchangeably.
499 return self.
termination.objective_bounds.dual_bound
503 self, variables: None = ...
504 ) -> Dict[variables_mod.Variable, float]: ...
511 self, variables: Iterable[variables_mod.Variable]
512 ) -> List[float]: ...
515 """The variable values from the best primal feasible solution.
517 An error will be raised if there are no primal feasible solutions.
520 variables: an optional Variable or iterator of Variables indicating what
521 variable values to return. If not provided, variable_values returns a
522 dictionary with all the variable values for all variables.
525 The variable values from the best primal feasible solution.
528 ValueError: There are no primal feasible solutions.
529 TypeError: Argument is not None, a Variable or an iterable of Variables.
530 KeyError: Variable values requested for an invalid variable (e.g. is not a
531 Variable or is a variable for another model).
534 raise ValueError(
"No primal feasible solution available.")
536 assert sol.primal_solution
is not None
537 if variables
is None:
538 return sol.primal_solution.variable_values
539 if isinstance(variables, variables_mod.Variable):
540 return sol.primal_solution.variable_values[variables]
541 if isinstance(variables, Iterable):
542 return [sol.primal_solution.variable_values[v]
for v
in variables]
544 "unsupported type in argument for "
545 f
"variable_values: {type(variables).__name__!r}"
549 """Returns true only if the problem has been shown to be feasible and bounded."""
551 self.
termination.problem_status.primal_status == FeasibilityStatus.FEASIBLE
553 == FeasibilityStatus.FEASIBLE
557 """Indicates if at least one primal ray is available.
559 This is NOT guaranteed to be true when termination.reason is
560 TerminationReason.kUnbounded or TerminationReason.kInfeasibleOrUnbounded.
563 True if at least one primal ray is available.
569 self, variables: None = ...
570 ) -> Dict[variables_mod.Variable, float]: ...
577 self, variables: Iterable[variables_mod.Variable]
578 ) -> List[float]: ...
581 """The variable values from the first primal ray.
583 An error will be raised if there are no primal rays.
586 variables: an optional Variable or iterator of Variables indicating what
587 variable values to return. If not provided, variable_values() returns a
588 dictionary with the variable values for all variables.
591 The variable values from the first primal ray.
594 ValueError: There are no primal rays.
595 TypeError: Argument is not None, a Variable or an iterable of Variables.
596 KeyError: Variable values requested for an invalid variable (e.g. is not a
597 Variable or is a variable for another model).
600 raise ValueError(
"No primal ray available.")
601 if variables
is None:
603 if isinstance(variables, variables_mod.Variable):
604 return self.
primal_rays[0].variable_values[variables]
605 if isinstance(variables, Iterable):
606 return [self.
primal_rays[0].variable_values[v]
for v
in variables]
608 "unsupported type in argument for "
609 f
"ray_variable_values: {type(variables).__name__!r}"
613 """Indicates if the best solution has an associated dual feasible solution.
615 This is NOT guaranteed to be true when termination.reason is
616 TerminationReason.Optimal. It also may be true even when the best solution
617 does not have an associated primal feasible solution.
620 True if the best solution has an associated dual feasible solution.
626 sol.dual_solution
is not None
627 and sol.dual_solution.feasibility_status == solution.SolutionStatus.FEASIBLE
632 self, linear_constraints: None = ...
633 ) -> Dict[linear_constraints_mod.LinearConstraint, float]: ...
637 self, linear_constraints: linear_constraints_mod.LinearConstraint
643 linear_constraints: Iterable[linear_constraints_mod.LinearConstraint],
644 ) -> List[float]: ...
647 """The dual values associated to the best solution.
649 If there is at least one primal feasible solution, this corresponds to the
650 dual values associated to the best primal feasible solution. An error will
651 be raised if the best solution does not have an associated dual feasible
655 linear_constraints: an optional LinearConstraint or iterator of
656 LinearConstraint indicating what dual values to return. If not provided,
657 dual_values() returns a dictionary with the dual values for all linear
661 The dual values associated to the best solution.
664 ValueError: The best solution does not have an associated dual feasible
666 TypeError: Argument is not None, a LinearConstraint or an iterable of
668 KeyError: LinearConstraint values requested for an invalid
669 linear constraint (e.g. is not a LinearConstraint or is a linear
670 constraint for another model).
673 raise ValueError(_NO_DUAL_SOLUTION_ERROR)
675 assert sol.dual_solution
is not None
676 if linear_constraints
is None:
677 return sol.dual_solution.dual_values
678 if isinstance(linear_constraints, linear_constraints_mod.LinearConstraint):
679 return sol.dual_solution.dual_values[linear_constraints]
680 if isinstance(linear_constraints, Iterable):
681 return [sol.dual_solution.dual_values[c]
for c
in linear_constraints]
683 "unsupported type in argument for "
684 f
"dual_values: {type(linear_constraints).__name__!r}"
689 self, variables: None = ...
690 ) -> Dict[variables_mod.Variable, float]: ...
697 self, variables: Iterable[variables_mod.Variable]
698 ) -> List[float]: ...
701 """The reduced costs associated to the best solution.
703 If there is at least one primal feasible solution, this corresponds to the
704 reduced costs associated to the best primal feasible solution. An error will
705 be raised if the best solution does not have an associated dual feasible
709 variables: an optional Variable or iterator of Variables indicating what
710 reduced costs to return. If not provided, reduced_costs() returns a
711 dictionary with the reduced costs for all variables.
714 The reduced costs associated to the best solution.
717 ValueError: The best solution does not have an associated dual feasible
719 TypeError: Argument is not None, a Variable or an iterable of Variables.
720 KeyError: Variable values requested for an invalid variable (e.g. is not a
721 Variable or is a variable for another model).
724 raise ValueError(_NO_DUAL_SOLUTION_ERROR)
726 assert sol.dual_solution
is not None
727 if variables
is None:
728 return sol.dual_solution.reduced_costs
729 if isinstance(variables, variables_mod.Variable):
730 return sol.dual_solution.reduced_costs[variables]
731 if isinstance(variables, Iterable):
732 return [sol.dual_solution.reduced_costs[v]
for v
in variables]
734 "unsupported type in argument for "
735 f
"reduced_costs: {type(variables).__name__!r}"
739 """Indicates if at least one dual ray is available.
741 This is NOT guaranteed to be true when termination.reason is
742 TerminationReason.Infeasible.
745 True if at least one dual ray is available.
751 self, linear_constraints: None = ...
752 ) -> Dict[linear_constraints_mod.LinearConstraint, float]: ...
756 self, linear_constraints: linear_constraints_mod.LinearConstraint
762 linear_constraints: Iterable[linear_constraints_mod.LinearConstraint],
763 ) -> List[float]: ...
766 """The dual values from the first dual ray.
768 An error will be raised if there are no dual rays.
771 linear_constraints: an optional LinearConstraint or iterator of
772 LinearConstraint indicating what dual values to return. If not provided,
773 ray_dual_values() returns a dictionary with the dual values for all
777 The dual values from the first dual ray.
780 ValueError: There are no dual rays.
781 TypeError: Argument is not None, a LinearConstraint or an iterable of
783 KeyError: LinearConstraint values requested for an invalid
784 linear constraint (e.g. is not a LinearConstraint or is a linear
785 constraint for another model).
788 raise ValueError(
"No dual ray available.")
790 if linear_constraints
is None:
791 return ray.dual_values
792 if isinstance(linear_constraints, linear_constraints_mod.LinearConstraint):
793 return ray.dual_values[linear_constraints]
794 if isinstance(linear_constraints, Iterable):
795 return [ray.dual_values[v]
for v
in linear_constraints]
797 "unsupported type in argument for "
798 f
"ray_dual_values: {type(linear_constraints).__name__!r}"
803 self, variables: None = ...
804 ) -> Dict[variables_mod.Variable, float]: ...
811 self, variables: Iterable[variables_mod.Variable]
812 ) -> List[float]: ...
815 """The reduced costs from the first dual ray.
817 An error will be raised if there are no dual rays.
820 variables: an optional Variable or iterator of Variables indicating what
821 reduced costs to return. If not provided, ray_reduced_costs() returns a
822 dictionary with the reduced costs for all variables.
825 The reduced costs from the first dual ray.
828 ValueError: There are no dual rays.
829 TypeError: Argument is not None, a Variable or an iterable of Variables.
830 KeyError: Variable values requested for an invalid variable (e.g. is not a
831 Variable or is a variable for another model).
834 raise ValueError(
"No dual ray available.")
836 if variables
is None:
837 return ray.reduced_costs
838 if isinstance(variables, variables_mod.Variable):
839 return ray.reduced_costs[variables]
840 if isinstance(variables, Iterable):
841 return [ray.reduced_costs[v]
for v
in variables]
843 "unsupported type in argument for "
844 f
"ray_reduced_costs: {type(variables).__name__!r}"
848 """Indicates if the best solution has an associated basis.
850 This is NOT guaranteed to be true when termination.reason is
851 TerminationReason.Optimal. It also may be true even when the best solution
852 does not have an associated primal feasible solution.
855 True if the best solution has an associated basis.
859 return self.
solutions[0].basis
is not None
863 self, linear_constraints: None = ...
868 self, linear_constraints: linear_constraints_mod.LinearConstraint
874 linear_constraints: Iterable[linear_constraints_mod.LinearConstraint],
878 """The constraint basis status associated to the best solution.
880 If there is at least one primal feasible solution, this corresponds to the
881 basis associated to the best primal feasible solution. An error will
882 be raised if the best solution does not have an associated basis.
886 linear_constraints: an optional LinearConstraint or iterator of
887 LinearConstraint indicating what constraint statuses to return. If not
888 provided, returns a dictionary with the constraint statuses for all
892 The constraint basis status associated to the best solution.
895 ValueError: The best solution does not have an associated basis.
896 TypeError: Argument is not None, a LinearConstraint or an iterable of
898 KeyError: LinearConstraint values requested for an invalid
899 linear constraint (e.g. is not a LinearConstraint or is a linear
900 constraint for another model).
903 raise ValueError(_NO_BASIS_ERROR)
905 assert basis
is not None
906 if linear_constraints
is None:
907 return basis.constraint_status
908 if isinstance(linear_constraints, linear_constraints_mod.LinearConstraint):
909 return basis.constraint_status[linear_constraints]
910 if isinstance(linear_constraints, Iterable):
911 return [basis.constraint_status[c]
for c
in linear_constraints]
913 "unsupported type in argument for "
914 f
"constraint_status: {type(linear_constraints).__name__!r}"
919 self, variables: None = ...
924 self, variables: variables_mod.Variable
929 self, variables: Iterable[variables_mod.Variable]
933 """The variable basis status associated to the best solution.
935 If there is at least one primal feasible solution, this corresponds to the
936 basis associated to the best primal feasible solution. An error will
937 be raised if the best solution does not have an associated basis.
940 variables: an optional Variable or iterator of Variables indicating what
941 reduced costs to return. If not provided, variable_status() returns a
942 dictionary with the reduced costs for all variables.
945 The variable basis status associated to the best solution.
948 ValueError: The best solution does not have an associated basis.
949 TypeError: Argument is not None, a Variable or an iterable of Variables.
950 KeyError: Variable values requested for an invalid variable (e.g. is not a
951 Variable or is a variable for another model).
954 raise ValueError(_NO_BASIS_ERROR)
956 assert basis
is not None
957 if variables
is None:
958 return basis.variable_status
959 if isinstance(variables, variables_mod.Variable):
960 return basis.variable_status[variables]
961 if isinstance(variables, Iterable):
962 return [basis.variable_status[v]
for v
in variables]
964 "unsupported type in argument for "
965 f
"variable_status: {type(variables).__name__!r}"
969 """Returns an equivalent protocol buffer for a SolveResult."""
970 proto = result_pb2.SolveResultProto(
972 solutions=[s.to_proto()
for s
in self.
solutions],
973 primal_rays=[r.to_proto()
for r
in self.
primal_rays],
974 dual_rays=[r.to_proto()
for r
in self.
dual_rays],
979 existing_solver_specific_output =
None
981 def has_solver_specific_output(solver_name: str) ->
None:
982 nonlocal existing_solver_specific_output
983 if existing_solver_specific_output
is not None:
985 "found solver specific output for both"
986 f
" {existing_solver_specific_output} and {solver_name}"
988 existing_solver_specific_output = solver_name
991 has_solver_specific_output(
"gscip")
994 has_solver_specific_output(
"osqp")
997 has_solver_specific_output(
"pdlp")
1003 result_proto: result_pb2.SolveResultProto,
1004) -> result_pb2.ProblemStatusProto:
1005 if result_proto.termination.HasField(
"problem_status"):
1006 return result_proto.termination.problem_status
1007 return result_proto.solve_stats.problem_status
1011 result_proto: result_pb2.SolveResultProto,
1012) -> result_pb2.ObjectiveBoundsProto:
1013 if result_proto.termination.HasField(
"objective_bounds"):
1014 return result_proto.termination.objective_bounds
1015 return result_pb2.ObjectiveBoundsProto(
1016 primal_bound=result_proto.solve_stats.best_primal_bound,
1017 dual_bound=result_proto.solve_stats.best_dual_bound,
1022 result_proto: result_pb2.SolveResultProto,
1023) -> result_pb2.TerminationProto:
1024 return result_pb2.TerminationProto(
1025 reason=result_proto.termination.reason,
1026 limit=result_proto.termination.limit,
1027 detail=result_proto.termination.detail,
1034 proto: result_pb2.SolveResultProto,
1037 validate: bool =
True,
1039 """Returns a SolveResult equivalent to the input proto."""
1047 for solution_proto
in proto.solutions:
1048 result.solutions.append(
1049 solution.parse_solution(solution_proto, mod, validate=validate)
1051 for primal_ray_proto
in proto.primal_rays:
1052 result.primal_rays.append(
1053 solution.parse_primal_ray(primal_ray_proto, mod, validate=validate)
1055 for dual_ray_proto
in proto.dual_rays:
1056 result.dual_rays.append(
1057 solution.parse_dual_ray(dual_ray_proto, mod, validate=validate)
1059 if proto.HasField(
"gscip_output"):
1060 result.gscip_specific_output = proto.gscip_output
1061 elif proto.HasField(
"osqp_output"):
1062 result.osqp_specific_output = proto.osqp_output
1063 elif proto.HasField(
"pdlp_output"):
1064 result.pdlp_specific_output = proto.pdlp_output