3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2025-08-23 03:27:52 +00:00
z3/src/math/polysat/solver.cpp
2023-02-03 17:37:09 +01:00

1558 lines
60 KiB
C++

/*++
Copyright (c) 2021 Microsoft Corporation
Module Name:
polysat
Abstract:
Polynomial solver for modular arithmetic.
Author:
Nikolaj Bjorner (nbjorner) 2021-03-19
Jakob Rath 2021-04-06
--*/
#include "math/polysat/solver.h"
#include "math/polysat/log.h"
#include "math/polysat/polysat_params.hpp"
#include "math/polysat/variable_elimination.h"
#include <variant>
// For development; to be removed once the linear solver works well enough
#define ENABLE_LINEAR_SOLVER 0
namespace polysat {
solver::solver(reslimit& lim):
m_lim(lim),
m_viable(*this),
m_viable_fallback(*this),
m_linear_solver(*this),
m_fixed_bits(*this),
m_conflict(*this),
m_simplify_clause(*this),
m_simplify(*this),
m_restart(*this),
m_bvars(),
m_free_pvars(m_activity),
m_constraints(*this),
m_search(*this) {
}
solver::~solver() {
// Need to remove any lingering clause/constraint references before the constraint manager is destructed
m_conflict.reset();
}
void solver::updt_params(params_ref const& p) {
polysat_params pp(p);
m_params.append(p);
m_config.m_max_conflicts = m_params.get_uint("max_conflicts", UINT_MAX);
m_config.m_max_decisions = m_params.get_uint("max_decisions", UINT_MAX);
m_config.m_log_conflicts = pp.log_conflicts();
m_rand.set_seed(m_params.get_uint("random_seed", 0));
}
bool solver::should_search() {
return
m_lim.inc() &&
(m_stats.m_num_conflicts < get_config().m_max_conflicts) &&
(m_stats.m_num_decisions < get_config().m_max_decisions);
}
lbool solver::check_sat() {
#ifndef NDEBUG
SASSERT(!m_is_solving);
flet<bool> save_(m_is_solving, true);
#endif
LOG("Starting");
while (should_search()) {
m_stats.m_num_iterations++;
LOG_H1("Next solving loop iteration (#" << m_stats.m_num_iterations << ")");
LOG("Free variables: " << m_free_pvars);
LOG("Assignment: " << assignments_pp(*this));
if (is_conflict()) LOG("Conflict: " << m_conflict);
IF_LOGGING(m_viable.log());
SASSERT(var_queue_invariant());
if (is_conflict() && at_base_level()) { LOG_H2("UNSAT"); return l_false; }
else if (is_conflict()) resolve_conflict();
else if (should_add_pwatch()) add_pwatch();
else if (can_propagate()) propagate();
else if (!can_decide()) { LOG_H2("SAT"); VERIFY(verify_sat()); return l_true; }
else if (m_constraints.should_gc()) m_constraints.gc();
else if (m_simplify.should_apply()) m_simplify();
else if (m_restart.should_apply()) m_restart();
else decide();
}
LOG_H2("UNDEF (resource limit)");
return l_undef;
}
lbool solver::unit_propagate() {
return l_undef;
// disabled to allow debugging unsoundness for watched literals
flet<uint64_t> _max_d(m_config.m_max_conflicts, m_stats.m_num_conflicts + 2);
return check_sat();
}
dd::pdd_manager& solver::sz2pdd(unsigned sz) const {
m_pdd.reserve(sz + 1);
if (!m_pdd[sz])
m_pdd.set(sz, alloc(dd::pdd_manager, 1000, dd::pdd_manager::semantics::mod2N_e, sz));
return *m_pdd[sz];
}
dd::pdd_manager& solver::var2pdd(pvar v) const {
return sz2pdd(size(v));
}
unsigned solver::add_var(unsigned sz) {
pvar v = m_value.size();
m_value.push_back(rational::zero());
m_justification.push_back(justification::unassigned());
m_viable.push_var(sz);
m_viable_fallback.push_var(sz);
m_pwatch.push_back({});
m_activity.push_back(0);
m_vars.push_back(sz2pdd(sz).mk_var(v));
m_size.push_back(sz);
m_trail.push_back(trail_instr_t::add_var_i);
m_free_pvars.mk_var_eh(v);
return v;
}
pdd solver::value(rational const& v, unsigned sz) {
return sz2pdd(sz).mk_val(v);
}
void solver::del_var() {
NOT_IMPLEMENTED_YET(); // need to take care of helper variables in learned constraints, arising e.g. from bitwise "and"-terms.
// TODO also remove v from all learned constraints.
pvar v = m_value.size() - 1;
m_viable.pop_var();
m_viable_fallback.pop_var();
m_value.pop_back();
m_justification.pop_back();
m_pwatch.pop_back();
m_activity.pop_back();
m_vars.pop_back();
m_size.pop_back();
m_free_pvars.del_var_eh(v);
}
void solver::assign_eh(signed_constraint c, dependency dep) {
// This method is part of the external interface and should not be used to create internal constraints during solving.
SASSERT(!m_is_solving);
backjump(base_level());
SASSERT(at_base_level());
SASSERT(c);
if (is_conflict())
return; // no need to do anything if we already have a conflict at base level
sat::literal lit = c.blit();
LOG("New constraint: " << lit_pp(*this, c));
switch (m_bvars.value(lit)) {
case l_false:
// Input literal contradicts current boolean state (e.g., opposite literals in the input)
// => conflict only flags the inconsistency
set_conflict_at_base_level(dep);
return;
case l_true:
// constraint c is already asserted => ignore
SASSERT(m_bvars.level(lit) <= m_level);
return;
case l_undef:
break;
default:
UNREACHABLE();
}
switch (c.eval()) {
case l_false:
// asserted an always-false constraint => conflict at base level
LOG("Always false: " << c);
set_conflict_at_base_level(dep);
return;
case l_true:
// asserted an always-true constraint => ignore
return;
case l_undef:
break;
default:
UNREACHABLE();
}
m_bvars.assumption(lit, m_level, dep);
m_trail.push_back(trail_instr_t::assign_bool_i);
m_search.push_boolean(lit);
if (c.is_currently_false(*this))
set_conflict(c);
#if ENABLE_LINEAR_SOLVER
m_linear_solver.new_constraint(*c.get());
#endif
}
bool solver::can_propagate() {
return m_qhead < m_search.size() && !is_conflict();
}
void solver::propagate() {
if (!can_propagate())
return;
#ifndef NDEBUG
SASSERT(!m_is_propagating);
flet<bool> save_(m_is_propagating, true);
#endif
push_qhead();
while (can_propagate()) {
auto const& item = m_search[m_qhead++];
if (item.is_assignment())
propagate(item.var());
else
propagate(item.lit());
}
if (!is_conflict())
linear_propagate();
SASSERT(wlist_invariant());
SASSERT(bool_watch_invariant());
SASSERT(eval_invariant());
}
/**
* Propagate assignment to a Boolean variable
*/
void solver::propagate(sat::literal lit) {
LOG_H2("Propagate bool " << lit << "@" << m_bvars.level(lit) << " " << m_level << " qhead: " << m_qhead);
LOG("Literal " << lit_pp(*this, lit));
signed_constraint c = lit2cnstr(lit);
activate_constraint(c);
auto& wlist = m_bvars.watch(~lit);
LOG("wlist[" << ~lit << "]: " << wlist);
unsigned i = 0, j = 0, sz = wlist.size();
for (; i < sz && !is_conflict(); ++i)
if (!propagate(lit, *wlist[i]))
wlist[j++] = wlist[i];
for (; i < sz; ++i)
wlist[j++] = wlist[i];
wlist.shrink(j);
}
/**
* Propagate assignment to a pvar
*/
void solver::propagate(pvar v) {
LOG_H2("Propagate " << assignment_pp(*this, v, get_value(v)));
SASSERT(!m_locked_wlist);
DEBUG_CODE(m_locked_wlist = v;);
unsigned i = 0, j = 0;
for (; i < m_pwatch[v].size() && !is_conflict(); ++i)
if (!propagate(v, m_pwatch[v][i])) // propagate may change watch-list reference
m_pwatch[v][j++] = m_pwatch[v][i];
auto& wlist = m_pwatch[v];
for (; i < wlist.size(); ++i)
wlist[j++] = wlist[i];
wlist.shrink(j);
if (is_conflict())
shuffle(wlist.size(), wlist.data(), m_rand);
DEBUG_CODE(m_locked_wlist = std::nullopt;);
}
/**
* Propagate assignment to variable v into constraint c.
* Return true if a new watch was found; or false to keep the existing one.
*/
bool solver::propagate(pvar v, constraint* c) {
lbool const bvalue = m_bvars.value(c->bvar());
LOG_H3("Propagate " << m_vars[v] << " in " << constraint_pp(c, bvalue));
SASSERT(is_assigned(v));
SASSERT(!c->vars().empty());
unsigned idx = 0;
if (c->var(idx) != v)
idx = 1;
SASSERT(v == c->var(idx));
// find other watch variable.
for (unsigned i = c->vars().size(); i-- > 2; ) {
unsigned other_v = c->vars()[i];
if (!is_assigned(other_v)) {
add_pwatch(c, other_v);
std::swap(c->vars()[idx], c->vars()[i]);
return true;
}
}
// at most one pvar remains unassigned
if (bvalue != l_undef) {
// constraint state: bool-propagated
signed_constraint sc(c, bvalue == l_true);
if (c->vars().size() >= 2) {
unsigned other_v = c->var(1 - idx);
if (!is_assigned(other_v))
m_viable_fallback.push_constraint(other_v, sc);
}
sc.narrow(*this, false);
} else {
// constraint state: active but unassigned (bvalue undef, but pwatch is set; e.g., new constraints generated for lemmas)
#if 1
if (c->vars().size() >= 2) {
unsigned other_v = c->var(1 - idx);
// Wait for the remaining variable to be assigned
// (although sometimes we would be able to evaluate constraints earlier)
if (!is_assigned(other_v))
return false;
}
// Evaluate constraint
signed_constraint sc(c, true);
if (sc.is_currently_true(*this))
assign_eval(sc.blit());
else {
SASSERT(sc.is_currently_false(*this));
assign_eval(~sc.blit());
}
#else
signed_constraint sc(c, true);
switch (sc.eval(*this)) {
case l_true:
assign_eval(sc.blit());
break;
case l_false:
assign_eval(~sc.blit());
break;
default:
break;
}
#endif
}
return false;
}
/**
* Propagate boolean assignment of literal lit into clause cl.
* Return true if a new watch was found; or false to keep the existing one.
*/
bool solver::propagate(sat::literal lit, clause& cl) {
SASSERT(m_bvars.is_true(lit));
SASSERT(cl.size() >= 2);
unsigned idx = (cl[0] == ~lit) ? 1 : 0;
SASSERT(cl[1 - idx] == ~lit);
if (m_bvars.is_true(cl[idx]))
return false;
unsigned i = 2;
for (; i < cl.size() && m_bvars.is_false(cl[i]); ++i);
if (i < cl.size()) {
// found non-false literal in cl; watch it instead
m_bvars.watch(cl[i]).push_back(&cl);
std::swap(cl[1 - idx], cl[i]);
return true;
}
// all literals in cl are false, except possibly the other watch cl[idx]
if (m_bvars.is_false(cl[idx]))
set_conflict(cl);
else
assign_propagate(cl[idx], cl);
return false;
}
void solver::linear_propagate() {
#if ENABLE_LINEAR_SOLVER
switch (m_linear_solver.check()) {
case l_false:
// TODO extract conflict
break;
default:
break;
}
#endif
}
/** Enqueue constraint c to perform add_pwatch(c) on the next solver iteration */
void solver::enqueue_pwatch(constraint* c) {
SASSERT(c);
if (c->is_pwatched())
return;
m_pwatch_queue.push_back(c);
}
bool solver::should_add_pwatch() const {
return !m_pwatch_queue.empty();
}
void solver::add_pwatch() {
for (constraint* c : m_pwatch_queue) {
add_pwatch(c);
}
m_pwatch_queue.reset();
}
void solver::add_pwatch(constraint* c) {
SASSERT(c);
if (c->is_pwatched())
return;
auto& vars = c->vars();
unsigned i = 0, j = 0, sz = vars.size();
for (; i < sz && j < 2; ++i)
if (!is_assigned(vars[i]))
std::swap(vars[i], vars[j++]);
if (vars.size() > 0)
add_pwatch(c, vars[0]);
if (vars.size() > 1)
add_pwatch(c, vars[1]);
c->set_pwatched(true);
#if 0
m_pwatch_trail.push_back(c);
m_trail.push_back(trail_instr_t::pwatch_i);
#endif
}
void solver::add_pwatch(constraint* c, pvar v) {
SASSERT(m_locked_wlist != v); // the propagate loop will not discover the new size
LOG_V(20, "Watching v" << v << " in constraint " << show_deref(c));
m_pwatch[v].push_back(c);
}
void solver::erase_pwatch(constraint* c) {
if (!c->is_pwatched())
return;
auto const& vars = c->vars();
if (vars.size() > 0)
erase_pwatch(vars[0], c);
if (vars.size() > 1)
erase_pwatch(vars[1], c);
c->set_pwatched(false);
}
void solver::erase_pwatch(pvar v, constraint* c) {
if (v == null_var)
return;
SASSERT(m_locked_wlist != v);
LOG("Unwatching v" << v << " in constraint " << show_deref(c));
auto& wlist = m_pwatch[v];
unsigned sz = wlist.size();
for (unsigned i = 0; i < sz; ++i) {
if (c == wlist[i]) {
wlist[i] = wlist.back();
wlist.pop_back();
return;
}
}
}
void solver::push_level() {
++m_level;
m_trail.push_back(trail_instr_t::inc_level_i);
#if ENABLE_LINEAR_SOLVER
m_linear_solver.push();
#endif
m_fixed_bits.push();
}
void solver::pop_levels(unsigned num_levels) {
if (num_levels == 0)
return;
SASSERT(m_level >= num_levels);
unsigned const target_level = m_level - num_levels;
using replay_item = std::variant<sat::literal, pvar>;
vector<replay_item> replay;
sat::literal_vector repropagate;
LOG("Pop " << num_levels << " levels (lvl " << m_level << " -> " << target_level << ")");
#if ENABLE_LINEAR_SOLVER
m_linear_solver.pop(num_levels);
#endif
m_fixed_bits.pop();
while (num_levels > 0) {
switch (m_trail.back()) {
case trail_instr_t::qhead_i: {
pop_qhead();
break;
}
case trail_instr_t::lemma_qhead_i: {
m_lemmas_qhead--;
break;
}
case trail_instr_t::add_lemma_i: {
m_lemmas.pop_back();
break;
}
#if 0
// NOTE: erase_pwatch should be called when the constraint is deleted from the solver.
case trail_instr_t::pwatch_i: {
constraint* c = m_pwatch_trail.back();
erase_pwatch(c);
m_pwatch_trail.pop_back();
break;
}
#endif
case trail_instr_t::add_var_i: {
// NOTE: currently we cannot delete variables during solving,
// since lemmas may introduce new helper variables if they use operations like bitwise and or pseudo-inverse.
// For easier experimentation with new lemmas, we simply keep all variables for now.
#if 0
del_var();
#endif
break;
}
case trail_instr_t::inc_level_i: {
--m_level;
--num_levels;
break;
}
case trail_instr_t::viable_add_i: {
m_viable.pop_viable();
break;
}
case trail_instr_t::viable_rem_i: {
m_viable.push_viable();
break;
}
case trail_instr_t::viable_constraint_i: {
m_viable_fallback.pop_constraint();
break;
}
case trail_instr_t::assign_i: {
auto v = m_search.back().var();
LOG_V(20, "Undo assign_i: v" << v);
unsigned active_level = get_level(v);
if (active_level <= target_level) {
SASSERT(m_justification[v].is_propagation());
replay.push_back(v);
}
else {
m_free_pvars.unassign_var_eh(v);
m_justification[v] = justification::unassigned();
}
m_search.pop();
break;
}
case trail_instr_t::assign_bool_i: {
sat::literal lit = m_search.back().lit();
LOG_V(20, "Undo assign_bool_i: " << lit);
unsigned active_level = m_bvars.level(lit);
if (m_bvars.is_decision(lit))
repropagate.push_back(lit);
if (active_level <= target_level) {
SASSERT(!m_bvars.is_decision(lit));
replay.push_back(lit);
} else
m_bvars.unassign(lit);
m_search.pop();
break;
}
default:
UNREACHABLE();
}
m_trail.pop_back();
}
m_constraints.release_level(m_level + 1);
SASSERT(m_level == target_level);
// Repropagate:
// Consider the following case:
// 1. Literal L is decision@2
// 2. We are at level 10, add a clause C := A & B ==> L where A@0, B@0 (i.e., L should be propagation@0 now)
// 3. For whatever reason we now backtrack to 0 or 1.
// We have unassigned L but the unit propagation for C does not trigger.
// 4. To fix that situation we explicitly "repropagate" C after backtracking.
for (sat::literal lit : repropagate)
for (clause* cl : m_bvars.watch(lit))
propagate_clause(*cl);
// Replay:
// (since levels on the search stack may be out of order)
for (unsigned j = replay.size(); j-- > 0; ) {
replay_item item = replay[j];
std::visit([this](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, sat::literal>) {
sat::literal lit = arg;
m_search.push_boolean(lit);
m_trail.push_back(trail_instr_t::assign_bool_i);
LOG("Replay: " << lit);
}
else if constexpr (std::is_same_v<T, pvar>) {
pvar v = arg;
m_search.push_assignment(v, m_value[v]);
m_trail.push_back(trail_instr_t::assign_i);
LOG("Replay: " << assignment_pp(*this, v, m_value[v]));
// TODO: are the viable sets propagated properly?
// when substituting polynomials, it will now take into account the replayed variables, which may itself depend on previous propagations.
// will we get into trouble with cyclic dependencies?
// But we do want to take into account variables that are assigned but not yet propagated.
// Possible solutions:
// - keep the replay queue outside of this method?
// prioritize putting stuff on the stack from the replay queue.
// this might however introduce new propagations in between? maybe that is ok?
// - when evaluating/narrowing instead of passing the full assignment,
// we pass a "dependency level" which is basically an index into the search stack.
// then we get an assignment up to that dependency level.
// each literal can only depend on entries with lower dependency level
// (that is the invariant that propagations are justified by a prefix of the search stack.)
// Actually, cyclic dependencies probably don't happen:
// - viable restrictions only occur when all-but-one variable is set (or some vars are irrelevant... those might introduce additional fake dependencies)
// - we only replay propagations... so all new variable assignments are propagations (that depend on earlier decisions)
// - but now the replayed constraints may evaluate to true already and thus not give the forbidden intervals from before anymore...
// so maybe we miss some dependencies this way? a variable was propagated because of a constraint, but after replay the constraint evaluates to true and thus does not add an interval anymore.
// TODO: work out example to explain and test this
}
else
static_assert(always_false<T>::value, "non-exhaustive visitor");
}, item);
}
}
bool solver::can_decide() const {
return can_pdecide() || can_bdecide();
}
bool solver::can_pdecide() const {
return !m_free_pvars.empty();
}
bool solver::can_bdecide() const {
return m_lemmas_qhead < m_lemmas.size();
}
void solver::decide() {
LOG_H2("Decide");
SASSERT(can_decide());
#if 1
// Simple hack to try deciding the boolean skeleton first
if (!can_bdecide()) {
// enqueue all not-yet-true clauses
for (clause const& cl : m_constraints.clauses()) {
bool is_true = any_of(cl, [&](sat::literal lit) { return m_bvars.is_true(lit); });
if (is_true)
continue;
size_t undefs = count_if(cl, [&](sat::literal lit) { return !m_bvars.is_assigned(lit); });
if (undefs >= 2)
m_lemmas.push_back(&cl);
else {
SASSERT_EQ(undefs, 0); // if !is_true && undefs == 1 then we missed a propagation.
}
}
}
#endif
if (can_bdecide())
bdecide();
else
pdecide(m_free_pvars.next_var());
}
void solver::bdecide() {
clause const& lemma = *m_lemmas[m_lemmas_qhead++];
on_scope_exit update_trail = [this]() {
// must be done after push_level, but also if we return early.
m_trail.push_back(trail_instr_t::lemma_qhead_i);
};
LOG_H2("Decide on non-asserting lemma: " << lemma);
for (sat::literal lit : lemma) {
LOG(lit_pp(*this, lit));
}
sat::literal choice = sat::null_literal;
for (sat::literal lit : lemma) {
switch (m_bvars.value(lit)) {
case l_true:
// Clause is satisfied; nothing to do here
// Happens when all other branches of the lemma have been tried.
// The last branch is entered due to propagation, while the lemma is still on the stack as a decision point.
LOG("Skip decision (clause already satisfied)");
return;
case l_false:
break;
case l_undef:
if (choice == sat::null_literal)
choice = lit;
break;
}
}
LOG("Choice is " << lit_pp(*this, choice));
SASSERT(choice != sat::null_literal);
SASSERT(2 <= count_if(lemma, [this](sat::literal lit) { return !m_bvars.is_assigned(lit); }));
SASSERT(can_pdecide()); // if !can_pdecide(), all boolean literals have been evaluated
push_level();
assign_decision(choice);
}
void solver::pdecide(pvar v) {
LOG("Decide v" << v);
IF_LOGGING(m_viable.log(v));
rational val;
justification j;
switch (m_viable.find_viable(v, val)) {
case find_t::empty:
UNREACHABLE(); // should have been discovered earlier in viable::intersect
return;
case find_t::singleton:
UNREACHABLE(); // should have been discovered earlier in viable::intersect
return;
case find_t::multiple:
j = justification::decision(m_level + 1);
break;
case find_t::resource_out:
verbose_stream() << "TODO: solver::pdecide got resource_out\n";
m_lim.cancel();
return;
default:
UNREACHABLE();
return;
}
SASSERT(j.is_decision());
push_level();
assign_core(v, val, j);
}
void solver::assign_propagate(pvar v, rational const& val) {
LOG("Propagation: " << assignment_pp(*this, v, val));
SASSERT(!is_assigned(v));
SASSERT(m_viable.is_viable(v, val));
m_free_pvars.del_var_eh(v);
// NOTE:
// The propagation v := val might depend on a lower level than the current level (m_level).
// This can happen if the constraints that cause the propagation have been bool-propagated at an earlier level,
// but appear later in the stack (cf. replay).
// The level of v should then also be the earlier level instead of m_level.
unsigned lvl = 0;
for (signed_constraint c : m_viable.get_constraints(v)) {
LOG("due to: " << lit_pp(*this, c));
if (!m_bvars.is_assigned(c.blit())) // side condition, irrelevant because all vars are already in the main condition
continue;
SASSERT(m_bvars.is_assigned(c.blit()));
lvl = std::max(lvl, m_bvars.level(c.blit()));
for (pvar w : c->vars())
if (is_assigned(w)) // TODO: question of which variables are relevant. e.g., v1 > v0 implies v1 > 0 without dependency on v0. maybe add a lemma v1 > v0 --> v1 > 0 on the top level to reduce false variable dependencies? instead of handling such special cases separately everywhere.
lvl = std::max(lvl, get_level(w));
}
// NOTE: we do not have to check the univariate solver here.
// Since we propagate, this means at most the single value 'val' is viable.
// If it is not actually viable, the propagation loop will find out and enter the conflict state.
// (However, if we do check here, we might find the conflict earlier. Might be worth a try.)
assign_core(v, val, justification::propagation(lvl));
}
void solver::assign_core(pvar v, rational const& val, justification const& j) {
VERIFY(!is_assigned(v));
if (j.is_decision())
++m_stats.m_num_decisions;
else
++m_stats.m_num_propagations;
LOG(assignment_pp(*this, v, val) << " by " << j);
SASSERT(m_viable.is_viable(v, val));
SASSERT(j.is_decision() || j.is_propagation());
SASSERT(j.level() <= m_level);
SASSERT(!is_assigned(v));
SASSERT(all_of(get_assignment(), [v](auto p) { return p.first != v; }));
m_value[v] = val;
m_search.push_assignment(v, val);
m_trail.push_back(trail_instr_t::assign_i);
m_justification[v] = j;
#if ENABLE_LINEAR_SOLVER
// TODO: convert justification into a format that can be tracked in a dependency core.
m_linear_solver.set_value(v, val, UINT_MAX);
#endif
}
/**
* Conflict resolution.
*/
void solver::resolve_conflict() {
LOG_H2("Resolve conflict");
LOG("\n" << *this);
LOG(search_state_pp(m_search, true));
++m_stats.m_num_conflicts;
SASSERT(is_conflict());
for (unsigned i = m_search.size(); i-- > 0; ) {
auto& item = m_search[i];
m_search.set_resolved(i);
if (item.is_assignment()) {
// Resolve over variable assignment
pvar v = item.var();
if (!m_conflict.is_relevant_pvar(v)) {
continue;
}
LOG_H2("Working on " << search_item_pp(m_search, item));
LOG(m_justification[v]);
LOG("Conflict: " << m_conflict);
justification const& j = m_justification[v];
// NOTE: propagation level may be out of order (cf. replay), but decisions are always in order
if (j.level() <= base_level()) {
if (j.is_decision()) {
report_unsat();
return;
}
continue;
}
if (j.is_decision()) {
m_conflict.revert_pvar(v);
revert_decision(v);
return;
}
m_conflict.resolve_value(v);
}
else {
// Resolve over boolean literal
SASSERT(item.is_boolean());
sat::literal const lit = item.lit();
sat::bool_var const var = lit.var();
if (!m_conflict.is_relevant(lit))
continue;
LOG_H2("Working on " << search_item_pp(m_search, item));
LOG(bool_justification_pp(m_bvars, lit));
LOG("Literal " << lit << " is " << lit2cnstr(lit));
LOG("Conflict: " << m_conflict);
// NOTE: the levels of boolean literals on the stack aren't always ordered by level (cf. replay functionality in pop_levels).
// Thus we can only skip base level literals here, instead of aborting the loop.
if (m_bvars.level(var) <= base_level()) {
if (m_bvars.is_decision(var)) {
report_unsat(); // decisions are always in order
return;
}
continue;
}
SASSERT(!m_bvars.is_assumption(var)); // TODO: "assumption" is basically "propagated by unit clause" (or "at base level"); except we do not explicitly store the unit clause.
if (m_bvars.is_decision(var)) {
revert_bool_decision(lit);
return;
}
if (m_bvars.is_bool_propagation(var))
// TODO: this could be a propagation at an earlier level.
// do we really want to resolve these eagerly?
m_conflict.resolve_bool(lit, *m_bvars.reason(lit));
else
m_conflict.resolve_evaluated(lit);
}
}
LOG("End of resolve_conflict loop");
m_conflict.logger().end_conflict();
report_unsat();
}
void solver::revert_decision(pvar v) {
unsigned max_jump_level = get_level(v) - 1;
backjump_and_learn(max_jump_level, false);
}
void solver::revert_bool_decision(sat::literal const lit) {
unsigned max_jump_level = m_bvars.level(lit) - 1;
backjump_and_learn(max_jump_level, true);
SASSERT(m_level < max_jump_level || m_bvars.is_false(lit));
}
std::optional<lemma_score> solver::compute_lemma_score(clause const& lemma) {
unsigned max_level = 0; // highest level in lemma
unsigned lits_at_max_level = 0; // how many literals at the highest level in lemma
unsigned snd_level = 0; // second-highest level in lemma
bool is_propagation = false; // whether there is an unassigned literal (at most one)
for (sat::literal lit : lemma) {
if (m_bvars.is_true(lit)) // may happen if we only use the clause to justify a new constraint; it is not a real lemma
return std::nullopt;
if (!m_bvars.is_assigned(lit)) {
DEBUG_CODE({
if (lit2cnstr(lit).eval(*this) != l_undef)
LOG("WARNING: missed evaluation of literal: " << lit_pp(*this, lit));
});
SASSERT(!is_propagation);
is_propagation = true;
continue;
}
unsigned const lit_level = m_bvars.level(lit);
if (lit_level > max_level) {
snd_level = max_level;
max_level = lit_level;
lits_at_max_level = 1;
}
else if (lit_level == max_level)
lits_at_max_level++;
else if (max_level > lit_level && lit_level > snd_level)
snd_level = lit_level;
}
SASSERT(lemma.empty() || lits_at_max_level > 0);
// The MCSAT paper distinguishes between "UIP clauses" and "semantic split clauses".
// It is the same as our distinction between "asserting" and "non-asserting" lemmas.
// - UIP clause: a single literal on the highest decision level in the lemma.
// Do the standard backjumping known from SAT solving (back to second-highest level in the lemma, propagate it there).
// - Semantic split clause: multiple literals on the highest level in the lemma.
// Backtrack to "highest level - 1" and split on the lemma there.
// For now, we follow the same convention for computing the jump levels,
// but we support an additional type of clause:
// - Propagation clause: a single literal is unassigned and should be propagated after backjumping.
// backjump to max_level so we can propagate
unsigned jump_level;
unsigned branching_factor = lits_at_max_level;
if (is_propagation)
jump_level = max_level, branching_factor = 1;
else if (lits_at_max_level <= 1)
jump_level = snd_level;
else
jump_level = (max_level == 0) ? 0 : (max_level - 1);
return {{jump_level, branching_factor}};
}
void solver::backjump_and_learn(unsigned max_jump_level, bool force_fallback_lemma) {
sat::literal_vector narrow_queue = m_conflict.take_narrow_queue();
clause_ref_vector lemmas = m_conflict.take_lemmas();
// Select the "best" lemma
// - lowest jump level
// - lowest number of literals at the highest level
// We must do so before backjump() when the search stack is still intact.
lemma_score best_score = lemma_score::max();
clause* best_lemma = nullptr;
auto appraise_lemma = [&](clause* lemma) {
auto score = compute_lemma_score(*lemma);
if (score)
LOG(" score: " << *score);
else
LOG(" score: <none>");
if (score && *score < best_score) {
best_score = *score;
best_lemma = lemma;
}
};
for (clause* lemma : lemmas)
appraise_lemma(lemma);
if (force_fallback_lemma || !best_lemma || best_score.jump_level() > max_jump_level) {
// No (good) lemma has been found, so build the fallback lemma from the conflict state.
lemmas.push_back(m_conflict.build_lemma());
appraise_lemma(lemmas.back());
}
if (!best_lemma) {
verbose_stream() << "conflict: " << m_conflict << "\n";
verbose_stream() << "no lemma\n";
for (clause* cl: lemmas) {
verbose_stream() << *cl << "\n";
for (sat::literal lit : *cl)
verbose_stream() << " " << lit_pp(*this, lit) << "\n";
}
}
SASSERT(best_score < lemma_score::max());
VERIFY(best_lemma);
unsigned const jump_level = std::max(best_score.jump_level(), base_level());
SASSERT(jump_level <= max_jump_level);
LOG("best_score: " << best_score);
LOG("best_lemma: " << *best_lemma);
m_conflict.reset();
backjump(jump_level);
for (sat::literal lit : narrow_queue) {
LOG("Narrow queue: " << lit_pp(*this, lit));
switch (m_bvars.value(lit)) {
case l_true:
lit2cnstr(lit).narrow(*this, false);
break;
case l_false:
lit2cnstr(~lit).narrow(*this, false);
break;
case l_undef:
/* do nothing */
break;
default:
UNREACHABLE();
}
if (is_conflict()) {
// The remainder of narrow_queue is forgotten at this point,
// but keep the lemmas for later.
for (clause* lemma : lemmas)
m_conflict.restore_lemma(lemma);
return;
}
}
for (auto it = lemmas.begin(); it != lemmas.end(); ++it) {
clause& lemma = **it;
if (!lemma.is_active())
add_clause(lemma);
else
propagate_clause(lemma);
// NOTE: currently, the backjump level is an overapproximation,
// since the level of evaluated constraints may not be exact (see TODO in assign_eval).
// For this reason, we may actually get a conflict at this point
// (because the actual jump_level of the lemma may be lower that best_level.)
if (is_conflict()) {
// Keep the remaining lemmas for later.
for (; it != lemmas.end(); ++it)
m_conflict.restore_lemma(*it);
return;
}
}
if (best_score.branching_factor() > 1) {
// NOTE: at this point it is possible that the best_lemma is non-asserting.
// We need to double-check, because the backjump level may not be exact (see comment on checking is_conflict above).
bool const is_asserting = all_of(*best_lemma, [this](sat::literal lit) { return m_bvars.is_assigned(lit); });
if (!is_asserting) {
LOG_H3("Main lemma is not asserting: " << *best_lemma);
for (sat::literal lit : *best_lemma) {
LOG(lit_pp(*this, lit));
}
m_lemmas.push_back(best_lemma);
m_trail.push_back(trail_instr_t::add_lemma_i);
// TODO: currently we forget non-asserting lemmas when backjumping over them.
// We surely don't want to keep them in m_lemmas because then we will start doing case splits
// even if the lemma should instead be waiting for propagations.
// We could instead watch its pvars and re-insert into m_lemmas when all but one are assigned.
// The same could even be done in general for all lemmas, instead of distinguishing between
// asserting and non-asserting lemmas.
// (Note that the same lemma can be asserting in one branch of the search but non-asserting in another,
// depending on which pvars are assigned.)
SASSERT(can_bdecide());
}
}
} // backjump_and_learn
// Explicit boolean propagation over the given clause, without relying on watch lists.
void solver::propagate_clause(clause& cl) {
sat::literal prop = sat::null_literal;
for (sat::literal lit : cl) {
if (m_bvars.is_true(lit))
return; // clause is true
if (m_bvars.is_false(lit))
continue;
SASSERT(!m_bvars.is_assigned(lit));
if (prop != sat::null_literal)
return; // two or more undef literals
prop = lit;
}
if (prop == sat::null_literal)
return;
assign_propagate(prop, cl);
}
unsigned solver::level(sat::literal lit0, clause const& cl) {
unsigned lvl = 0;
for (auto lit : cl) {
if (lit == lit0)
continue;
SASSERT(m_bvars.is_false(lit));
lvl = std::max(lvl, m_bvars.level(lit));
}
return lvl;
}
void solver::assign_decision(sat::literal lit) {
SASSERT(lit != sat::null_literal);
m_bvars.decision(lit, m_level);
m_trail.push_back(trail_instr_t::assign_bool_i);
m_search.push_boolean(lit);
}
void solver::assign_propagate(sat::literal lit, clause& reason) {
SASSERT(lit != sat::null_literal);
m_bvars.propagate(lit, level(lit, reason), reason);
m_trail.push_back(trail_instr_t::assign_bool_i);
m_search.push_boolean(lit);
}
void solver::assign_eval(sat::literal lit) {
signed_constraint const c = lit2cnstr(lit);
LOG_V(10, "Evaluate: " << lit_pp(*this, lit));
if (!c.is_currently_true(*this)) IF_VERBOSE(0, verbose_stream() << c << " is not currently true\n");
SASSERT(c.is_currently_true(*this));
VERIFY_EQ(c.eval(*this), l_true);
unsigned level = 0;
// NOTE: constraint may be evaluated even if some variables are still unassigned (e.g., 0*x doesn't depend on x).
for (auto v : c->vars())
if (is_assigned(v))
level = std::max(get_level(v), level);
// TODO: the level computed here is not exact, because evaluation of constraints may not depend on all variables that occur in the constraint.
// For example, consider x := 0 @ 1 and y := 0 @ 3. Then x*y == 0 eval@3, even though we can already evaluate it at level 1.
// To get the exact level:
// - consider the levels get_level(var) for var in c->vars().
// - the maximum of these is the estimate we start with (and which we currently use)
// - successively reduce the level, as long as the constraint still evaluates
m_bvars.eval(lit, level);
m_trail.push_back(trail_instr_t::assign_bool_i);
m_search.push_boolean(lit);
}
/** Push c onto \Gamma, unless it is already true. */
void solver::try_assign_eval(signed_constraint c) {
sat::literal const lit = c.blit();
if (m_bvars.is_assigned(lit))
return;
if (c.is_always_true())
return;
assign_eval(lit);
}
sat::literal solver::try_eval(sat::literal lit) {
signed_constraint const c = lit2cnstr(lit);
switch (c.eval(*this)) {
case l_true:
assign_eval(lit);
break;
case l_false:
assign_eval(~lit);
break;
default:
break;
}
return lit;
}
/**
* Activate constraint immediately
* Activation and de-activation of constraints follows the scope controlled by push/pop.
* constraints activated within the linear solver are de-activated when the linear
* solver is popped.
*/
void solver::activate_constraint(signed_constraint c) {
SASSERT(c);
LOG("Activating constraint: " << c);
SASSERT_EQ(m_bvars.value(c.blit()), l_true);
add_pwatch(c.get());
pvar v = null_var;
if (c->vars().size() == 1)
// If there is exactly one variable in c, then c is always univariate.
v = c->vars()[0];
else {
// Otherwise, check if exactly one variable in c remains unassigned.
for (pvar w : c->vars()) {
if (is_assigned(w))
continue;
if (v != null_var) {
// two or more unassigned vars; abort
v = null_var;
break;
}
v = w;
}
}
if (v != null_var)
m_viable_fallback.push_constraint(v, c);
// TODO: we use narrow with first=true to add axioms about the constraint to the solver.
// However, constraints can be activated multiple times (e.g., if it comes from a lemma and is propagated at a non-base level).
// So the same axioms may be added multiple times.
// Maybe separate narrow/activate? And keep a flag in m_bvars to remember whether constraint has already been activated.
c.narrow(*this, true);
#if ENABLE_LINEAR_SOLVER
m_linear_solver.activate_constraint(c);
#endif
}
void solver::backjump(unsigned new_level) {
SASSERT(new_level >= base_level());
if (m_level != new_level) {
LOG_H3("Backjumping to level " << new_level << " from level " << m_level << " (base_level: " << base_level() << ")");
pop_levels(m_level - new_level);
}
}
void solver::add_clause(clause_ref clause) {
VERIFY(clause);
add_clause(*clause);
}
// Add clause to solver
void solver::add_clause(clause& clause) {
LOG((clause.is_redundant() ? "Lemma: ": "Aux: ") << clause);
for (sat::literal lit : clause) {
// Try to evaluate literals without boolean value.
// (Normally this should have been done already by using clause_builder::insert_eval/insert_try_eval when constructing the clause.)
if (!m_bvars.is_assigned(lit)) {
lbool const status = lit2cnstr(lit).eval(*this);
if (status == l_true)
assign_eval(lit);
else if (status == l_false)
assign_eval(~lit);
}
LOG(" " << lit_pp(*this, lit));
}
SASSERT(!clause.empty());
m_constraints.store(&clause);
// Defer add_pwatch until the next solver iteration, because during propagation of a variable v the watchlist for v is locked.
// NOTE: for non-redundant clauses, pwatching its constraints is required for soundness.
for (sat::literal lit : clause)
enqueue_pwatch(lit2cnstr(lit).get());
}
void solver::add_clause(unsigned n, signed_constraint const* cs, bool is_redundant) {
clause_ref clause = mk_clause(n, cs, is_redundant);
if (clause)
add_clause(*clause);
}
void solver::add_clause(std::initializer_list<signed_constraint> cs, bool is_redundant) {
add_clause(static_cast<unsigned>(cs.size()), std::data(cs), is_redundant);
}
void solver::add_clause(signed_constraint c1, bool is_redundant) {
add_clause({ c1 }, is_redundant);
}
void solver::add_clause(signed_constraint c1, signed_constraint c2, bool is_redundant) {
add_clause({ c1, c2 }, is_redundant);
}
void solver::add_clause(signed_constraint c1, signed_constraint c2, signed_constraint c3, bool is_redundant) {
add_clause({ c1, c2, c3 }, is_redundant);
}
void solver::add_clause(signed_constraint c1, signed_constraint c2, signed_constraint c3, signed_constraint c4, bool is_redundant) {
add_clause({ c1, c2, c3, c4 }, is_redundant);
}
clause_ref solver::mk_clause(std::initializer_list<signed_constraint> cs, bool is_redundant) {
return mk_clause(static_cast<unsigned>(cs.size()), std::data(cs), is_redundant);
}
clause_ref solver::mk_clause(unsigned n, signed_constraint const* cs, bool is_redundant) {
clause_builder cb(*this);
for (unsigned i = 0; i < n; ++i)
cb.insert(cs[i]);
cb.set_redundant(is_redundant);
return cb.build();
}
clause_ref solver::mk_clause(signed_constraint c1, bool is_redundant) {
return mk_clause({ c1 }, is_redundant);
}
clause_ref solver::mk_clause(signed_constraint c1, signed_constraint c2, bool is_redundant) {
return mk_clause({ c1, c2 }, is_redundant);
}
clause_ref solver::mk_clause(signed_constraint c1, signed_constraint c2, signed_constraint c3, bool is_redundant) {
return mk_clause({ c1, c2, c3 }, is_redundant);
}
clause_ref solver::mk_clause(signed_constraint c1, signed_constraint c2, signed_constraint c3, signed_constraint c4, bool is_redundant) {
return mk_clause({ c1, c2, c3, c4 }, is_redundant);
}
clause_ref solver::mk_clause(signed_constraint c1, signed_constraint c2, signed_constraint c3, signed_constraint c4, signed_constraint c5, bool is_redundant) {
return mk_clause({ c1, c2, c3, c4, c5 }, is_redundant);
}
void solver::push() {
LOG_H3("Push user scope");
push_level();
m_base_levels.push_back(m_level);
}
void solver::pop(unsigned num_scopes) {
VERIFY(m_base_levels.size() >= num_scopes);
unsigned const base_level = m_base_levels[m_base_levels.size() - num_scopes];
LOG_H3("Pop " << num_scopes << " user scopes");
pop_levels(m_level - base_level + 1);
if (m_level < m_conflict.level())
m_conflict.reset();
m_base_levels.shrink(m_base_levels.size() - num_scopes);
}
bool solver::at_base_level() const {
return m_level == base_level();
}
unsigned solver::base_level() const {
return m_base_levels.empty() ? 0 : m_base_levels.back();
}
bool solver::try_eval(pdd const& p, rational& out_value) const {
pdd r = subst(p);
if (r.is_val())
out_value = r.val();
return r.is_val();
}
/**
* Variable activity accounting.
* As a placeholder we increment activity
* 1. when a variable assignment is used in a conflict.
* 2. when a variable propagation is resolved against.
* The hypothesis that this is useful should be tested against a
* broader suite of benchmarks and tested with micro-benchmarks.
* It should be tested in conjunction with restarts.
*/
void solver::inc_activity(pvar v) {
unsigned& act = m_activity[v];
act += m_activity_inc;
m_free_pvars.activity_increased_eh(v);
if (act > (1 << 24))
rescale_activity();
}
void solver::decay_activity() {
m_activity_inc *= m_variable_decay;
m_activity_inc /= 100;
}
void solver::rescale_activity() {
for (unsigned& act : m_activity) {
act >>= 14;
}
m_activity_inc >>= 14;
}
void solver::report_unsat() {
backjump(base_level());
VERIFY(!m_conflict.empty());
}
void solver::unsat_core(dependency_vector& deps) {
VERIFY(is_conflict());
deps.reset();
m_conflict.find_deps(deps);
IF_VERBOSE(10,
verbose_stream() << "polysat unsat_core";
for (auto d : deps)
verbose_stream() << " " << d;
verbose_stream() << "\n";
);
}
std::ostream& solver::display(std::ostream& out) const {
out << "Search Stack:\n";
for (auto item : m_search) {
if (item.is_assignment()) {
pvar v = item.var();
auto const& j = m_justification[v];
out << "\t" << assignment_pp(*this, v, get_value(v)) << " @" << j.level() << " ";
if (j.is_propagation())
for (auto const& c : m_viable.get_constraints(v))
out << c << " ";
out << "\n";
}
else {
sat::bool_var v = item.lit().var();
out << "\t" << lit_pp(*this, item.lit());
if (m_bvars.reason(v))
out << " reason " << *m_bvars.reason(v);
out << "\n";
}
}
out << "Constraints:\n";
for (auto c : m_constraints)
out << "\t" << c->bvar2string() << ": " << *c << "\n";
out << "Clauses:\n";
for (clause const& cl : m_constraints.clauses()) {
out << "\t" << cl << "\n";
for (sat::literal lit : cl)
out << "\t\t" << lit << ": " << lit2cnstr(lit) << "\n";
}
return out;
}
std::ostream& assignments_pp::display(std::ostream& out) const {
return out << s.get_assignment();
}
std::ostream& assignment_pp::display(std::ostream& out) const {
out << "v" << var << " := " << num_pp(s, var, val);
if (with_justification)
out << " (" << s.m_justification[var] << ")";
return out;
}
std::ostream& lit_pp::display(std::ostream& out) const {
signed_constraint const c = s.lit2cnstr(lit);
out << lpad(5, lit) << ": " << rpad(30, c);
if (!c)
return out;
out << " [ b:" << rpad(7, s.m_bvars.value(lit));
out << " p:" << rpad(7, c.eval(s));
if (s.m_bvars.is_assigned(lit)) {
out << ' ';
if (s.m_bvars.is_assumption(lit))
out << "assert";
else if (s.m_bvars.is_bool_propagation(lit))
out << "bprop";
else if (s.m_bvars.is_evaluation(lit))
out << "eval";
else if (s.m_bvars.is_decision(lit))
out << "decide";
out << '@' << s.m_bvars.level(lit);
}
if (c->is_pwatched())
out << " pwatched";
if (c->is_external())
out << " ext";
dependency const d = s.m_bvars.dep(lit);
if (!d.is_null())
out << " dep:" << d.val();
out << " ]";
return out;
}
std::ostream& num_pp::display(std::ostream& out) const {
return out << dd::val_pp(s.var2pdd(var), val, require_parens);
}
void solver::collect_statistics(statistics& st) const {
st.update("polysat iterations", m_stats.m_num_iterations);
st.update("polysat decisions", m_stats.m_num_decisions);
st.update("polysat conflicts", m_stats.m_num_conflicts);
st.update("polysat propagations", m_stats.m_num_propagations);
st.update("polysat restarts", m_stats.m_num_restarts);
st.update("polysat viable fallback", m_stats.m_num_viable_fallback);
}
bool solver::invariant() {
return true;
}
/**
* levels are gone
*/
bool solver::invariant(signed_constraints const& cs) {
return true;
}
/**
* Check that two variables of each constraint are watched.
*/
bool solver::wlist_invariant() const {
#if 0
for (pvar v = 0; v < m_value.size(); ++v) {
std::stringstream s;
for (constraint* c : m_pwatch[v])
s << " " << c->bvar();
LOG("Watch for v" << v << ": " << s.str());
}
#endif
// Skip boolean variables that aren't active yet
uint_set skip;
for (unsigned i = m_qhead; i < m_search.size(); ++i)
if (m_search[i].is_boolean())
skip.insert(m_search[i].lit().var());
SASSERT(is_conflict() || skip.empty()); // after propagation we either finished the queue or we are in a conflict
for (auto c : m_constraints) {
if (skip.contains(c->bvar()))
continue;
lbool value = m_bvars.value(c->bvar());
if (value == l_undef)
continue;
bool is_positive = value == l_true;
int64_t num_watches = 0;
signed_constraint sc(c, is_positive);
for (auto const& wlist : m_pwatch) {
auto n = count(wlist, c);
if (n > 1)
std::cout << sc << "\n" << * this << "\n";
VERIFY(n <= 1); // no duplicates in the watchlist
num_watches += n;
}
unsigned expected_watches = std::min(2u, c->vars().size());
if (num_watches != expected_watches)
LOG("Wrong number of watches: " << sc.blit() << ": " << sc << " (vars: " << sc->vars() << ")");
VERIFY_EQ(num_watches, expected_watches);
}
return true;
}
bool solver::bool_watch_invariant() const {
if (is_conflict()) // propagation may be unfinished if a conflict was discovered
return true;
// Check for missed boolean propagations:
// - no clause should have exactly one unassigned literal, unless it is already true.
// - no clause should be false
for (clause const& cl : m_constraints.clauses()) {
bool const is_true = any_of(cl, [&](auto lit) { return m_bvars.is_true(lit); });
if (is_true)
continue;
size_t const undefs = count_if(cl, [&](auto lit) { return !m_bvars.is_assigned(lit); });
if (undefs == 1) {
verbose_stream() << "Missed boolean propagation of clause: " << cl << "\n";
for (sat::literal lit : cl) {
verbose_stream() << " " << lit_pp(*this, lit);
if (count(m_bvars.watch(lit), &cl) != 0)
verbose_stream() << " (bool-watched)";
verbose_stream() << "\n";
}
}
VERIFY(undefs != 1);
bool const is_false = all_of(cl, [&](auto lit) { return m_bvars.is_false(lit); });
VERIFY(!is_false);
}
return true;
}
pdd solver::subst(pdd const& p) const {
return get_assignment().apply_to(p);
}
/** Check that boolean assignment and constraint evaluation are consistent */
bool solver::eval_invariant() const {
if (is_conflict())
return true;
bool ok = true;
for (sat::bool_var v = m_bvars.size(); v-- > 0; ) {
sat::literal lit(v);
auto c = lit2cnstr(lit);
if (!all_of(c->vars(), [this](auto w) { return is_assigned(w); }))
continue;
ok &= (m_bvars.value(lit) != l_true) || !c.is_currently_false(*this);
ok &= (m_bvars.value(lit) != l_false) || !c.is_currently_true(*this);
if (!ok) {
LOG("assignment invariant is broken " << v << "\n" << *this);
break;
}
}
return ok;
}
/** Check that each variable is either assigned or queued for decisions */
bool solver::var_queue_invariant() const {
if (is_conflict())
return true;
uint_set active;
bool ok = true;
for (pvar v : m_free_pvars)
active.insert(v);
for (auto const& [v, val] : get_assignment()) {
if (active.contains(v)) {
ok = false;
LOG("Variable v" << v << " is in free var queue despite already assigned " << assignment_pp(*this, v, val));
}
active.insert(v);
}
for (pvar v = 0; v < num_vars(); ++v) {
if (!active.contains(v)) {
ok = false;
LOG("Lost variable v" << v << " (it is neither assigned nor free)");
}
}
return ok;
}
/// Check that all constraints on the stack are satisfied by the current model.
bool solver::verify_sat() {
LOG_H1("Checking current model...");
LOG("Assignment: " << assignments_pp(*this));
bool all_ok = true;
for (auto s : m_search) {
if (s.is_boolean()) {
bool ok = lit2cnstr(s.lit()).is_currently_true(*this);
LOG((ok ? "PASS" : "FAIL") << ": " << s.lit());
all_ok = all_ok && ok;
}
}
for (clause const& cl : m_constraints.clauses()) {
bool clause_ok = false;
for (sat::literal lit : cl) {
bool ok = lit2cnstr(lit).is_currently_true(*this);
if (ok) {
clause_ok = true;
break;
}
}
LOG((clause_ok ? "PASS" : "FAIL") << ": " << cl << (cl.is_redundant() ? " (redundant)" : ""));
all_ok = all_ok && clause_ok;
}
if (all_ok) LOG("All good!");
return all_ok;
}
}