diff --git a/ortools/math_opt/cpp/model.h b/ortools/math_opt/cpp/model.h index edecacb61e..8ee6a1fcf3 100644 --- a/ortools/math_opt/cpp/model.h +++ b/ortools/math_opt/cpp/model.h @@ -52,7 +52,7 @@ namespace math_opt { // using ::operations_research::math_opt::SolveParameters; // using ::operations_research::math_opt::SolveResultProto; // using ::operations_research::math_opt::Variable; -// using ::operations_research::math_opt::SOLVER_TYPE_GSCIP; +// using ::operations_research::math_opt::SolverType; // // Version 1: // @@ -67,7 +67,7 @@ namespace math_opt { // model.set_objective_coefficient(y, 1.0); // model.set_maximize(); // const SolveResult result = Solve( -// model, SOLVER_TYPE_GSCIP, SolveParametersProto()).value(); +// model, SolverType::kGscip, SolveParametersProto()).value(); // for (const auto& warning : result.warnings) { // std::cerr << "Solver warning: " << warning << std::endl; // } @@ -93,7 +93,7 @@ namespace math_opt { // objective_expression += y; // model.Maximize(objective_expression); // const SolveResult result = Solve( -// model, SOLVER_TYPE_GSCIP, SolveParametersProto()).value(); +// model, SolverType::kGscip, SolveParametersProto()).value(); // for (const auto& warning : result.warnings) { // std::cerr << "Solver warning: " << warning << std::endl; // } diff --git a/ortools/math_opt/solvers/cp_sat_solver.cc b/ortools/math_opt/solvers/cp_sat_solver.cc index 4fc046ed92..3b1f46d381 100644 --- a/ortools/math_opt/solvers/cp_sat_solver.cc +++ b/ortools/math_opt/solvers/cp_sat_solver.cc @@ -63,6 +63,35 @@ namespace { constexpr double kInf = std::numeric_limits::infinity(); +// Returns true on success. +bool ApplyCutoff(const double cutoff, MPModelProto* model) { + // TODO(b/204083726): we need to be careful here if we support quadratic + // objectives + if (model->has_quadratic_objective()) { + return false; + } + // CP-SAT detects a constraint parallel to the objective and uses it as + // an objective bound, which is the closest we can get to cutoff. + // See FindDuplicateConstraints() in CP-SAT codebase. + MPConstraintProto* const cutoff_constraint = model->add_constraint(); + for (int i = 0; i < model->variable_size(); ++i) { + const double obj_coef = model->variable(i).objective_coefficient(); + if (obj_coef != 0) { + cutoff_constraint->add_var_index(i); + cutoff_constraint->add_coefficient(obj_coef); + } + } + const double cutoff_minus_offset = cutoff - model->objective_offset(); + if (model->maximize()) { + // Add the constraint obj >= cutoff + cutoff_constraint->set_lower_bound(cutoff_minus_offset); + } else { + // Add the constraint obj <= cutoff + cutoff_constraint->set_upper_bound(cutoff_minus_offset); + } + return true; +} + // Returns a list of warnings from parameter settings that were // invalid/unsupported (specific to CP-SAT), one element per bad parameter. std::vector SetSolveParameters( @@ -100,10 +129,7 @@ std::vector SetSolveParameters( if (parameters.has_absolute_gap_limit()) { sat_parameters.set_absolute_gap_limit(parameters.absolute_gap_limit()); } - if (parameters.has_cutoff_limit()) { - warnings.push_back( - "The cutoff_limit parameter is not supported for CP-SAT."); - } + // cutoff_limit is handled outside this function as it modifies the model. if (parameters.has_best_bound_limit()) { warnings.push_back( "The best_bound_limit parameter is not supported for CP-SAT."); @@ -203,6 +229,7 @@ std::vector SetSolveParameters( absl::StatusOr> GetTerminationAndStats(const bool is_interrupted, const bool maximize, + const bool used_cutoff, const MPSolutionResponse& response) { SolveStatsProto solve_stats; TerminationProto termination; @@ -228,10 +255,15 @@ GetTerminationAndStats(const bool is_interrupted, const bool maximize, solve_stats.set_best_dual_bound(response.best_objective_bound()); break; case MPSOLVER_INFEASIBLE: + if (used_cutoff) { + termination = + NoSolutionFoundTermination(LIMIT_CUTOFF, response.status_str()); + } else { termination = TerminateForReason(TERMINATION_REASON_INFEASIBLE, response.status_str()); solve_stats.mutable_problem_status()->set_primal_status( FEASIBILITY_STATUS_INFEASIBLE); + } break; case MPSOLVER_UNKNOWN_STATUS: // For a basic unbounded problem, CP-SAT internally returns @@ -299,7 +331,6 @@ absl::StatusOr> CpSatSolver::New( "MathOpt does not currently support CP-SAT models with quadratic " "objectives"); } - // We must use WrapUnique here since the constructor is private. return absl::WrapUnique( new CpSatSolver(std::move(cp_sat_model), std::move(variable_ids))); } @@ -328,11 +359,21 @@ absl::StatusOr CpSatSolver::Solve( // Here we must make a copy since Solve() can be called multiple times with // different parameters. Hence we can't move `cp_sat_model`. *req.mutable_model() = cp_sat_model_; + req.set_solver_type(MPModelRequest::SAT_INTEGER_PROGRAMMING); + bool used_cutoff = false; { std::vector param_warnings = SetSolveParameters(parameters, /*has_message_callback=*/message_cb != nullptr, req); + if (parameters.has_cutoff_limit()) { + used_cutoff = ApplyCutoff(parameters.cutoff_limit(), req.mutable_model()); + if (!used_cutoff) { + param_warnings.push_back( + "The cutoff_limit parameter not supported for quadratic objectives " + "with CP-SAT."); + } + } if (!param_warnings.empty()) { if (parameters.strictness().bad_parameter()) { return absl::InvalidArgumentError(absl::StrJoin(param_warnings, "; ")); @@ -402,14 +443,15 @@ absl::StatusOr CpSatSolver::Solve( // supported by CP-SAT and we have validated they are empty. }; } - ASSIGN_OR_RETURN(const MPSolutionResponse response, SatSolveProto(std::move(req), &interrupt_solve, logging_callback, solution_callback)); RETURN_IF_ERROR(callback_error) << "error in callback"; - ASSIGN_OR_RETURN((auto [solve_stats, termination]), + ASSIGN_OR_RETURN( + (auto [solve_stats, termination]), GetTerminationAndStats(local_interrupter.IsInterrupted(), - cp_sat_model_.maximize(), response)); + /*maximize=*/cp_sat_model_.maximize(), + /*used_cutoff=*/used_cutoff, response)); *result.mutable_solve_stats() = std::move(solve_stats); *result.mutable_termination() = std::move(termination); if (response.status() == MPSOLVER_OPTIMAL ||