Skip to content

Commit 402aef7

Browse files
committed
[CP-SAT] more work on lrat; expose response.solve_log in C#, Java, python
1 parent 24ee469 commit 402aef7

20 files changed

+317
-219
lines changed

ortools/java/com/google/ortools/sat/CpSolver.java

Lines changed: 13 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -68,40 +68,6 @@ public CpSolverStatus solve(CpModel model, CpSolverSolutionCallback cb) {
6868
return solveResponse.getStatus();
6969
}
7070

71-
/**
72-
* Solves the given model, passes each incumber solution to the solution callback if not null, and
73-
* returns the solve status.
74-
*
75-
* @deprecated Use the solve() method with the same signature.
76-
*/
77-
@Deprecated
78-
public CpSolverStatus solveWithSolutionCallback(CpModel model, CpSolverSolutionCallback cb) {
79-
return solve(model, cb);
80-
}
81-
82-
/**
83-
* Searches for all solutions of a satisfiability problem.
84-
*
85-
* <p>This method searches for all feasible solutions of a given model. Then it feeds the
86-
* solutions to the callback.
87-
*
88-
* <p>Note that the model cannot have an objective.
89-
*
90-
* @param model the model to solve
91-
* @param cb the callback that will be called at each solution
92-
* @return the status of the solve (FEASIBLE, INFEASIBLE...)
93-
* @deprecated Use the solve() method with the same signature, after setting the
94-
* enumerate_all_solution parameter to true.
95-
*/
96-
@Deprecated
97-
public CpSolverStatus searchAllSolutions(CpModel model, CpSolverSolutionCallback cb) {
98-
boolean oldValue = solveParameters.getEnumerateAllSolutions();
99-
solveParameters.setEnumerateAllSolutions(true);
100-
CpSolverStatus status = solve(model, cb);
101-
solveParameters.setEnumerateAllSolutions(oldValue);
102-
return status;
103-
}
104-
10571
private synchronized void createSolveWrapper() {
10672
solveWrapper = new SolveWrapper();
10773
}
@@ -213,10 +179,23 @@ public String responseStats() {
213179
* Returns some information on how the solution was found, or the reason why the model or the
214180
* parameters are invalid.
215181
*/
182+
public String solutionInfo() {
183+
return solveResponse.getSolutionInfo();
184+
}
185+
186+
/** Returns the solution info. @Deprecated */
216187
public String getSolutionInfo() {
217188
return solveResponse.getSolutionInfo();
218189
}
219190

191+
/**
192+
* Returns the solve log. You need to set the parameters log_to_response to true to get the solve
193+
* log.
194+
*/
195+
public String solveLog() {
196+
return solveResponse.getSolveLog();
197+
}
198+
220199
private CpSolverResponse solveResponse;
221200
private final SatParameters.Builder solveParameters;
222201
private Consumer<String> logCallback;

ortools/sat/BUILD.bazel

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -904,23 +904,16 @@ cc_test(
904904
":cp_model_cc_proto",
905905
":cp_model_checker",
906906
":cp_model_solver",
907-
":cp_model_solver_helpers",
908907
":cp_model_test_utils",
909-
":cp_model_utils",
910-
":drat_checker",
911908
":lp_utils",
912909
":model",
913-
":sat_base",
914910
":sat_parameters_cc_proto",
915-
":sat_solver",
916-
":synchronization",
917911
"//ortools/base:gmock_main",
918912
"//ortools/base:parse_test_proto",
919913
"//ortools/linear_solver:linear_solver_cc_proto",
920914
"//ortools/port:os",
921915
"//ortools/util:logging",
922916
"@abseil-cpp//absl/container:flat_hash_set",
923-
"@abseil-cpp//absl/flags:flag",
924917
"@abseil-cpp//absl/log",
925918
"@abseil-cpp//absl/strings",
926919
],
@@ -4274,6 +4267,7 @@ cc_library(
42744267
":model",
42754268
":sat_base",
42764269
"//ortools/base:file",
4270+
"@abseil-cpp//absl/flags:flag",
42774271
"@abseil-cpp//absl/log",
42784272
"@abseil-cpp//absl/log:check",
42794273
"@abseil-cpp//absl/strings",

ortools/sat/clause.cc

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ bool LiteralsAreFixedAtRoot(const Trail& trail,
103103
ClauseManager::ClauseManager(Model* model)
104104
: SatPropagator("ClauseManager"),
105105
clause_id_generator_(model->GetOrCreate<ClauseIdGenerator>()),
106+
parameters_(*model->GetOrCreate<SatParameters>()),
107+
assignment_(model->GetOrCreate<Trail>()->Assignment()),
106108
implication_graph_(model->GetOrCreate<BinaryImplicationGraph>()),
107109
trail_(model->GetOrCreate<Trail>()),
108110
num_inspected_clauses_(0),
@@ -327,6 +329,9 @@ SatClause* ClauseManager::AddRemovableClause(ClauseId id,
327329
}
328330
if (add_clause_callback_ != nullptr) add_clause_callback_(lbd, literals);
329331
CHECK(AttachAndPropagate(clause, trail));
332+
333+
// Create an entry in clauses_info_ to mark that clause as removable.
334+
clauses_info_[clause].lbd = lbd;
330335
return clause;
331336
}
332337

@@ -484,6 +489,35 @@ bool ClauseManager::InprocessingFixLiteral(
484489
return implication_graph_->Propagate(trail_);
485490
}
486491

492+
void ClauseManager::ChangeLbdIfBetter(SatClause* clause, int new_lbd) {
493+
auto it = clauses_info_.find(clause);
494+
if (it == clauses_info_.end()) return;
495+
496+
// Always take the min.
497+
if (new_lbd > it->second.lbd) return;
498+
499+
++num_lbd_promotions_;
500+
if (new_lbd <= parameters_.clause_cleanup_lbd_bound()) {
501+
// We keep the clause forever.
502+
clauses_info_.erase(it);
503+
} else {
504+
it->second.lbd = new_lbd;
505+
}
506+
}
507+
508+
bool ClauseManager::RemoveFixedLiteralsAndTestIfTrue(SatClause* clause) {
509+
if (clause->RemoveFixedLiteralsAndTestIfTrue(assignment_)) {
510+
// The clause is always true, detach it.
511+
LazyDelete(clause, DeletionSourceForStat::FIXED_AT_TRUE);
512+
return true;
513+
}
514+
515+
// We should have dealt with unit and unsat clause before this.
516+
CHECK_GE(clause->size(), 2);
517+
ChangeLbdIfBetter(clause, clause->size());
518+
return false;
519+
}
520+
487521
bool ClauseManager::InprocessingRewriteClause(
488522
SatClause* clause, absl::Span<const Literal> new_clause,
489523
absl::Span<const ClauseId> clause_ids) {
@@ -543,6 +577,7 @@ bool ClauseManager::InprocessingRewriteClause(
543577
}
544578

545579
clause->Rewrite(new_clause);
580+
ChangeLbdIfBetter(clause, new_clause.size());
546581

547582
// And we reattach it.
548583
if (all_clauses_are_attached_) {

ortools/sat/clause.h

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -107,14 +107,6 @@ class SatClause {
107107
return absl::Span<const Literal>(&(literals_[0]), size_);
108108
}
109109

110-
// Removes literals that are fixed. This should only be called at level 0
111-
// where a literal is fixed iff it is assigned. Aborts and returns true if
112-
// they are not all false.
113-
//
114-
// Note that the removed literal can still be accessed in the portion [size,
115-
// old_size) of literals().
116-
bool RemoveFixedLiteralsAndTestIfTrue(const VariablesAssignment& assignment);
117-
118110
// Returns true if the clause is satisfied for the given assignment. Note that
119111
// the assignment may be partial, so false does not mean that the clause can't
120112
// be satisfied by completing the assignment.
@@ -134,6 +126,14 @@ class SatClause {
134126
// never be used afterwards.
135127
void Clear() { size_ = 0; }
136128

129+
// Removes literals that are fixed. This should only be called at level 0
130+
// where a literal is fixed iff it is assigned. Aborts and returns true if
131+
// they are not all false.
132+
//
133+
// Note that the removed literal can still be accessed in the portion [size,
134+
// old_size) of literals().
135+
bool RemoveFixedLiteralsAndTestIfTrue(const VariablesAssignment& assignment);
136+
137137
// Rewrites a clause with another shorter one. Note that the clause shouldn't
138138
// be attached when this is called.
139139
void Rewrite(absl::Span<const Literal> new_clause) {
@@ -154,7 +154,7 @@ class SatClause {
154154
struct ClauseInfo {
155155
double activity = 0.0;
156156
int32_t lbd = 0;
157-
bool protected_during_next_cleanup = false;
157+
int32_t num_cleanup_rounds_since_last_bumped = 0;
158158
};
159159

160160
class BinaryImplicationGraph;
@@ -267,6 +267,18 @@ class ClauseManager : public SatPropagator {
267267
if (it == clauses_info_.end()) return 0;
268268
return it->second.lbd;
269269
}
270+
void KeepClauseForever(SatClause* const clause) {
271+
clauses_info_.erase(clause);
272+
}
273+
void RescaleClauseActivities(double scaling_factor) {
274+
for (auto& entry : clauses_info_) {
275+
entry.second.activity *= scaling_factor;
276+
}
277+
}
278+
279+
// If the new lbd is better than the stored one, update it.
280+
// And return the result of IsRemovable() (this save one hash lookup).
281+
void ChangeLbdIfBetter(SatClause* clause, int new_lbd);
270282

271283
// Total number of clauses inspected during calls to Propagate().
272284
int64_t num_inspected_clauses() const { return num_inspected_clauses_; }
@@ -280,6 +292,10 @@ class ClauseManager : public SatPropagator {
280292
// Number of clauses currently watched.
281293
int64_t num_watched_clauses() const { return num_watched_clauses_; }
282294

295+
// Number of time an existing clause lbd was reduced (due to inprocessing or
296+
// recomputation of lbd in different branches).
297+
int64_t num_lbd_promotions() const { return num_lbd_promotions_; }
298+
283299
ClauseId GetClauseId(const SatClause* clause) const {
284300
const auto it = clause_id_.find(clause);
285301
return it != clause_id_.end() ? it->second : kNoClauseId;
@@ -343,6 +359,8 @@ class ClauseManager : public SatPropagator {
343359
SatClause* clause, absl::Span<const Literal> new_clause,
344360
absl::Span<const ClauseId> clause_ids = {});
345361

362+
bool RemoveFixedLiteralsAndTestIfTrue(SatClause* clause);
363+
346364
// Fix a literal either with an existing LRAT `unit_clause_id`, or with a new
347365
// inferred unit clause, using `clause_ids` as proof.
348366
// This do NOT need to be between [Detach/Attach]AllClauses() calls.
@@ -427,14 +445,17 @@ class ClauseManager : public SatPropagator {
427445
SparseBitset<LiteralIndex> needs_cleaning_;
428446
bool is_clean_ = true;
429447

448+
const SatParameters& parameters_;
449+
const VariablesAssignment& assignment_;
430450
BinaryImplicationGraph* implication_graph_;
431451
Trail* trail_;
432452

433453
// For statistic reporting.
434454
std::vector<int64_t> deletion_counters_;
435-
int64_t num_inspected_clauses_;
436-
int64_t num_inspected_clause_literals_;
437-
int64_t num_watched_clauses_;
455+
int64_t num_inspected_clauses_ = 0;
456+
int64_t num_inspected_clause_literals_ = 0;
457+
int64_t num_watched_clauses_ = 0;
458+
int64_t num_lbd_promotions_ = 0;
438459
mutable StatsGroup stats_;
439460

440461
// For DetachAllClauses()/AttachAllClauses().

ortools/sat/cp_model_presolve_test.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8155,10 +8155,10 @@ TEST(PresolveCpModelTest, SolveDiophantine) {
81558155

81568156
SatParameters params;
81578157
params.set_cp_model_presolve(true);
8158+
params.set_num_workers(1);
81588159
// Should solve in < .01 second. Note that deterministic time is not
81598160
// completely accurate.
81608161
params.set_max_deterministic_time(.001);
8161-
params.set_num_workers(1);
81628162
const CpSolverResponse response_with =
81638163
SolveWithParameters(model_proto, params);
81648164

ortools/sat/cp_model_search.cc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,13 @@ std::vector<SatParameters> GetFullWorkerParameters(
934934
params.search_branching() == SatParameters::FIXED_SEARCH) {
935935
continue;
936936
}
937+
// As of November 2025, we don't support any LP reasoning when producing an
938+
// UNSAT proof.
939+
if ((params.check_lrat_proof() || params.check_drat_proof() ||
940+
params.output_drat_proof()) &&
941+
params.linearization_level() > 1) {
942+
continue;
943+
}
937944

938945
// TODO(user): Enable probing_search in deterministic mode.
939946
// Currently it timeouts on small problems as the deterministic time limit

ortools/sat/cp_model_solver.cc

Lines changed: 4 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@
4949
#include "absl/types/span.h"
5050
#include "google/protobuf/arena.h"
5151
#include "google/protobuf/text_format.h"
52-
#include "ortools/base/file.h"
5352
#include "ortools/base/helpers.h"
5453
#include "ortools/base/logging.h"
5554
#include "ortools/base/options.h"
@@ -132,32 +131,6 @@ ABSL_FLAG(bool, cp_model_use_hint_for_debug_only, false,
132131
"complete, validate that no buggy propagator make it infeasible.");
133132
ABSL_FLAG(bool, cp_model_fingerprint_model, true, "Fingerprint the model.");
134133

135-
ABSL_FLAG(std::string, cp_model_drat_output, "",
136-
"If non-empty, a proof in DRAT format will be written to this file. "
137-
"This only works in the same conditions as the --cp_model_lrat_check "
138-
"flag, and only for pure SAT models.");
139-
140-
ABSL_FLAG(bool, cp_model_drat_check, false,
141-
"If true, a proof in DRAT format will be stored in memory and "
142-
"checked if the problem is UNSAT. This only works in the same "
143-
"conditions as the --cp_model_lrat_check flag, and only for pure SAT "
144-
"models.");
145-
146-
ABSL_FLAG(bool, cp_model_lrat_check, false,
147-
"If true, inferred clauses are checked with an LRAT checker as they "
148-
"are learned. As of November 2025, this only works with a single "
149-
"worker and symmetry level 0 or 1. This also works with presolve, if "
150-
"find_clauses_that_are_exactly_one is false and "
151-
"merge_at_most_one_work_limit is 0. However, in this case, the "
152-
"presolved problem is assumed to be correct, without proof. If the "
153-
"model is not pure SAT, the checks are only partial (some clauses "
154-
"can be assumed without proof).");
155-
156-
ABSL_FLAG(double, cp_model_max_drat_time_in_seconds,
157-
std::numeric_limits<double>::infinity(),
158-
"Maximum time in seconds to check the DRAT proof. This will only "
159-
"be used is the cp_model_drat_check flag is enabled.");
160-
161134
ABSL_FLAG(bool, cp_model_check_intermediate_solutions, false,
162135
"When true, all intermediate solutions found by the solver will be "
163136
"checked. This can be expensive, therefore it is off by default.");
@@ -1035,26 +1008,6 @@ bool SolutionHintIsCompleteAndFeasible(
10351008
}
10361009
}
10371010

1038-
std::unique_ptr<LratProofHandler> MaybeCreateLratProofHandler(Model* model) {
1039-
const bool check_lrat = absl::GetFlag(FLAGS_cp_model_lrat_check);
1040-
const bool check_drat = absl::GetFlag(FLAGS_cp_model_drat_check);
1041-
File* drat_output = nullptr;
1042-
if (!absl::GetFlag(FLAGS_cp_model_drat_output).empty()) {
1043-
CHECK_OK(file::Open(absl::GetFlag(FLAGS_cp_model_drat_output), "w",
1044-
&drat_output, file::Defaults()));
1045-
}
1046-
if (!check_lrat && !check_drat && drat_output == nullptr) return nullptr;
1047-
1048-
// TODO(user): pass the [presolved] model proto to the handler, so that
1049-
// it can map internal problem clause IDs to constraint indices in the
1050-
// original model. This will be needed to write the LRAT proof in a file that
1051-
// can be checked with an external LRAT checker, expecting the standard LRAT
1052-
// ASCII file format (which requires problem clauses IDs between 1 and n).
1053-
return std::make_unique<LratProofHandler>(model, check_lrat, check_drat,
1054-
drat_output,
1055-
/*in_binary_drat_format=*/false);
1056-
}
1057-
10581011
// Encapsulate a full CP-SAT solve without presolve in the SubSolver API.
10591012
class FullProblemSolver : public SubSolver {
10601013
public:
@@ -1082,7 +1035,7 @@ class FullProblemSolver : public SubSolver {
10821035
shared_->RegisterSharedClassesInLocalModel(&local_model_);
10831036

10841037
std::unique_ptr<LratProofHandler> lrat_proof_handler =
1085-
MaybeCreateLratProofHandler(&local_model_);
1038+
LratProofHandler::MaybeCreate(&local_model_);
10861039
if (lrat_proof_handler != nullptr) {
10871040
local_model_.Register<LratProofHandler>(lrat_proof_handler.get());
10881041
local_model_.TakeOwnership(lrat_proof_handler.release());
@@ -1111,8 +1064,7 @@ class FullProblemSolver : public SubSolver {
11111064
WallTimer timer;
11121065
timer.Start();
11131066
const bool valid = local_model_.GetOrCreate<SatSolver>()->ModelIsUnsat()
1114-
? lrat_proof_handler->Check(absl::GetFlag(
1115-
FLAGS_cp_model_max_drat_time_in_seconds))
1067+
? lrat_proof_handler->Check()
11161068
: lrat_proof_handler->Valid();
11171069
shared_->lrat_proof_status->NewSubsolverProofStatus(
11181070
valid ? DratChecker::Status::VALID : DratChecker::Status::INVALID,
@@ -1801,12 +1753,8 @@ void SolveCpModelParallel(SharedClasses* shared, Model* global_model) {
18011753
const SatParameters& params = *global_model->GetOrCreate<SatParameters>();
18021754
if (global_model->GetOrCreate<TimeLimit>()->LimitReached()) return;
18031755

1804-
if (absl::GetFlag(FLAGS_cp_model_drat_check) ||
1805-
!absl::GetFlag(FLAGS_cp_model_drat_output).empty()) {
1806-
LOG(WARNING)
1807-
<< "DRAT check and output are skipped when using several workers";
1808-
absl::SetFlag(&FLAGS_cp_model_drat_check, false);
1809-
absl::SetFlag(&FLAGS_cp_model_drat_output, "");
1756+
if (params.check_drat_proof() || params.output_drat_proof()) {
1757+
LOG(FATAL) << "DRAT proofs are not supported with several workers";
18101758
}
18111759

18121760
// If specified by the user, we might disable some parameters based on their

ortools/sat/cp_model_solver.h

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,6 @@
2626

2727
#ifndef SWIG
2828
OR_DLL ABSL_DECLARE_FLAG(bool, cp_model_dump_response);
29-
OR_DLL ABSL_DECLARE_FLAG(bool, cp_model_drat_check);
30-
OR_DLL ABSL_DECLARE_FLAG(bool, cp_model_lrat_check);
31-
OR_DLL ABSL_DECLARE_FLAG(double, cp_model_max_drat_time_in_seconds);
3229
#endif
3330

3431
namespace operations_research {

0 commit comments

Comments
 (0)