// Copyright 2010-2025 Google LLC // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "ortools/math_opt/cpp/solve_result.h" #include #include #include #include #include #include #include #include #include "absl/log/check.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/escaping.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_join.h" #include "absl/strings/string_view.h" #include "absl/time/time.h" #include "absl/types/span.h" #include "ortools/base/protoutil.h" #include "ortools/base/status_macros.h" #include "ortools/math_opt/core/math_opt_proto_utils.h" #include "ortools/math_opt/cpp/linear_constraint.h" #include "ortools/math_opt/cpp/variable_and_expressions.h" #include "ortools/math_opt/result.pb.h" #include "ortools/math_opt/solution.pb.h" #include "ortools/math_opt/storage/model_storage.h" #include "ortools/port/proto_utils.h" #include "ortools/util/fp_roundtrip_conv.h" #include "ortools/util/status_macros.h" namespace operations_research::math_opt { namespace { constexpr double kInf = std::numeric_limits::infinity(); } // namespace ObjectiveBoundsProto ObjectiveBounds::Proto() const { ObjectiveBoundsProto proto; proto.set_primal_bound(primal_bound); proto.set_dual_bound(dual_bound); return proto; } ObjectiveBounds ObjectiveBounds::FromProto( const ObjectiveBoundsProto& objective_bounds_proto) { ObjectiveBounds result; result.primal_bound = objective_bounds_proto.primal_bound(); result.dual_bound = objective_bounds_proto.dual_bound(); return result; } std::ostream& operator<<(std::ostream& ostr, const ObjectiveBounds& objective_bounds) { ostr << "{primal_bound: " << RoundTripDoubleFormat(objective_bounds.primal_bound); ostr << ", dual_bound: " << RoundTripDoubleFormat(objective_bounds.dual_bound); ostr << "}"; return ostr; } std::string ObjectiveBounds::ToString() const { std::ostringstream stream; stream << *this; return stream.str(); } ObjectiveBounds ObjectiveBounds::MakeTrivial(const bool is_maximize) { const double primal_bound = is_maximize ? -kInf : kInf; const double dual_bound = -primal_bound; return ObjectiveBounds{.primal_bound = primal_bound, .dual_bound = dual_bound}; } ObjectiveBounds ObjectiveBounds::MaximizeMakeTrivial() { return ObjectiveBounds::MakeTrivial(true); } ObjectiveBounds ObjectiveBounds::MinimizeMakeTrivial() { return ObjectiveBounds::MakeTrivial(false); } ObjectiveBounds ObjectiveBounds::MakeUnbounded(const bool is_maximize) { const double primal_bound = is_maximize ? kInf : -kInf; const double dual_bound = primal_bound; return ObjectiveBounds{.primal_bound = primal_bound, .dual_bound = dual_bound}; } ObjectiveBounds ObjectiveBounds::MinimizeMakeUnbounded() { return ObjectiveBounds::MakeUnbounded(/*is_maximize=*/false); } ObjectiveBounds ObjectiveBounds::MaximizeMakeUnbounded() { return ObjectiveBounds::MakeUnbounded(/*is_maximize=*/true); } ObjectiveBounds ObjectiveBounds::MakeOptimal(double objective_value) { return ObjectiveBounds{.primal_bound = objective_value, .dual_bound = objective_value}; } std::optional Enum::ToOptString( FeasibilityStatus value) { switch (value) { case FeasibilityStatus::kUndetermined: return "undetermined"; case FeasibilityStatus::kFeasible: return "feasible"; case FeasibilityStatus::kInfeasible: return "infeasible"; } return std::nullopt; } absl::Span Enum::AllValues() { static constexpr FeasibilityStatus kFeasibilityStatus[] = { FeasibilityStatus::kUndetermined, FeasibilityStatus::kFeasible, FeasibilityStatus::kInfeasible, }; return absl::MakeConstSpan(kFeasibilityStatus); } std::optional Enum::ToOptString( TerminationReason value) { switch (value) { case TerminationReason::kOptimal: return "optimal"; case TerminationReason::kInfeasible: return "infeasible"; case TerminationReason::kUnbounded: return "unbounded"; case TerminationReason::kInfeasibleOrUnbounded: return "infeasible_or_unbounded"; case TerminationReason::kImprecise: return "imprecise"; case TerminationReason::kFeasible: return "feasible"; case TerminationReason::kNoSolutionFound: return "no_solution_found"; case TerminationReason::kNumericalError: return "numerical_error"; case TerminationReason::kOtherError: return "other_error"; } return std::nullopt; } absl::Span Enum::AllValues() { static constexpr TerminationReason kTerminationReasonValues[] = { TerminationReason::kOptimal, TerminationReason::kInfeasible, TerminationReason::kUnbounded, TerminationReason::kInfeasibleOrUnbounded, TerminationReason::kImprecise, TerminationReason::kFeasible, TerminationReason::kNoSolutionFound, TerminationReason::kNumericalError, TerminationReason::kOtherError, }; return absl::MakeConstSpan(kTerminationReasonValues); } std::optional Enum::ToOptString(Limit value) { switch (value) { case Limit::kUndetermined: return "undetermined"; case Limit::kIteration: return "iteration"; case Limit::kTime: return "time"; case Limit::kNode: return "node"; case Limit::kSolution: return "solution"; case Limit::kMemory: return "memory"; case Limit::kCutoff: return "cutoff"; case Limit::kObjective: return "objective"; case Limit::kNorm: return "norm"; case Limit::kInterrupted: return "interrupted"; case Limit::kSlowProgress: return "slow_progress"; case Limit::kOther: return "other"; } return std::nullopt; } absl::Span Enum::AllValues() { static constexpr Limit kLimitValues[] = { Limit::kUndetermined, Limit::kIteration, Limit::kTime, Limit::kNode, Limit::kSolution, Limit::kMemory, Limit::kCutoff, Limit::kObjective, Limit::kNorm, Limit::kInterrupted, Limit::kSlowProgress, Limit::kOther}; return absl::MakeConstSpan(kLimitValues); } Termination::Termination(const bool is_maximize, const TerminationReason reason, std::string detail) : reason(reason), detail(std::move(detail)), objective_bounds(ObjectiveBounds::MakeTrivial(is_maximize)) {} Termination Termination::Optimal(const double primal_objective_value, const double dual_objective_value, const std::string detail) { Termination termination(/*is_maximize=*/false, TerminationReason::kOptimal, detail); termination.objective_bounds.primal_bound = primal_objective_value; termination.objective_bounds.dual_bound = dual_objective_value; termination.problem_status.primal_status = FeasibilityStatus::kFeasible; termination.problem_status.dual_status = FeasibilityStatus::kFeasible; return termination; } Termination Termination::Optimal(const double objective_value, const std::string detail) { return Optimal(objective_value, objective_value, detail); } Termination Termination::Infeasible(const bool is_maximize, FeasibilityStatus dual_feasibility_status, const std::string detail) { Termination termination(is_maximize, TerminationReason::kInfeasible, detail); if (dual_feasibility_status == FeasibilityStatus::kFeasible) { termination.objective_bounds.dual_bound = termination.objective_bounds.primal_bound; } termination.problem_status.primal_status = FeasibilityStatus::kInfeasible; termination.problem_status.dual_status = dual_feasibility_status; return termination; } Termination Termination::InfeasibleOrUnbounded( const bool is_maximize, const FeasibilityStatus dual_feasibility_status, const std::string detail) { Termination termination(is_maximize, TerminationReason::kInfeasibleOrUnbounded, detail); termination.problem_status.primal_status = FeasibilityStatus::kUndetermined; termination.problem_status.dual_status = dual_feasibility_status; if (dual_feasibility_status == FeasibilityStatus::kUndetermined) { termination.problem_status.primal_or_dual_infeasible = true; } return termination; } Termination Termination::Unbounded(const bool is_maximize, const std::string detail) { Termination termination(is_maximize, TerminationReason::kUnbounded, detail); termination.objective_bounds = ObjectiveBounds::MakeUnbounded(is_maximize); termination.problem_status.primal_status = FeasibilityStatus::kFeasible; termination.problem_status.dual_status = FeasibilityStatus::kInfeasible; return termination; } Termination Termination::NoSolutionFound( const bool is_maximize, Limit limit, const std::optional optional_dual_objective, const std::string detail) { Termination termination(is_maximize, TerminationReason::kNoSolutionFound, detail); termination.problem_status.primal_status = FeasibilityStatus::kUndetermined; termination.problem_status.dual_status = FeasibilityStatus::kUndetermined; if (optional_dual_objective.has_value()) { termination.objective_bounds.dual_bound = *optional_dual_objective; termination.problem_status.dual_status = FeasibilityStatus::kFeasible; } termination.limit = limit; return termination; } Termination Termination::Feasible( const bool is_maximize, const Limit limit, const double finite_primal_objective, const std::optional optional_dual_objective, const std::string detail) { Termination termination(is_maximize, TerminationReason::kFeasible, detail); termination.problem_status.primal_status = FeasibilityStatus::kFeasible; termination.objective_bounds.primal_bound = finite_primal_objective; termination.problem_status.dual_status = FeasibilityStatus::kUndetermined; if (optional_dual_objective.has_value()) { termination.objective_bounds.dual_bound = *optional_dual_objective; termination.problem_status.dual_status = FeasibilityStatus::kFeasible; } termination.limit = limit; return termination; } Termination Termination::Cutoff(const bool is_maximize, const std::string detail) { return NoSolutionFound(is_maximize, Limit::kCutoff, /*optional_dual_objective=*/std::nullopt, detail); } TerminationProto Termination::Proto() const { TerminationProto proto; proto.set_reason(EnumToProto(reason)); proto.set_limit(EnumToProto(limit)); proto.set_detail(detail); *proto.mutable_problem_status() = problem_status.Proto(); *proto.mutable_objective_bounds() = objective_bounds.Proto(); return proto; } bool Termination::limit_reached() const { return reason == TerminationReason::kFeasible || reason == TerminationReason::kNoSolutionFound; } absl::Status Termination::EnsureReasonIs(TerminationReason reason) const { if (this->reason == reason) return absl::OkStatus(); return util::InternalErrorBuilder() << "expected termination reason '" << reason << "' but got " << *this; } absl::Status Termination::EnsureReasonIsAnyOf( std::initializer_list reasons) const { for (const TerminationReason reason : reasons) { if (this->reason == reason) return absl::OkStatus(); } return util::InternalErrorBuilder() << "expected termination reason in {" << absl::StrJoin(reasons, ", ", [](std::string* out, TerminationReason reason) { absl::StrAppend(out, "'"); absl::StreamFormatter()(out, reason); absl::StrAppend(out, "'"); }) << "} but got " << *this; } absl::Status Termination::EnsureIsOptimal() const { return EnsureReasonIs(TerminationReason::kOptimal); } bool Termination::IsOptimalOrFeasible() const { return reason == TerminationReason::kOptimal || reason == TerminationReason::kFeasible; } absl::Status Termination::EnsureIsOptimalOrFeasible() const { return EnsureReasonIsAnyOf( {TerminationReason::kOptimal, TerminationReason::kFeasible}); } bool Termination::IsOptimal() const { return reason == TerminationReason::kOptimal; } absl::StatusOr Termination::FromProto( const TerminationProto& termination_proto) { const std::optional reason = EnumFromProto(termination_proto.reason()); if (!reason.has_value()) { return absl::InvalidArgumentError("reason must be specified"); } Termination result(/*is_maximize=*/false, *reason, termination_proto.detail()); result.limit = EnumFromProto(termination_proto.limit()); OR_ASSIGN_OR_RETURN3( result.problem_status, ProblemStatus::FromProto(termination_proto.problem_status()), _ << "invalid problem_status"); result.objective_bounds = ObjectiveBounds::FromProto(termination_proto.objective_bounds()); return result; } std::ostream& operator<<(std::ostream& ostr, const Termination& termination) { ostr << "{reason: " << termination.reason; if (termination.limit.has_value()) { ostr << ", limit: " << *termination.limit; } if (!termination.detail.empty()) { ostr << ", detail: " << '"' << absl::CEscape(termination.detail) << '"'; } ostr << ", problem_status: " << termination.problem_status; ostr << ", objective_bounds: " << termination.objective_bounds; ostr << "}"; return ostr; } std::string Termination::ToString() const { std::ostringstream stream; stream << *this; return stream.str(); } ProblemStatusProto ProblemStatus::Proto() const { ProblemStatusProto proto; proto.set_primal_status(EnumToProto(primal_status)); proto.set_dual_status(EnumToProto(dual_status)); proto.set_primal_or_dual_infeasible(primal_or_dual_infeasible); return proto; } absl::StatusOr ProblemStatus::FromProto( const ProblemStatusProto& problem_status_proto) { const std::optional primal_status = EnumFromProto(problem_status_proto.primal_status()); if (!primal_status.has_value()) { return absl::InvalidArgumentError("primal_status must be specified"); } const std::optional dual_status = EnumFromProto(problem_status_proto.dual_status()); if (!dual_status.has_value()) { return absl::InvalidArgumentError("dual_status must be specified"); } return ProblemStatus{.primal_status = *primal_status, .dual_status = *dual_status, .primal_or_dual_infeasible = problem_status_proto.primal_or_dual_infeasible()}; } std::ostream& operator<<(std::ostream& ostr, const ProblemStatus& problem_status) { ostr << "{primal_status: " << problem_status.primal_status; ostr << ", dual_status: " << problem_status.dual_status; ostr << ", primal_or_dual_infeasible: " << (problem_status.primal_or_dual_infeasible ? "true" : "false"); ostr << "}"; return ostr; } std::string ProblemStatus::ToString() const { std::ostringstream stream; stream << *this; return stream.str(); } absl::StatusOr SolveStats::Proto() const { SolveStatsProto proto; RETURN_IF_ERROR( util_time::EncodeGoogleApiProto(solve_time, proto.mutable_solve_time())) << "invalid solve_time (value must be finite)"; proto.set_simplex_iterations(simplex_iterations); proto.set_barrier_iterations(barrier_iterations); proto.set_first_order_iterations(first_order_iterations); proto.set_node_count(node_count); return proto; } absl::StatusOr SolveStats::FromProto( const SolveStatsProto& solve_stats_proto) { SolveStats result; OR_ASSIGN_OR_RETURN3( result.solve_time, util_time::DecodeGoogleApiProto(solve_stats_proto.solve_time()), _ << "invalid solve_time"); result.simplex_iterations = solve_stats_proto.simplex_iterations(); result.barrier_iterations = solve_stats_proto.barrier_iterations(); result.first_order_iterations = solve_stats_proto.first_order_iterations(); result.node_count = solve_stats_proto.node_count(); return result; } std::ostream& operator<<(std::ostream& ostr, const SolveStats& solve_stats) { ostr << "{solve_time: " << solve_stats.solve_time; ostr << ", simplex_iterations: " << solve_stats.simplex_iterations; ostr << ", barrier_iterations: " << solve_stats.barrier_iterations; ostr << ", first_order_iterations: " << solve_stats.first_order_iterations; ostr << ", node_count: " << solve_stats.node_count; ostr << "}"; return ostr; } std::string SolveStats::ToString() const { std::ostringstream stream; stream << *this; return stream.str(); } absl::Status CheckSolverSpecificOutputEmpty(const SolveResultProto& result) { if (result.solver_specific_output_case() == SolveResultProto::SOLVER_SPECIFIC_OUTPUT_NOT_SET) { return absl::OkStatus(); } return util::InvalidArgumentErrorBuilder() << "cannot set solver specific output twice, was already " << static_cast(result.solver_specific_output_case()); } absl::StatusOr SolveResult::Proto() const { SolveResultProto result; *result.mutable_termination() = termination.Proto(); OR_ASSIGN_OR_RETURN3(*result.mutable_solve_stats(), solve_stats.Proto(), _ << "invalid solve_stats"); for (const Solution& solution : solutions) { *result.add_solutions() = solution.Proto(); } for (const PrimalRay& primal_ray : primal_rays) { *result.add_primal_rays() = primal_ray.Proto(); } for (const DualRay& dual_ray : dual_rays) { *result.add_dual_rays() = dual_ray.Proto(); } // See yaqs/5107601535926272 on checking if a proto is empty. if (gscip_solver_specific_output.ByteSizeLong() > 0) { *result.mutable_gscip_output() = gscip_solver_specific_output; } if (pdlp_solver_specific_output.ByteSizeLong() > 0) { *result.mutable_pdlp_output() = pdlp_solver_specific_output; } return result; } namespace { TerminationProto UpgradedTerminationProtoForStatsMigration( const SolveResultProto& solve_result_proto) { TerminationProto termination; termination.set_reason(solve_result_proto.termination().reason()); termination.set_limit(solve_result_proto.termination().limit()); termination.set_detail(solve_result_proto.termination().detail()); *termination.mutable_problem_status() = GetProblemStatus(solve_result_proto); *termination.mutable_objective_bounds() = GetObjectiveBounds(solve_result_proto); return termination; } } // namespace absl::StatusOr SolveResult::FromProto( const ModelStorageCPtr model, const SolveResultProto& solve_result_proto) { OR_ASSIGN_OR_RETURN3( auto termination, Termination::FromProto( // TODO(b/290091715): Remove once solve_stats proto no longer has // best_primal/dual_bound/problem_status and // problem_status/objective_bounds are guaranteed to be present in // termination proto. UpgradedTerminationProtoForStatsMigration(solve_result_proto)), _ << "invalid termination"); SolveResult result(std::move(termination)); OR_ASSIGN_OR_RETURN3(result.solve_stats, SolveStats::FromProto(solve_result_proto.solve_stats()), _ << "invalid solve_stats"); for (int i = 0; i < solve_result_proto.solutions_size(); ++i) { OR_ASSIGN_OR_RETURN3( auto solution, Solution::FromProto(model, solve_result_proto.solutions(i)), _ << "invalid solution at index " << i); result.solutions.push_back(std::move(solution)); } for (int i = 0; i < solve_result_proto.primal_rays_size(); ++i) { OR_ASSIGN_OR_RETURN3( auto primal_ray, PrimalRay::FromProto(model, solve_result_proto.primal_rays(i)), _ << "invalid primal ray at index " << i); result.primal_rays.push_back(std::move(primal_ray)); } for (int i = 0; i < solve_result_proto.dual_rays_size(); ++i) { OR_ASSIGN_OR_RETURN3( auto dual_ray, DualRay::FromProto(model, solve_result_proto.dual_rays(i)), _ << "invalid dual ray at index " << i); result.dual_rays.push_back(std::move(dual_ray)); } switch (solve_result_proto.solver_specific_output_case()) { case SolveResultProto::kGscipOutput: result.gscip_solver_specific_output = solve_result_proto.gscip_output(); return result; case SolveResultProto::kPdlpOutput: result.pdlp_solver_specific_output = solve_result_proto.pdlp_output(); return result; case SolveResultProto::SOLVER_SPECIFIC_OUTPUT_NOT_SET: return result; } return util::InvalidArgumentErrorBuilder() << "unexpected value of solver_specific_output_case " << solve_result_proto.solver_specific_output_case(); } bool SolveResult::has_primal_feasible_solution() const { return !solutions.empty() && solutions[0].primal_solution.has_value() && (solutions[0].primal_solution->feasibility_status == SolutionStatus::kFeasible); } const PrimalSolution& SolveResult::best_primal_solution() const { CHECK(has_primal_feasible_solution()); return *solutions.front().primal_solution; } double SolveResult::best_objective_bound() const { return termination.objective_bounds.dual_bound; } double SolveResult::primal_bound() const { return termination.objective_bounds.primal_bound; } double SolveResult::dual_bound() const { return termination.objective_bounds.dual_bound; } double SolveResult::objective_value() const { CHECK(has_primal_feasible_solution()); return solutions[0].primal_solution->objective_value; } double SolveResult::objective_value(const Objective objective) const { CHECK(has_primal_feasible_solution()); return solutions[0].primal_solution->get_objective_value(objective); } bool SolveResult::bounded() const { return termination.problem_status.primal_status == FeasibilityStatus::kFeasible && termination.problem_status.dual_status == FeasibilityStatus::kFeasible; } const VariableMap& SolveResult::variable_values() const { CHECK(has_primal_feasible_solution()); return solutions[0].primal_solution->variable_values; } const VariableMap& SolveResult::ray_variable_values() const { CHECK(has_ray()); return primal_rays[0].variable_values; } bool SolveResult::has_dual_feasible_solution() const { return !solutions.empty() && solutions[0].dual_solution.has_value() && (solutions[0].dual_solution->feasibility_status == SolutionStatus::kFeasible); } const LinearConstraintMap& SolveResult::dual_values() const { CHECK(has_dual_feasible_solution()); return solutions[0].dual_solution->dual_values; } const VariableMap& SolveResult::reduced_costs() const { CHECK(has_dual_feasible_solution()); return solutions[0].dual_solution->reduced_costs; } const LinearConstraintMap& SolveResult::ray_dual_values() const { CHECK(has_dual_ray()); return dual_rays[0].dual_values; } const VariableMap& SolveResult::ray_reduced_costs() const { CHECK(has_dual_ray()); return dual_rays[0].reduced_costs; } bool SolveResult::has_basis() const { return !solutions.empty() && solutions[0].basis.has_value(); } const LinearConstraintMap& SolveResult::constraint_status() const { CHECK(has_basis()); return solutions[0].basis->constraint_status; } const VariableMap& SolveResult::variable_status() const { CHECK(has_basis()); return solutions[0].basis->variable_status; } namespace { // Prints only the vector size, not its content. template void PrintVectorSize(std::ostream& out, const std::vector& v) { out << '['; if (!v.empty()) { out << v.size() << " available"; } out << ']'; } } // namespace std::ostream& operator<<(std::ostream& out, const SolveResult& result) { out << "{termination: " << result.termination << ", solve_stats: " << result.solve_stats << ", solutions: "; PrintVectorSize(out, result.solutions); out << ", primal_rays: "; PrintVectorSize(out, result.primal_rays); out << ", dual_rays: "; PrintVectorSize(out, result.dual_rays); { const std::string gscip_specific_output = ProtobufShortDebugString(result.gscip_solver_specific_output); if (!gscip_specific_output.empty()) { out << ", gscip_solver_specific_output: " << gscip_specific_output; } } out << '}'; return out; } } // namespace operations_research::math_opt