/*++ Copyright (c) 2021 Microsoft Corporation Module Name: polysat conflict Author: Nikolaj Bjorner (nbjorner) 2021-03-19 Jakob Rath 2021-04-06 Notes: TODO: - update notes/example above - question: if a side lemma justifies a constraint, then we resolve over one of the premises of the lemma; do we want to update the lemma or not? - conflict resolution plugins - may generate lemma - post-processing/simplification on lemma (e.g., literal subsumption; can be done in solver) - may force backjumping without further conflict resolution (e.g., if applicable lemma was found by global analysis of search state) - bailout lemma if no method applies (log these cases in particular because it indicates where we are missing something) - force a restart if we get a bailout lemma or non-asserting conflict? - Find a way to use resolve_value with forbidden interval lemmas. Maybe: x := a, y := b, z has no viable value - assume y was propagated - FI-Lemma C1 \/ ... \/ Cn without z. - for each i, we should have x := a /\ Ci ==> y != b - can we choose one of the Ci to cover the domain of y and extract an FI-Lemma D1 \/ ... \/ Dk without y,z? - or try to find an L(x,y) such that C1 -> L, ..., Cn -> L, and L -> y != b (under x := a); worst case y != b can work as L - fix minimize_vars: - it is not sound to do minimization for each constraint separately, if there are multiple constraints. - condition `c.is_currently_false(s, a)` is too weak (need also `c.bvalue(s) == l_true`?) - we are missing a stopping criterion like "first UIP" in SAT solving. Currently we always resolve backwards until the decision. --*/ #include "math/polysat/conflict.h" #include "math/polysat/clause_builder.h" #include "math/polysat/solver.h" #include "math/polysat/inference_logger.h" #include "math/polysat/log.h" #include "math/polysat/log_helper.h" #include "math/polysat/superposition.h" #include "math/polysat/eq_explain.h" #include "math/polysat/forbidden_intervals.h" #include "math/polysat/saturation.h" #include "math/polysat/variable_elimination.h" #include namespace polysat { class conflict_resolver { saturation m_saturation; ex_polynomial_superposition m_poly_sup; free_variable_elimination m_free_variable_elimination; public: conflict_resolver(solver& s) : m_saturation(s) , m_poly_sup(s) , m_free_variable_elimination(s) {} void infer_lemmas_for_value(pvar v, conflict& core) { (void)m_poly_sup.perform(v, core); m_saturation.perform(v, core); } void infer_lemmas_for_value(pvar v, signed_constraint const& c, conflict& core) { (void)m_saturation.perform(v, c, core); } // Analyse current conflict core to extract additional lemmas void find_extra_lemmas(conflict& core) { m_saturation.try_div_monotonicity(core); #if 1 // Don't do variable elimination for now #else m_free_variable_elimination.find_lemma(core); #endif } }; class header_with_var : public displayable { char const* m_text; pvar m_var; public: header_with_var(char const* text, pvar var) : m_text(text), m_var(var) { SASSERT(text); } std::ostream& display(std::ostream& out) const override { return out << m_text << m_var; } }; struct inf_resolve_bool : public inference { sat::literal m_lit; clause const& m_clause; inf_resolve_bool(sat::literal lit, clause const& cl) : m_lit(lit), m_clause(cl) {} std::ostream& display(std::ostream& out) const override { return out << "Resolve upon " << m_lit << " with " << m_clause; } }; struct inf_resolve_evaluated : public inference { solver& s; sat::literal lit; signed_constraint c; inf_resolve_evaluated(solver& s, sat::literal lit, signed_constraint c) : s(s), lit(lit), c(c) {} std::ostream& display(std::ostream& out) const override { out << "Resolve upon " << lit << " with assignment:"; for (pvar v : c->vars()) if (s.is_assigned(v)) out << " " << assignment_pp(s, v, s.get_value(v), true); return out; } }; struct inf_resolve_value : public inference { solver& s; pvar v; inf_resolve_value(solver& s, pvar v) : s(s), v(v) {} std::ostream& display(std::ostream& out) const override { return out << "Value resolution with " << assignment_pp(s, v, s.get_value(v), true); } }; conflict::conflict(solver& s) : s(s) { m_resolver = alloc(conflict_resolver, s); } conflict::~conflict() {} inference_logger& conflict::logger() { if (!m_logger) { if (s.config().m_log_conflicts) m_logger = alloc(file_inference_logger, s); else m_logger = alloc(dummy_inference_logger); } return *m_logger; } conflict::const_iterator conflict::begin() const { return conflict_iterator::begin(s.m_constraints, m_literals); } conflict::const_iterator conflict::end() const { return conflict_iterator::end(s.m_constraints, m_literals); } bool conflict::empty() const { bool const is_empty = (m_level == UINT_MAX); if (is_empty) { SASSERT(m_literals.empty()); SASSERT(m_vars.empty()); SASSERT(m_vars_occurring.empty()); SASSERT(m_lemmas.empty()); SASSERT(m_narrow_queue.empty()); SASSERT(m_dep.is_null()); } return is_empty; } void conflict::reset() { LOG("Reset conflict"); m_literals.reset(); m_vars.reset(); m_var_occurrences.reset(); m_vars_occurring.reset(); m_lemmas.reset(); m_narrow_queue.reset(); m_level = UINT_MAX; m_dep = null_dependency; SASSERT(empty()); } bool conflict::is_relevant_pvar(pvar v) const { return contains_pvar(v); } bool conflict::is_relevant(sat::literal lit) const { return contains(lit) || contains(~lit); } unsigned conflict::effective_level() const { SASSERT(!empty()); // If m_dep is set, the corresponding constraint was asserted at m_level and is not valid earlier. if (!m_dep.is_null()) return m_level; // In other cases, m_level just tracks the level at which the conflict appeared. // Find the minimal level at which the conflict is still valid. unsigned lvl = 0; for (unsigned lit_idx : m_literals) { sat::literal const lit = sat::to_literal(lit_idx); lvl = std::max(lvl, s.m_bvars.level(lit)); } for (pvar v : m_vars) lvl = std::max(lvl, s.get_level(v)); return lvl; } bool conflict::is_valid() const { SASSERT(!empty()); // If m_dep is set, the corresponding constraint was asserted at m_level and is not valid earlier. if (!m_dep.is_null() && m_level > s.m_level) return false; // All conflict constraints must be bool-assigned. for (unsigned lit_idx : m_literals) if (!s.m_bvars.is_assigned(sat::to_literal(lit_idx))) return false; // All conflict variables must be assigned. for (pvar v : m_vars) if (!s.is_assigned(v)) return false; return true; } void conflict::init_at_base_level(dependency dep) { SASSERT(empty()); SASSERT(s.at_base_level()); m_level = s.m_level; m_dep = dep; SASSERT(!empty()); // TODO: logger().begin_conflict??? // TODO: check uses of logger().begin_conflict(). sometimes we call it before adding constraints, sometimes after... logger().begin_conflict("base level"); } void conflict::init(dependency dep, signed_constraint c) { SASSERT(empty()); m_level = s.m_level; m_dep = dep; SASSERT(!empty()); insert_vars(c); logger().begin_conflict(); } void conflict::init(signed_constraint c) { LOG("Conflict: constraint " << lit_pp(s, c)); SASSERT(empty()); m_level = s.m_level; m_narrow_queue.push_back(c.blit()); // if the conflict is only due to a missed propagation of c VERIFY_EQ(c.bvalue(s), l_true); SASSERT(c.is_currently_false(s)); insert(c); insert_vars(c); SASSERT(!empty()); logger().begin_conflict(); } void conflict::init(clause& cl) { LOG("Conflict: clause " << cl); for (sat::literal lit : cl) LOG(" " << lit_pp(s, lit)); SASSERT(empty()); m_level = s.m_level; for (auto lit : cl) { auto c = s.lit2cnstr(lit); VERIFY_EQ(c.bvalue(s), l_false); insert(~c); } // NOTE: usually in SAT solving, the conflict clause has at least two false literals at the max level. // (otherwise, the last literal would have been propagated at an earlier level.) // This is not true if we add clauses on demand; // after backtracking we may have the case that the conflict clause has // exactly one undefined literal that must be propagated explicitly. m_lemmas.push_back(&cl); SASSERT(!empty()); logger().begin_conflict(cl.name()); } // void conflict::init_by_viable_interval(pvar v) { // LOG("Conflict: viable_interval v" << v); // SASSERT(empty()); // SASSERT(!s.is_assigned(v)); // m_level = s.m_level; // logger().begin_conflict(header_with_var("viable_interval v", v)); // if (s.m_viable.resolve_interval(v, *this)) { // revert_pvar(v); // at this point, v is not assigned // } // SASSERT(!empty()); // } void conflict::init_viable_begin(pvar v, bool by_intervals) { LOG("Conflict: viable_" << (by_intervals ? "interval" : "fallback") << " v" << v); SASSERT(empty()); SASSERT(!s.is_assigned(v)); m_level = s.m_level; logger().begin_conflict(header_with_var(by_intervals ? "viable_interval" : "viable_fallback v", v)); } void conflict::init_viable_end(pvar v) { SASSERT(!empty()); revert_pvar(v); // at this point, v is not assigned } bool conflict::contains(sat::literal lit) const { SASSERT(lit != sat::null_literal); return m_literals.contains(lit.index()); } void conflict::insert(signed_constraint c) { if (contains(c)) return; if (c.is_always_true()) // TODO: caller should avoid this? return; LOG("Inserting " << lit_pp(s, c)); SASSERT_EQ(c.bvalue(s), l_true); VERIFY_EQ(c.bvalue(s), l_true); SASSERT(!c.is_always_true()); // such constraints would be removed earlier SASSERT(!c.is_always_false()); // if we added c, the core would be a tautology SASSERT(!c->vars().empty()); m_literals.insert(c.blit().index()); for (pvar v : c->vars()) { if (v >= m_var_occurrences.size()) m_var_occurrences.resize(v + 1, 0); if (!m_var_occurrences[v]) m_vars_occurring.insert(v); m_var_occurrences[v]++; } } bool conflict::insert_or_replace(signed_constraint c) { // TODO: what if we have already passed c in the trail in resolve_conflict? should check that. (probably restart the resolve_conflict loop with the new conflict?) switch (c.bvalue(s)) { case l_true: // regular case insert(c); return false; case l_undef: VERIFY_EQ(c.eval(s), l_true); s.assign_eval(c.blit()); insert(c); return false; case l_false: VERIFY_EQ(c.eval(s), l_true); break; } // TODO: could keep lemmas? but whatever, this case seems very rare reset(); init(~c); return true; } void conflict::insert_vars(signed_constraint c) { for (pvar v : c->vars()) if (s.is_assigned(v)) m_vars.insert(v); } void conflict::add_lemma(char const* name, std::initializer_list cs) { add_lemma(name, std::data(cs), cs.size()); } void conflict::add_lemma(char const* name, signed_constraint const* cs, size_t cs_len) { clause_builder cb(s, name); for (size_t i = 0; i < cs_len; ++i) cb.insert_eval(cs[i]); add_lemma(cb.build()); } void conflict::add_lemma(clause_ref lemma) { LOG_H3("Lemma: " << ": " << show_deref(lemma)); VERIFY(lemma); IF_VERBOSE(1, { for (auto lit : *lemma) if (s.m_bvars.is_true(lit)) { verbose_stream() << "REDUNDANT lemma " << lit << " : " << show_deref(lemma) << "\n"; for (sat::literal lit : *lemma) verbose_stream() << " " << lit_pp(s, lit) << "\n"; } }); s.m_simplify_clause.apply(*lemma); lemma->set_redundant(true); for (sat::literal lit : *lemma) { LOG(lit_pp(s, lit)); // NOTE: it can happen that the literal's bvalue is l_true at this point. // E.g., lit has been assigned to true on the search stack but not yet propagated. // A propagation before lit will cause a conflict, and by chance the viable conflict will contain lit. // (in that case, the evaluation of lit in the current assignment must be false, and it would have caused a conflict by itself when propagated.) SASSERT(s.m_bvars.value(lit) != l_true || !s.lit2cnstr(lit).is_currently_true(s)); } m_lemmas.push_back(std::move(lemma)); // TODO: pass to inference_logger (with name) } void conflict::restore_lemma(clause_ref lemma) { SASSERT(!empty()); LOG_H3("Restore Lemma: " << ": " << show_deref(lemma)); m_lemmas.push_back(std::move(lemma)); } void conflict::remove(signed_constraint c) { SASSERT(contains(c)); m_literals.remove(c.blit().index()); for (pvar v : c->vars()) { m_var_occurrences[v]--; if (!m_var_occurrences[v]) m_vars_occurring.remove(v); } } void conflict::remove_all() { SASSERT(!empty()); m_literals.reset(); m_vars.reset(); m_var_occurrences.reset(); m_vars_occurring.reset(); } void conflict::resolve_bool(sat::literal lit, clause const& cl) { // Note: core: x, y, z; corresponds to clause ~x \/ ~y \/ ~z // clause: x \/ u \/ v // resolvent: ~y \/ ~z \/ u \/ v; as core: y, z, ~u, ~v SASSERT(contains(lit)); SASSERT(count(cl, lit) > 0); SASSERT(!contains(~lit)); SASSERT(count(cl, ~lit) == 0); remove(s.lit2cnstr(lit)); for (sat::literal other : cl) if (other != lit) insert(s.lit2cnstr(~other)); logger().log(inf_resolve_bool(lit, cl)); } void conflict::resolve_evaluated(sat::literal lit) { // The reason for lit is conceptually: // x1 = v1 /\ ... /\ xn = vn ==> lit SASSERT(s.m_bvars.is_evaluation(lit)); SASSERT(contains(lit)); SASSERT(!contains(~lit)); signed_constraint c = s.lit2cnstr(lit); unsigned const eval_idx = s.m_search.get_bool_index(lit); for (pvar v : c->vars()) { if (s.is_assigned(v) && s.m_search.get_pvar_index(v) <= eval_idx) { m_vars.insert(v); // TODO - figure out what to do with constraints from conflict lemma that disappear here. // if (s.m_bvars.is_false(lit)) // m_resolver->infer_lemmas_for_value(v, ~c, *this); } } remove(c); SASSERT(!contains(lit)); SASSERT(!contains(~lit)); logger().log(inf_resolve_evaluated(s, lit, c)); } void conflict::revert_pvar(pvar v) { m_resolver->infer_lemmas_for_value(v, *this); } void conflict::resolve_value_by_viable(pvar v) { SASSERT(contains_pvar(v)); SASSERT(s.m_justification[v].is_propagation_by_viable()); s.inc_activity(v); m_vars.remove(v); for (signed_constraint const c : s.m_viable.get_constraints(v)) if (insert_or_replace(c)) return; for (auto const& i : s.m_viable.units(v)) { if (insert_or_replace(s.eq(i.lo(), i.lo_val()))) return; if (insert_or_replace(s.eq(i.hi(), i.hi_val()))) return; } logger().log(inf_resolve_value(s, v)); revert_pvar(v); } void conflict::resolve_value_by_slicing(pvar v) { SASSERT(contains_pvar(v)); SASSERT(s.m_justification[v].is_propagation_by_slicing()); s.inc_activity(v); m_vars.remove(v); s.m_slicing.explain_value(v, [this](sat::literal lit) { insert_or_replace(s.lit2cnstr(lit)); }, [this](pvar w) { SASSERT(s.is_assigned(w)); m_vars.insert(w); }); logger().log("value was propagated by slicing"); // TODO: add information about v revert_pvar(v); } clause_ref conflict::build_lemma() { LOG_H3("Build lemma from core"); LOG("core: " << *this); // TODO: do this after each step? m_resolver->find_extra_lemmas(*this); clause_builder lemma(s); for (signed_constraint c : *this) lemma.insert(~c); for (unsigned v : m_vars) { auto eq = s.eq(s.var(v), s.get_value(v)); lemma.insert_eval(~eq); } s.decay_activity(); logger().log_lemma(lemma); logger().end_conflict(); return lemma.build(); } clause_ref_vector conflict::take_lemmas() { #ifndef NDEBUG on_scope_exit check_empty([this]() { SASSERT(m_lemmas.empty()); }); #endif return std::move(m_lemmas); } sat::literal_vector conflict::take_narrow_queue() { #ifndef NDEBUG on_scope_exit check_empty([this]() { SASSERT(m_narrow_queue.empty()); }); #endif return std::move(m_narrow_queue); } #if 0 // TODO: convert minimize_vars into a clause simplifier bool conflict::minimize_vars(signed_constraint c) { if (m_vars.empty()) return false; if (!c.is_currently_false(s)) return false; assignment_t a; for (auto v : m_vars) a.push_back(std::make_pair(v, s.get_value(v))); for (unsigned i = 0; i < a.size(); ++i) { std::pair save = a[i]; std::pair last = a.back(); a[i] = last; a.pop_back(); if (c.is_currently_false(s, a)) --i; else { a.push_back(last); a[i] = save; } } if (a.size() == m_vars.num_elems()) return false; m_vars.reset(); for (auto const& [v, val] : a) m_vars.insert(v); logger().log("minimize vars"); LOG("reduced " << m_vars); return true; } #endif void conflict::find_deps(dependency_vector& out_deps) const { sat::literal_vector todo_lits; sat::bool_var_set done_lits; unsigned_vector todo_vars; uint_set done_vars; indexed_uint_set deps; LOG_V(11, "State:\n" << s); LOG_V(11, "Conflict: " << *this); // LOG_V(11, "Viable:\n" << s.m_viable); // for (pvar v : m_vars) { // s.m_viable.log(v); // } SASSERT(s.at_base_level()); auto const enqueue_lit = [&](sat::literal lit) { if (done_lits.contains(lit.var())) return; if (!s.m_bvars.is_assigned(lit)) return; IF_VERBOSE(11, verbose_stream() << " enqueue " << lit_pp(s, lit) << "\n";); todo_lits.push_back(lit); done_lits.insert(lit.var()); }; auto const enqueue_constraint = [&](signed_constraint c) { enqueue_lit(c.blit()); }; auto const enqueue_var = [&](pvar v) { if (done_vars.contains(v)) return; IF_VERBOSE(11, { verbose_stream() << " enqueue v" << v << "\n"; if (signed_constraint c = s.m_constraints.find_op_by_result_var(v)) verbose_stream() << " defined by op_constraint: " << lit_pp(s, c) << "\n"; }); SASSERT(s.is_assigned(v)); todo_vars.push_back(v); done_vars.insert(v); }; // Starting at literals/variables in the conflict, chase propagations backwards and accumulate dependencies. for (signed_constraint c : *this) enqueue_constraint(c); for (pvar v : m_vars) enqueue_var(v); while (!todo_vars.empty() || !todo_lits.empty()) { while (!todo_vars.empty()) { pvar const v = todo_vars.back(); todo_vars.pop_back(); IF_VERBOSE(11, verbose_stream() << "Handling v" << v << "\n";); auto const& j = s.m_justification[v]; if (j.is_propagation_by_viable()) { for (signed_constraint c : s.m_viable.get_constraints(v)) enqueue_constraint(c); for (auto const& i : s.m_viable.units(v)) { enqueue_constraint(s.eq(i.lo(), i.lo_val())); enqueue_constraint(s.eq(i.hi(), i.hi_val())); } } else if (j.is_propagation_by_slicing()) { s.m_slicing.explain_value(v, enqueue_lit, enqueue_var); } else { // no decisions at base level UNREACHABLE(); } } while (!todo_lits.empty()) { sat::literal const lit = todo_lits.back(); todo_lits.pop_back(); IF_VERBOSE(11, verbose_stream() << "Handling " << lit_pp(s, lit) << "\n";); if (s.m_bvars.is_assumption(lit)) { // only assumptions have external dependencies dependency const d = s.m_bvars.dep(lit); if (!d.is_null()) deps.insert(d.val()); } else if (s.m_bvars.is_bool_propagation(lit)) { IF_VERBOSE(11, verbose_stream() << " reason " << *s.m_bvars.reason(lit) << "\n";); for (sat::literal other : *s.m_bvars.reason(lit)) enqueue_lit(other); } else if (s.m_bvars.is_evaluation(lit)) { unsigned const eval_idx = s.m_search.get_bool_index(lit); for (pvar v : s.lit2cnstr(lit).vars()) { if (!s.is_assigned(v)) continue; if (s.m_search.get_pvar_index(v) > eval_idx) continue; enqueue_var(v); } } else { // no decisions at base level UNREACHABLE(); } } } for (unsigned d : deps) out_deps.push_back(dependency(d)); if (!m_dep.is_null() && !deps.contains(m_dep.val())) out_deps.push_back(m_dep); } std::ostream& conflict::display(std::ostream& out) const { out << "lvl " << m_level; if (!m_dep.is_null()) out << " dep " << m_dep; char const* sep = " "; for (auto c : *this) out << sep << c->bvar2string() << " " << c, sep = " ; "; if (!m_vars.empty()) out << " vars"; for (auto v : m_vars) out << " v" << v; if (!m_lemmas.empty()) out << " lemmas"; for (clause const* lemma : m_lemmas) out << " " << show_deref(lemma); return out; } }