Skip to content

Commit 6173e15

Browse files
committed
Add support for unsat cores
We implement basic support for computing unsat cores using our proof producing capabilities. We can obtain unsat core by logging resolution proof; the compact version with resolution chains as logged by CDCL is sufficient. We can then walk the resolution chains from root to the leaves, and collect all the original clauses that participate in the proof. These form unsat core at the clause level. To lift the unsat core to the level of SMT-LIB assertion, we leverage the fact that we propagate partitioning information from top-level assertions to clauses. We can, therefore collect the partition information from clausal unsat core and then just take corresponding top-level assertions.
1 parent b4490d6 commit 6173e15

File tree

8 files changed

+141
-2
lines changed

8 files changed

+141
-2
lines changed

src/api/MainSolver.cc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,20 @@ void MainSolver::printResolutionProofSMT2() const {
228228
return smt_solver->printResolutionProofSMT2(std::cout);
229229
}
230230

231+
vec<PTRef> MainSolver::getUnsatCore() const {
232+
using Partitions = ipartitions_t;
233+
if (not config.produce_unsat_cores()) { throw OsmtApiException("Producing unsat cores is not enabled"); }
234+
if (status != s_False) { throw OsmtApiException("Unsat core cannot be extracted if solver is not in UNSAT state"); }
235+
236+
vec<CRef> clauseRefs = smt_solver->getUnsatCoreClauses();
237+
Partitions partitions;
238+
for (CRef cref : clauseRefs) {
239+
auto const & partition = pmanager.getClauseClassMask(cref);
240+
opensmt::orbit(partitions, partitions, partition);
241+
}
242+
return pmanager.getPartitions(partitions);
243+
}
244+
231245
lbool MainSolver::getTermValue(PTRef tr) const {
232246
if (logic.getSortRef(tr) != logic.getSort_bool()) { return l_Undef; }
233247
if (not term_mapper->hasLit(tr)) { return l_Undef; }

src/api/MainSolver.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ class MainSolver {
9292
// Returns model of the last query (must be in satisfiable state)
9393
std::unique_ptr<Model> getModel();
9494

95+
vec<PTRef> getUnsatCore() const;
96+
9597
// Prints proof of the last query (must be in unsatisfiable state)
9698
void printResolutionProofSMT2() const;
9799

src/options/SMTConfig.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,7 @@ const Info& SMTConfig::getInfo(const char* name) const {
445445
}
446446

447447
const char* SMTConfig::o_produce_models = ":produce-models";
448+
const char* SMTConfig::o_produce_unsat_cores = ":produce-unsat-cores";
448449
const char* SMTConfig::o_verbosity = ":verbosity";
449450
const char* SMTConfig::o_incremental = ":incremental";
450451
const char* SMTConfig::o_produce_stats = ":produce-stats";

src/options/SMTConfig.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ struct SMTConfig
304304
static const char* o_sat_split_randomize_lookahead;
305305
static const char* o_sat_split_randomize_lookahead_buf;
306306
static const char* o_produce_models;
307+
static const char* o_produce_unsat_cores;
307308
static const char* o_sat_remove_symmetries;
308309
static const char* o_dryrun;
309310
static const char* o_do_substitutions;
@@ -511,6 +512,9 @@ struct SMTConfig
511512
return optionTable.has(o_produce_models) ?
512513
optionTable[o_produce_models]->getValue().numval :
513514
1; }
515+
bool produce_unsat_cores() const {
516+
return optionTable.has(o_produce_unsat_cores) && optionTable[o_produce_unsat_cores]->getValue().numval > 0;
517+
}
514518
int produceStats() const
515519
{ return optionTable.has(o_produce_stats) ?
516520
optionTable[o_produce_stats]->getValue().numval == 1: false; }
@@ -579,8 +583,8 @@ struct SMTConfig
579583
{ return optionTable.has(o_certify_inter) ?
580584
optionTable[o_certify_inter]->getValue().numval : 0; }
581585
bool produce_proof() const
582-
{ // produce_inter => produce_proof
583-
if (produce_inter()) return true;
586+
{ // produce_inter or produce_unsat_core => produce_proof
587+
if (produce_inter() or produce_unsat_cores()) return true;
584588
return optionTable.has(o_produce_proofs) ?
585589
optionTable[o_produce_proofs]->getValue().numval > 0 : false; }
586590
bool produce_inter() const

src/smtsolvers/CoreSMTSolver.cc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1897,3 +1897,28 @@ void CoreSMTSolver::fillBooleanVars(ModelBuilder &modelBuilder) {
18971897
modelBuilder.addVarValue(atom, val);
18981898
}
18991899
}
1900+
1901+
vec<CRef> CoreSMTSolver::getUnsatCoreClauses() const {
1902+
assert(resolutionProof);
1903+
vec<CRef> unsatCore;
1904+
auto const & derivations = resolutionProof->getProof();
1905+
std::vector<CRef> stack;
1906+
stack.push_back(CRef_Undef);
1907+
std::unordered_set<CRef> processed;
1908+
while (not stack.empty()) {
1909+
CRef current = stack.back();
1910+
stack.pop_back();
1911+
if (auto [_, inserted] = processed.insert(current); not inserted) { continue; }
1912+
auto it = derivations.find(current);
1913+
assert(it != derivations.end());
1914+
auto const & derivation = it->second;
1915+
if (derivation.type == clause_type::CLA_ORIG) {
1916+
unsatCore.push(current);
1917+
continue;
1918+
}
1919+
if (derivation.type == clause_type::CLA_LEARNT) {
1920+
for (CRef premise : derivation.chain_cla) { stack.push_back(premise); }
1921+
}
1922+
}
1923+
return unsatCore;
1924+
}

src/smtsolvers/CoreSMTSolver.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ class CoreSMTSolver
151151
void fillBooleanVars(ModelBuilder & modelBuilder);
152152

153153
ResolutionProof const & getResolutionProof() const { assert(resolutionProof); return *resolutionProof; }
154+
vec<CRef> getUnsatCoreClauses() const;
154155

155156
// Resource contraints:
156157
//

test/unit/CMakeLists.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,14 @@ target_sources(InterpolationTest
148148
target_link_libraries(InterpolationTest OpenSMT gtest gtest_main)
149149
gtest_add_tests(TARGET InterpolationTest)
150150

151+
add_executable(UnsatCoreTest)
152+
target_sources(UnsatCoreTest
153+
PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/test_UnsatCore.cc"
154+
)
155+
156+
target_link_libraries(UnsatCoreTest OpenSMT gtest gtest_main)
157+
gtest_add_tests(TARGET UnsatCoreTest)
158+
151159
add_executable(ExplainTest)
152160
target_sources(ExplainTest
153161
PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/test_Explain.cc"

test/unit/test_UnsatCore.cc

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright (c) 2024, Martin Blicha <[email protected]>
3+
*
4+
* SPDX-License-Identifier: MIT
5+
*/
6+
7+
#include <gtest/gtest.h>
8+
#include <ArithLogic.h>
9+
#include <MainSolver.h>
10+
#include <SMTConfig.h>
11+
12+
#include <memory>
13+
14+
class UFUnsatCoreTest : public ::testing::Test {
15+
protected:
16+
UFUnsatCoreTest(): logic{opensmt::Logic_t::QF_UF} {}
17+
void SetUp() override {
18+
ufsort = logic.declareUninterpretedSort("U");
19+
x = logic.mkVar(ufsort, "x");
20+
y = logic.mkVar(ufsort, "y");
21+
f = logic.declareFun("f", ufsort, {ufsort});
22+
g = logic.declareFun("g", ufsort, {ufsort, ufsort});
23+
p = logic.declareFun("p", logic.getSort_bool(), {ufsort});
24+
b1 = logic.mkBoolVar("b1");
25+
b2 = logic.mkBoolVar("b2");
26+
b3 = logic.mkBoolVar("b3");
27+
b4 = logic.mkBoolVar("b4");
28+
b5 = logic.mkBoolVar("b5");
29+
nb1 = logic.mkNot(b1);
30+
nb2 = logic.mkNot(b2);
31+
nb3 = logic.mkNot(b3);
32+
nb4 = logic.mkNot(b4);
33+
nb5 = logic.mkNot(b5);
34+
}
35+
Logic logic;
36+
SMTConfig config;
37+
SRef ufsort;
38+
PTRef x, y, b1, b2, b3, b4, b5;
39+
PTRef nb1, nb2, nb3, nb4, nb5;
40+
SymRef f, g, p;
41+
42+
MainSolver makeSolver() {
43+
const char* msg = "ok";
44+
config.setOption(SMTConfig::o_produce_unsat_cores, SMTOption(true), msg);
45+
return {logic, config, "unsat_core"};
46+
}
47+
};
48+
49+
TEST_F(UFUnsatCoreTest, Bool_Simple) {
50+
MainSolver solver = makeSolver();
51+
solver.insertFormula(b1);
52+
solver.insertFormula(b2);
53+
solver.insertFormula(nb1);
54+
auto res = solver.check();
55+
ASSERT_EQ(res, s_False);
56+
auto core = solver.getUnsatCore();
57+
ASSERT_EQ(core.size(), 2);
58+
EXPECT_TRUE(core[0] == b1 or core[1] == b1);
59+
EXPECT_TRUE(core[0] == nb1 or core[1] == nb1);
60+
}
61+
62+
TEST_F(UFUnsatCoreTest, Bool_ReuseProofChain) {
63+
// We add three assertions
64+
// a1 := (b1 or b2) and (b1 or ~b2)
65+
// a2 := (~b1 or b3) and (~b1 or ~b3)
66+
// a3 := b4
67+
// The first two assertions form the unsat core.
68+
MainSolver solver = makeSolver();
69+
PTRef c1 = logic.mkOr(b1, b2);
70+
PTRef c2 = logic.mkOr(b1, nb2);
71+
PTRef c3 = logic.mkOr(nb1, b3);
72+
PTRef c4 = logic.mkOr(nb1, nb3);
73+
PTRef a1 = logic.mkAnd(c1,c2);
74+
PTRef a2 = logic.mkAnd(c3,c4);
75+
solver.insertFormula(a1);
76+
solver.insertFormula(a2);
77+
solver.insertFormula(b4);
78+
auto res = solver.check();
79+
ASSERT_EQ(res, s_False);
80+
auto core = solver.getUnsatCore();
81+
ASSERT_EQ(core.size(), 2);
82+
EXPECT_TRUE(core[0] == a1 or core[1] == a1);
83+
EXPECT_TRUE(core[0] == a2 or core[1] == a2);
84+
}

0 commit comments

Comments
 (0)