abc/src/sat/cadical/cadical_vivify.cpp

1905 lines
61 KiB
C++

#include "global.h"
#include "vivify.hpp"
#include "internal.hpp"
#include "util.hpp"
#include <algorithm>
#include <limits>
#include <utility>
ABC_NAMESPACE_IMPL_START
namespace CaDiCaL {
/*------------------------------------------------------------------------*/
// Vivification is a special case of asymmetric tautology elimination (ATE)
// and asymmetric literal elimination (ALE). It strengthens and removes
// clauses proven redundant through unit propagation.
//
// The original algorithm is due to a paper by Piette, Hamadi and Sais
// published at ECAI'08. We have an inprocessing version, e.g., it does not
// necessarily run-to-completion. Our version also performs conflict
// analysis and uses a new heuristic for selecting clauses to vivify.
// Our idea is to focus on clauses with many occurrences of its literals in
// other clauses first. This both complements nicely our implementation of
// subsume, which is bounded, e.g., subsumption attempts are skipped for
// very long clauses with literals with many occurrences and also is
// stronger in the sense that it enables to remove more clauses due to unit
// propagation (AT checks).
// While first focusing on irredundant clause we then added a separate phase
// upfront which focuses on strengthening also redundant clauses in spirit
// of the ideas presented in the IJCAI'17 paper by M. Luo, C.-M. Li, F.
// Xiao, F. Manya, and Z. Lu.
// There is another very similar approach called 'distilliation' published
// by Han and Somenzi in DAC'07, which reorganizes the CNF in a trie data
// structure to reuse decisions and propagations along the trie. We used
// that as an inspiration but instead of building a trie we simple sort
// clauses and literals in such a way that we get the same effect. If a new
// clause is 'distilled' or 'vivified' we first check how many of the
// decisions (which are only lazily undone) can be reused for that clause.
// Reusing can be improved by picking a global literal order and sorting the
// literals in all clauses with respect to that order. We favor literals
// with more occurrences first. Then we sort clauses lexicographically with
// respect to that literal order.
/*------------------------------------------------------------------------*/
// Candidate clause 'subsumed' is subsumed by 'subsuming'.
inline void Internal::vivify_subsume_clause (Clause *subsuming,
Clause *subsumed) {
CADICAL_assert (subsumed != subsuming);
stats.subsumed++;
stats.vivifysubs++;
#ifndef CADICAL_NDEBUG
CADICAL_assert (subsuming);
CADICAL_assert (subsumed);
CADICAL_assert (subsuming != subsumed);
CADICAL_assert (!subsumed->garbage);
// size after removeing units;
int real_size_subsuming = 0, real_size_subsumed = 0;
for (auto lit : *subsuming) {
if (!val (lit) || var (lit).level)
++real_size_subsuming;
else
CADICAL_assert (val (lit) < 0);
}
for (auto lit : *subsumed) {
if (!val (lit) || var (lit).level)
++real_size_subsumed;
else
CADICAL_assert (val (lit) < 0);
}
CADICAL_assert (real_size_subsuming <= real_size_subsumed);
#endif
LOG (subsumed, "subsumed to be deleted");
LOG (subsuming, "subsuming to be (un)deleted");
if (subsumed->redundant && subsuming->redundant &&
subsuming->glue < subsumed->glue) {
promote_clause (subsuming, subsumed->glue);
}
if (subsumed->redundant) {
stats.subred++;
++stats.vivifysubred;
} else {
stats.subirr++;
++stats.vivifysubirr;
}
if (subsuming->garbage) {
CADICAL_assert (subsuming->size == 2);
LOG (subsuming,
"binary subsuming clause was already deleted, so undeleting");
subsuming->garbage = false;
subsuming->glue = 1;
++stats.current.total;
if (subsuming->redundant)
stats.current.redundant++;
else
stats.current.irredundant++, stats.irrlits += subsuming->size;
}
if (subsumed->redundant || !subsuming->redundant) {
mark_garbage (subsumed);
return;
}
LOG ("turning redundant subsuming clause into irredundant clause");
subsuming->redundant = false;
if (proof)
proof->strengthen (subsuming->id);
mark_garbage (subsumed);
mark_added (subsuming);
stats.current.irredundant++;
stats.added.irredundant++;
stats.irrlits += subsuming->size;
CADICAL_assert (stats.current.redundant > 0);
stats.current.redundant--;
CADICAL_assert (stats.added.redundant > 0);
stats.added.redundant--;
// ... and keep 'stats.added.total'.
}
// demoting a clause (opposite is promote from subsume.cpp)
//
// This turned out to not be useful, but we kept it for the API call in the
// proof tracers.
inline void Internal::demote_clause (Clause *c) {
stats.subsumed++;
stats.vivifydemote++;
LOG (c, "demoting");
CADICAL_assert (!c->redundant);
mark_removed (c);
c->redundant = true;
CADICAL_assert (stats.current.irredundant > 0);
stats.current.irredundant--;
CADICAL_assert (stats.added.irredundant > 0);
stats.added.irredundant--;
stats.irrlits -= c->size;
stats.current.redundant++;
stats.added.redundant++;
c->glue = c->size - 1;
// ... and keep 'stats.added.total'.
}
/*------------------------------------------------------------------------*/
// For vivification we have a separate dedicated propagation routine, which
// prefers to propagate binary clauses first. It also uses its own
// assignment procedure 'vivify_assign', which does not mess with phase
// saving during search nor the conflict and other statistics and further
// can be inlined separately here. The propagation routine needs to ignore
// (large) clauses which are currently vivified.
inline void Internal::vivify_assign (int lit, Clause *reason) {
require_mode (VIVIFY);
const int idx = vidx (lit);
CADICAL_assert (!vals[idx]);
CADICAL_assert (!flags (idx).eliminated () || !reason);
Var &v = var (idx);
v.level = level; // required to reuse decisions
v.trail = (int) trail.size (); // used in 'vivify_better_watch'
CADICAL_assert ((int) num_assigned < max_var);
num_assigned++;
v.reason = level ? reason : 0; // for conflict analysis
if (!level)
learn_unit_clause (lit);
const signed char tmp = sign (lit);
vals[idx] = tmp;
vals[-idx] = -tmp;
CADICAL_assert (val (lit) > 0);
CADICAL_assert (val (-lit) < 0);
trail.push_back (lit);
LOG (reason, "vivify assign %d", lit);
}
// Assume negated literals in candidate clause.
void Internal::vivify_assume (int lit) {
require_mode (VIVIFY);
level++;
control.push_back (Level (lit, trail.size ()));
LOG ("vivify decide %d", lit);
CADICAL_assert (level > 0);
CADICAL_assert (propagated == trail.size ());
vivify_assign (lit, 0);
}
// Dedicated routine similar to 'propagate' in 'propagate.cpp' and
// 'probe_propagate' with 'probe_propagate2' in 'probe.cpp'. Please refer
// to that code for more explanation on how propagation is implemented.
bool Internal::vivify_propagate (int64_t &ticks) {
require_mode (VIVIFY);
CADICAL_assert (!unsat);
START (propagate);
int64_t before = propagated2 = propagated;
for (;;) {
if (propagated2 != trail.size ()) {
const int lit = -trail[propagated2++];
LOG ("vivify propagating %d over binary clauses", -lit);
Watches &ws = watches (lit);
ticks +=
1 + cache_lines (ws.size (), sizeof (const_watch_iterator *));
for (const auto &w : ws) {
if (!w.binary ())
continue;
const signed char b = val (w.blit);
if (b > 0)
continue;
if (b < 0)
conflict = w.clause; // but continue
else {
ticks++;
build_chain_for_units (w.blit, w.clause, 0);
vivify_assign (w.blit, w.clause);
lrat_chain.clear ();
}
}
} else if (!conflict && propagated != trail.size ()) {
const int lit = -trail[propagated++];
LOG ("vivify propagating %d over large clauses", -lit);
Watches &ws = watches (lit);
const const_watch_iterator eow = ws.end ();
const_watch_iterator i = ws.begin ();
ticks += 1 + cache_lines (ws.size (), sizeof (*i));
watch_iterator j = ws.begin ();
while (i != eow) {
const Watch w = *j++ = *i++;
if (w.binary ())
continue;
if (val (w.blit) > 0)
continue;
ticks++;
if (w.clause->garbage) {
j--;
continue;
}
literal_iterator lits = w.clause->begin ();
const int other = lits[0] ^ lits[1] ^ lit;
const signed char u = val (other);
if (u > 0)
j[-1].blit = other;
else {
const int size = w.clause->size;
const const_literal_iterator end = lits + size;
const literal_iterator middle = lits + w.clause->pos;
literal_iterator k = middle;
signed char v = -1;
int r = 0;
while (k != end && (v = val (r = *k)) < 0)
k++;
if (v < 0) {
k = lits + 2;
CADICAL_assert (w.clause->pos <= size);
while (k != middle && (v = val (r = *k)) < 0)
k++;
}
w.clause->pos = k - lits;
CADICAL_assert (lits + 2 <= k), CADICAL_assert (k <= w.clause->end ());
if (v > 0)
j[-1].blit = r;
else if (!v) {
LOG (w.clause, "unwatch %d in", r);
lits[0] = other;
lits[1] = r;
*k = lit;
ticks++;
watch_literal (r, lit, w.clause);
j--;
} else if (!u) {
if (w.clause == ignore) {
LOG ("ignoring propagation due to clause to vivify");
continue;
}
ticks++;
CADICAL_assert (v < 0);
vivify_chain_for_units (other, w.clause);
vivify_assign (other, w.clause);
lrat_chain.clear ();
} else {
if (w.clause == ignore) {
LOG ("ignoring conflict due to clause to vivify");
continue;
}
CADICAL_assert (u < 0);
CADICAL_assert (v < 0);
conflict = w.clause;
break;
}
}
}
if (j != i) {
while (i != eow)
*j++ = *i++;
ws.resize (j - ws.begin ());
}
} else
break;
}
int64_t delta = propagated2 - before;
stats.propagations.vivify += delta;
if (conflict)
LOG (conflict, "conflict");
STOP (propagate);
return !conflict;
}
/*------------------------------------------------------------------------*/
// Check whether a literal occurs less often. In the implementation below
// (search for 'int64_t score = ...' or '@4') we actually compute a
// weighted occurrence count similar to the Jeroslow Wang heuristic.
struct vivify_more_noccs {
Internal *internal;
vivify_more_noccs (Internal *i) : internal (i) {}
bool operator() (int a, int b) {
int64_t n = internal->noccs (a);
int64_t m = internal->noccs (b);
if (n > m)
return true; // larger occurrences / score first
if (n < m)
return false; // smaller occurrences / score last
if (a == -b)
return a > 0; // positive literal first
return abs (a) < abs (b); // smaller index first
}
};
struct vivify_more_noccs_kissat {
Internal *internal;
vivify_more_noccs_kissat (Internal *i) : internal (i) {}
bool operator() (int a, int b) {
unsigned s = internal->noccs (a);
unsigned t = internal->noccs (b);
return (((t - s) |
((internal->vlit (a) - internal->vlit (b)) & ~(s - t))) >>
31);
}
};
/*------------------------------------------------------------------------*/
// Attempting on-the-fly subsumption during sorting when the last line
// is turned out to be trouble some for identical clauses. This is
// the single point where sorting is not asymmetric and would require
// 'stable' sorting for determinism. It can also not be made
// 'complete' on-the-fly. Instead of on-the-fly subsumption, we
// (optionnaly) thus go over the sorted scheduled in a linear scan
// again and remove certain subsumed clauses (the subsuming clause is
// syntactically a prefix of the subsumed clause), which includes
// those troublesome syntactically identical clauses.
struct vivify_flush_smaller {
bool operator() (Clause *a, Clause *b) const {
const auto eoa = a->end (), eob = b->end ();
auto i = a->begin (), j = b->begin ();
for (; i != eoa && j != eob; i++, j++)
if (*i != *j)
return *i < *j;
const bool smaller = j == eob && i != eoa;
return smaller;
}
};
void Internal::flush_vivification_schedule (std::vector<Clause *> &schedule,
int64_t &ticks) {
ticks += 1 + 3 * cache_lines (schedule.size (), sizeof (Clause *));
// we cannot use msort here, as we cannot calculate a score
stable_sort (schedule.begin (), schedule.end (), vivify_flush_smaller ());
const auto end = schedule.end ();
auto j = schedule.begin (), i = j;
Clause *prev = 0;
int64_t subsumed = 0;
for (; i != end; i++) {
ticks++;
Clause *c = *j++ = *i;
if (!prev || c->size < prev->size) {
prev = c;
continue;
}
const auto eop = prev->end ();
auto k = prev->begin ();
for (auto l = c->begin (); k != eop; k++, l++)
if (*k != *l)
break;
if (k == eop) {
LOG (c, "found subsumed");
LOG (prev, "subsuming");
CADICAL_assert (!c->garbage);
CADICAL_assert (!prev->garbage);
CADICAL_assert (c->redundant || !prev->redundant);
mark_garbage (c);
subsumed++;
j--;
} else
prev = c;
}
if (subsumed)
PHASE ("vivify", stats.vivifications,
"flushed %" PRId64 " subsumed scheduled clauses", subsumed);
stats.vivifysubs += subsumed;
stats.vivifyflushed += subsumed;
if (subsumed) {
schedule.resize (j - schedule.begin ());
shrink_vector (schedule);
} else
CADICAL_assert (j == end);
}
/*------------------------------------------------------------------------*/
// Depending on whether we try to vivify redundant or irredundant clauses,
// we schedule a clause to be vivified. For redundant clauses we initially
// only try to vivify them if they are likely to survive the next 'reduce'
// operation, but this left the last schedule empty most of the time.
bool Internal::consider_to_vivify_clause (Clause *c) {
if (c->garbage)
return false;
if (opts.vivifyonce >= 1 && c->redundant && c->vivified)
return false;
if (opts.vivifyonce >= 2 && !c->redundant && c->vivified)
return false;
if (!c->redundant)
return true;
CADICAL_assert (c->redundant);
// likely_to_be_kept_clause is too aggressive at removing tier-3 clauses
return true;
}
/*------------------------------------------------------------------------*/
// In a strengthened clause the idea is to move non-false literals to the
// front, followed by false literals. Literals are further sorted by
// reverse assignment order. The goal is to use watches which require to
// backtrack as few as possible decision levels.
struct vivify_better_watch {
Internal *internal;
vivify_better_watch (Internal *i) : internal (i) {}
bool operator() (int a, int b) {
const signed char av = internal->val (a), bv = internal->val (b);
if (av >= 0 && bv < 0)
return true;
if (av < 0 && bv >= 0)
return false;
return internal->var (a).trail > internal->var (b).trail;
}
};
// Common code to actually strengthen a candidate clause. The resulting
// strengthened clause is communicated through the global 'clause'.
void Internal::vivify_strengthen (Clause *c, int64_t &ticks) {
CADICAL_assert (!clause.empty ());
if (clause.size () == 1) {
backtrack_without_updating_phases ();
const int unit = clause[0];
LOG (c, "vivification shrunken to unit %d", unit);
CADICAL_assert (!val (unit));
assign_unit (unit);
// lrat_chain.clear (); done in search_assign
stats.vivifyunits++;
bool ok = vivify_propagate (ticks);
if (!ok)
learn_empty_clause ();
} else {
// See explanation before 'vivify_better_watch' above.
//
sort (clause.begin (), clause.end (), vivify_better_watch (this));
int new_level = level;
const int lit0 = clause[0];
signed char val0 = val (lit0);
if (val0 < 0) {
const int level0 = var (lit0).level;
LOG ("1st watch %d negative at level %d", lit0, level0);
new_level = level0 - 1;
}
const int lit1 = clause[1];
const signed char val1 = val (lit1);
if (val1 < 0 && !(val0 > 0 && var (lit0).level <= var (lit1).level)) {
const int level1 = var (lit1).level;
LOG ("2nd watch %d negative at level %d", lit1, level1);
new_level = level1 - 1;
}
CADICAL_assert (new_level >= 0);
if (new_level < level)
backtrack (new_level);
CADICAL_assert (val (lit0) >= 0);
CADICAL_assert (val (lit1) >= 0 || (val (lit0) > 0 && val (lit1) < 0 &&
var (lit0).level <= var (lit1).level));
Clause *d = new_clause_as (c);
LOG (c, "before vivification");
LOG (d, "after vivification");
(void) d;
}
clause.clear ();
mark_garbage (c);
lrat_chain.clear ();
++stats.vivifystrs;
}
// When shortening clauses, we have to update the watched literals.
// Actually we don't do it, and instead simply backtrack back enough.
void Internal::vivify_sort_watched (Clause *c) {
sort (c->begin (), c->end (), vivify_better_watch (this));
int new_level = level;
const int lit0 = c->literals[0];
signed char val0 = val (lit0);
if (val0 < 0) {
const int level0 = var (lit0).level;
LOG ("1st watch %d negative at level %d", lit0, level0);
new_level = level0 - 1;
}
const int lit1 = c->literals[1];
const signed char val1 = val (lit1);
if (val1 < 0 && !(val0 > 0 && var (lit0).level <= var (lit1).level)) {
const int level1 = var (lit1).level;
LOG ("2nd watch %d negative at level %d", lit1, level1);
new_level = level1 - 1;
}
CADICAL_assert (new_level >= 0);
if (new_level < level)
backtrack_without_updating_phases (new_level);
CADICAL_assert (val (lit0) >= 0);
CADICAL_assert (val (lit1) >= 0 || (val (lit0) > 0 && val (lit1) < 0 &&
var (lit0).level <= var (lit1).level));
}
/*------------------------------------------------------------------------*/
// Conflict analysis from 'start' which learns a decision only clause.
//
// We cannot use the stack-based implementation of Kissat, because we need
// to iterate over the conflict in topological ordering to produce a valid
// LRAT proof
void Internal::vivify_analyze (Clause *start, bool &subsumes,
Clause **subsuming,
const Clause *const candidate, int implied,
bool &redundant) {
const auto &t = &trail; // normal trail, so next_trail is wrong
int i = t->size (); // Start at end-of-trail.
Clause *reason = start;
CADICAL_assert (reason);
CADICAL_assert (!trail.empty ());
int uip = trail.back ();
bool mark_implied = (implied);
while (i >= 0) {
if (reason) {
redundant = (redundant || reason->redundant);
subsumes = (start != reason && reason->size <= start->size);
LOG (reason, "resolving on %d with", uip);
for (auto other : *reason) {
const Var v = var (other);
Flags &f = flags (other);
if (!marked2 (other) && v.level) {
LOG ("not subsuming due to lit %d", other);
subsumes = false;
}
if (!val (other)) {
LOG ("skipping unset lit %d", other);
continue;
}
if (other == uip) {
continue;
}
if (!v.level) {
if (f.seen || !lrat || reason == start)
continue;
LOG ("unit reason for %d", other);
int64_t id = unit_id (-other);
LOG ("adding unit reason %" PRId64 " for %s", id, LOGLIT (other));
unit_chain.push_back (id);
f.seen = true;
analyzed.push_back (other);
continue;
}
if (mark_implied && other != implied) {
LOG ("skipping non-implied literal %d on current level", other);
continue;
}
CADICAL_assert (val (other));
if (f.seen)
continue;
LOG ("pushing lit %d", other);
analyzed.push_back (other);
f.seen = true;
}
if (reason != start && reason->redundant) {
const int new_glue = recompute_glue (reason);
promote_clause (reason, new_glue);
}
if (subsumes) {
CADICAL_assert (reason);
LOG (reason, "clause found subsuming");
LOG (candidate, "clause found subsumed");
*subsuming = reason;
return;
}
} else {
LOG ("vivify analyzed decision %d", uip);
clause.push_back (-uip);
}
mark_implied = false;
uip = 0;
while (!uip && i > 0) {
CADICAL_assert (i > 0);
const int lit = (*t)[--i];
if (!var (lit).level)
continue;
if (flags (lit).seen)
uip = lit;
}
if (!uip)
break;
LOG ("uip is %d", uip);
Var &w = var (uip);
reason = w.reason;
if (lrat && reason)
lrat_chain.push_back (reason->id);
}
(void) candidate;
}
/*------------------------------------------------------------------------*/
// First decide which clause (candidate or conflict) to analyze and
// how to do it. We also prepare the clause by removing units.
void Internal::vivify_deduce (Clause *candidate, Clause *conflict,
int implied, Clause **subsuming,
bool &redundant) {
CADICAL_assert (lrat_chain.empty ());
bool subsumes;
Clause *reason;
CADICAL_assert (clause.empty ());
if (implied) {
reason = candidate;
mark2 (candidate);
const int not_implied = -implied;
CADICAL_assert (var (not_implied).level);
Flags &f = flags (not_implied);
f.seen = true;
LOG ("pushing implied lit %d", not_implied);
analyzed.push_back (not_implied);
clause.push_back (implied);
} else {
reason = (conflict ? conflict : candidate);
CADICAL_assert (reason);
CADICAL_assert (!reason->garbage || reason->size == 2);
mark2 (candidate);
subsumes = (candidate != reason);
redundant = reason->redundant;
LOG (reason, "resolving with");
if (lrat)
lrat_chain.push_back (reason->id);
for (auto lit : *reason) {
const Var &v = var (lit);
Flags &f = flags (lit);
CADICAL_assert (val (lit) < 0);
if (!v.level) {
if (!lrat)
continue;
LOG ("adding unit %d", lit);
if (!f.seen) {
// nevertheless we can use var (l) as if l was still assigned
// because var is updated lazily
int64_t id = unit_id (-lit);
LOG ("adding unit reason %" PRId64 " for %s", id, LOGLIT (lit));
unit_chain.push_back (id);
}
f.seen = true;
analyzed.push_back (lit);
continue;
}
CADICAL_assert (v.level);
if (!marked2 (lit)) {
LOG ("lit %d is not marked", lit);
subsumes = false;
}
LOG ("analyzing lit %d", lit);
LOG ("pushing lit %d", lit);
analyzed.push_back (lit);
f.seen = true;
}
if (reason != candidate && reason->redundant) {
const int new_glue = recompute_glue (reason);
promote_clause (reason, new_glue);
}
if (subsumes) {
CADICAL_assert (candidate != reason);
#ifndef CADICAL_NDEBUG
int nonfalse_reason = 0;
for (auto lit : *reason)
if (!fixed (lit))
++nonfalse_reason;
int nonfalse_candidate = 0;
for (auto lit : *candidate)
if (!fixed (lit))
++nonfalse_candidate;
CADICAL_assert (nonfalse_reason <= nonfalse_candidate);
#endif
LOG (candidate, "vivify subsumed 0");
LOG (reason, "vivify subsuming 0");
*subsuming = reason;
unmark (candidate);
if (lrat)
lrat_chain.clear ();
return;
}
}
vivify_analyze (reason, subsumes, subsuming, candidate, implied,
redundant);
unmark (candidate);
if (subsumes) {
CADICAL_assert (*subsuming);
LOG (candidate, "vivify subsumed");
LOG (*subsuming, "vivify subsuming");
if (lrat)
lrat_chain.clear ();
}
}
/*------------------------------------------------------------------------*/
// checks whether the clause can be strengthen or not.
bool Internal::vivify_shrinkable (const std::vector<int> &sorted,
Clause *conflict) {
unsigned count_implied = 0;
for (auto lit : sorted) {
const signed char value = val (lit);
if (!value) {
LOG ("vivification unassigned %d", lit);
return true;
}
if (value > 0) {
LOG ("vivification implied satisfied %d", lit);
if (conflict)
return true;
if (count_implied++) {
LOG ("at least one implied literal with conflict thus shrinking");
return true;
}
} else {
CADICAL_assert (value < 0);
const Var &v = var (lit);
const Flags &f = flags (lit);
if (!v.level)
continue;
if (!f.seen) {
LOG ("vivification non-analyzed %d", lit);
return true;
}
if (v.reason) {
LOG ("vivification implied falsified %d", lit);
return true;
}
}
}
return false;
}
/*------------------------------------------------------------------------*/
inline void Internal::vivify_increment_stats (const Vivifier &vivifier) {
switch (vivifier.tier) {
case Vivify_Mode::TIER1:
++stats.vivifystred1;
break;
case Vivify_Mode::TIER2:
++stats.vivifystred2;
break;
case Vivify_Mode::TIER3:
++stats.vivifystred3;
break;
default:
CADICAL_assert (vivifier.tier == Vivify_Mode::IRREDUNDANT);
++stats.vivifystrirr;
break;
}
}
/*------------------------------------------------------------------------*/
// instantiate last literal (see the description of the hack track
// 2023), fix the watches and backtrack two level back to make sure
// the watches are correct.
bool Internal::vivify_instantiate (
const std::vector<int> &sorted, Clause *c,
std::vector<std::tuple<int, Clause *, bool>> &lrat_stack,
int64_t &ticks) {
LOG ("now trying instantiation");
conflict = nullptr;
const int lit = sorted.back ();
LOG ("vivify instantiation");
CADICAL_assert (!var (lit).reason);
CADICAL_assert (var (lit).level);
CADICAL_assert (val (lit));
backtrack_without_updating_phases (level - 1);
CADICAL_assert (val (lit) == 0);
stats.vivifydecs++;
vivify_assume (lit);
bool ok = vivify_propagate (ticks);
if (!ok) {
LOG (c, "instantiate success with literal %d in", lit);
stats.vivifyinst++;
// strengthen clause
if (lrat) {
clear_analyzed_literals ();
CADICAL_assert (lrat_chain.empty ());
vivify_build_lrat (0, c, lrat_stack);
vivify_build_lrat (0, conflict, lrat_stack);
clear_analyzed_literals ();
}
int remove = lit;
conflict = nullptr;
unwatch_clause (c);
backtrack_without_updating_phases (level - 2);
strengthen_clause (c, remove);
vivify_sort_watched (c);
watch_clause (c);
CADICAL_assert (!conflict);
return true;
} else {
LOG ("vivify instantiation failed");
return false;
}
}
/*------------------------------------------------------------------------*/
// Main function: try to vivify this candidate clause in the given mode.
bool Internal::vivify_clause (Vivifier &vivifier, Clause *c) {
CADICAL_assert (c->size > 2); // see (NO-BINARY) below
CADICAL_assert (analyzed.empty ());
CADICAL_assert (!ignore);
c->vivify = false; // mark as checked / tried
c->vivified = true; // and globally remember
CADICAL_assert (!c->garbage);
auto &lrat_stack = vivifier.lrat_stack;
auto &ticks = vivifier.ticks;
ticks++;
// First check whether the candidate clause is already satisfied and at
// the same time copy its non fixed literals to 'sorted'. The literals
// in the candidate clause might not be sorted anymore due to replacing
// watches during propagation, even though we sorted them initially
// while pushing the clause onto the schedule and sorting the schedule.
//
auto &sorted = vivifier.sorted;
sorted.clear ();
for (const auto &lit : *c) {
const int tmp = fixed (lit);
if (tmp > 0) {
LOG (c, "satisfied by propagated unit %d", lit);
mark_garbage (c);
return false;
} else if (!tmp)
sorted.push_back (lit);
}
CADICAL_assert (sorted.size () > 1);
if (sorted.size () == 2) {
LOG ("skipping actual binary");
return false;
}
sort (sorted.begin (), sorted.end (), vivify_more_noccs_kissat (this));
CADICAL_assert (std::is_sorted (sorted.begin (), sorted.end (),
vivify_more_noccs (this)));
// The actual vivification checking is performed here, by assuming the
// negation of each of the remaining literals of the clause in turn and
// propagating it. If a conflict occurs or another literal in the
// clause becomes assigned during propagation, we can stop.
//
LOG (c, "vivification checking");
stats.vivifychecks++;
// If the decision 'level' is non-zero, then we can reuse decisions for
// the previous candidate, and avoid re-propagating them. In preliminary
// experiments this saved between 30%-50% decisions (and thus
// propagations), which in turn lets us also vivify more clauses within
// the same propagation bounds, or terminate earlier if vivify runs to
// completion.
//
if (level) {
#ifdef LOGGING
int orig_level = level;
#endif
// First check whether this clause is actually a reason for forcing
// one of its literals to true and then backtrack one level before
// that happened. Otherwise this clause might be incorrectly
// considered to be redundant or if this situation is checked then
// redundancy by other clauses using this forced literal becomes
// impossible.
//
int forced = 0;
// This search could be avoided if we would eagerly set the 'reason'
// boolean flag of clauses, which however we do not want to do for
// binary clauses (during propagation) and thus would still require
// a version of 'protect_reason' for binary clauses during 'reduce'
// (well binary clauses are not collected during 'reduce', but again
// this exception from the exception is pretty complex and thus a
// simply search here is probably easier to understand).
for (const auto &lit : *c) {
const signed char tmp = val (lit);
if (tmp < 0)
continue;
if (tmp > 0 && var (lit).reason == c)
forced = lit;
break;
}
if (forced) {
LOG ("clause is reason forcing %d", forced);
CADICAL_assert (var (forced).level);
backtrack_without_updating_phases (var (forced).level - 1);
}
// As long the (remaining) literals of the sorted clause match
// decisions on the trail we just reuse them.
//
if (level) {
int l = 1; // This is the decision level we want to reuse.
for (const auto &lit : sorted) {
CADICAL_assert (!fixed (lit));
const int decision = control[l].decision;
if (-lit == decision) {
LOG ("reusing decision %d at decision level %d", decision, l);
++stats.vivifyreused;
if (++l > level)
break;
} else {
LOG ("literal %d does not match decision %d at decision level %d",
lit, decision, l);
backtrack_without_updating_phases (l - 1);
break;
}
}
}
LOG ("reused %d decision levels from %d", level, orig_level);
}
LOG (sorted, "sorted size %zd probing schedule", sorted.size ());
// Make sure to ignore this clause during propagation. This is not that
// easy for binary clauses (NO-BINARY), e.g., ignoring binary clauses,
// without changing 'propagate'. Actually, we do not want to remove binary
// clauses which are subsumed. Those are hyper binary resolvents and
// should be kept as learned clauses instead, unless they are transitive
// in the binary implication graph, which in turn is detected during
// transitive reduction in 'transred'.
//
ignore = c;
int subsume = 0; // determined to be redundant / subsumed
// If the candidate is redundant, i.e., we are in redundant mode, the
// clause is subsumed (in one of the two cases below where 'subsume' is
// assigned) and further all reasons involved are only binary clauses,
// then this redundant clause is what we once called a hidden tautology,
// and even for redundant clauses it makes sense to remove the candidate.
// It does not add anything to propagation power of the formula. This is
// the same argument as removing transitive clauses in the binary
// implication graph during transitive reduction.
//
// Go over the literals in the candidate clause in sorted order.
//
for (const auto &lit : sorted) {
// Exit loop as soon a literal is positively implied (case '@5' below)
// or propagation of the negation of a literal fails ('@6').
//
if (subsume)
break;
// We keep on assigning literals, even though we know already that we
// can remove one (was negatively implied), since we either might run
// into the 'subsume' case above or more false literals become implied.
// In any case this might result in stronger vivified clauses. As a
// consequence continue with this loop even if 'remove' is non-zero.
const signed char tmp = val (lit);
if (tmp) { // literal already assigned
const Var &v = var (lit);
CADICAL_assert (v.level);
if (!v.reason) {
LOG ("skipping decision %d", lit);
continue;
}
if (tmp < 0) {
CADICAL_assert (v.level);
LOG ("literal %d is already false and can be removed", lit);
continue;
}
CADICAL_assert (tmp > 0);
LOG ("subsumed since literal %d already true", lit);
subsume = lit; // will be able to subsume candidate '@5'
break;
}
CADICAL_assert (!tmp);
stats.vivifydecs++;
vivify_assume (-lit);
LOG ("negated decision %d score %" PRId64 "", lit, noccs (lit));
if (!vivify_propagate (ticks)) {
break; // hot-spot
}
}
if (subsume) {
int better_subsume_trail = var (subsume).trail;
for (auto lit : sorted) {
if (val (lit) <= 0)
continue;
const Var v = var (lit);
if (v.trail < better_subsume_trail) {
LOG ("improving subsume from %d at %d to %d at %d", subsume,
better_subsume_trail, lit, v.trail);
better_subsume_trail = v.trail;
subsume = lit;
}
}
}
Clause *subsuming = nullptr;
bool redundant = false;
const int level_after_assumptions = level;
CADICAL_assert (level_after_assumptions);
vivify_deduce (c, conflict, subsume, &subsuming, redundant);
bool res;
// reverse lrat_chain. We could probably work with reversed iterators
// (views) to be more efficient but we would have to distinguish in proof
//
if (lrat) {
for (auto id : unit_chain)
lrat_chain.push_back (id);
unit_chain.clear ();
reverse (lrat_chain.begin (), lrat_chain.end ());
}
// For an explanation of the code, see our POS'25 paper ``Revisiting
// Clause Vivification'' (although for the experiments we focused on
// Kissat instead of CaDiCaL)
if (subsuming) {
CADICAL_assert (c != subsuming);
vivify_subsume_clause (subsuming, c);
res = false;
} else if (vivify_shrinkable (sorted, conflict)) {
vivify_increment_stats (vivifier);
LOG ("vivify succeeded, learning new clause");
clear_analyzed_literals ();
LOG (lrat_chain, "lrat");
LOG (clause, "learning clause");
conflict = nullptr; // TODO dup from below
vivify_strengthen (c, ticks);
res = true;
} else if (subsume && c->redundant) {
LOG (c, "vivification implied");
mark_garbage (c);
++stats.vivifyimplied;
res = true;
} else if ((conflict || subsume) && !c->redundant && !redundant) {
if (opts.vivifydemote) {
LOG ("demote clause from irredundant to redundant");
demote_clause (c);
const int new_glue = recompute_glue (c);
promote_clause (c, new_glue);
res = false;
} else {
mark_garbage (c);
++stats.vivifyimplied;
res = true;
}
} else if (subsume) {
LOG (c, "no vivification instantiation with implied literal %d",
subsume);
CADICAL_assert (!c->redundant);
CADICAL_assert (redundant);
res = false;
++stats.vivifyimplied;
} else {
CADICAL_assert (level > 2);
CADICAL_assert ((size_t) level == sorted.size ());
LOG (c, "vivification failed on");
lrat_chain.clear ();
CADICAL_assert (!subsume);
if (!subsume && opts.vivifyinst) {
res = vivify_instantiate (sorted, c, lrat_stack, ticks);
CADICAL_assert (!conflict);
} else {
LOG ("cannot apply instantiation");
res = false;
}
}
if (conflict && level == level_after_assumptions) {
LOG ("forcing backtracking at least one level after conflict");
backtrack_without_updating_phases (level - 1);
}
clause.clear ();
clear_analyzed_literals (); // TODO why needed?
lrat_chain.clear ();
conflict = nullptr;
ignore = nullptr;
if (res) {
switch (vivifier.tier) {
case Vivify_Mode::IRREDUNDANT:
++stats.vivifiedirred;
break;
case Vivify_Mode::TIER1:
++stats.vivifiedtier1;
break;
case Vivify_Mode::TIER2:
++stats.vivifiedtier2;
break;
default:
CADICAL_assert (vivifier.tier == Vivify_Mode::TIER3);
++stats.vivifiedtier3;
break;
}
}
return res;
}
// when we can strengthen clause c we have to build lrat.
// uses f.seen so do not forget to clear flags afterwards.
// this can happen in three cases. (1), (2) are only sound in redundant mode
// (1) literal l in c is positively implied. in this case we call the
// function with (l, l.reason). This justifies the reduction because the new
// clause c' will include l and all decisions so l.reason is a conflict
// assuming -c' (2) conflict during vivify propagation. function is called
// with (0, conflict) similar to (1) but more direct. (3) some literals in c
// are negatively implied and can therefore be removed. in this case we call
// the function with (0, c). originally we justified each literal in c on
// its own but this is not actually necessary.
//
// Non-recursive version, as some bugs have been found by Dominik Schreiber
// during his very large experiments. DFS over the reasons with preordering
// (aka we explore the entire reason before exploring deeper)
void Internal::vivify_build_lrat (
int lit, Clause *reason,
std::vector<std::tuple<int, Clause *, bool>> &stack) {
CADICAL_assert (stack.empty ());
stack.push_back ({lit, reason, false});
while (!stack.empty ()) {
int lit;
Clause *reason;
bool finished;
std::tie (lit, reason, finished) = stack.back ();
LOG ("VIVIFY LRAT justifying %d", lit);
stack.pop_back ();
if (lit && flags (lit).seen) {
LOG ("skipping already justified");
continue;
}
if (finished) {
lrat_chain.push_back (reason->id);
if (lit && reason) {
Flags &f = flags (lit);
f.seen = true;
analyzed.push_back (lit); // CADICAL_assert (val (other) < 0);
CADICAL_assert (flags (lit).seen);
}
continue;
} else
stack.push_back ({lit, reason, true});
for (const auto &other : *reason) {
if (other == lit)
continue;
Var &v = var (other);
Flags &f = flags (other);
if (f.seen)
continue;
if (!v.level) {
const int64_t id = unit_id (-other);
lrat_chain.push_back (id);
f.seen = true;
analyzed.push_back (other);
continue;
}
if (v.reason) { // recursive justification
LOG ("VIVIFY LRAT pushing %d", other);
stack.push_back ({other, v.reason, false});
}
}
}
stack.clear ();
}
// calculate lrat_chain
//
inline void Internal::vivify_chain_for_units (int lit, Clause *reason) {
if (!lrat)
return;
if (level)
return; // not decision level 0
CADICAL_assert (lrat_chain.empty ());
for (auto &reason_lit : *reason) {
if (lit == reason_lit)
continue;
CADICAL_assert (val (reason_lit));
const int signed_reason_lit = val (reason_lit) * reason_lit;
int64_t id = unit_id (signed_reason_lit);
lrat_chain.push_back (id);
}
lrat_chain.push_back (reason->id);
}
vivify_ref create_ref (Internal *internal, Clause *c) {
LOG (c, "creating vivify_refs of clause");
vivify_ref ref;
ref.clause = c;
ref.size = c->size;
for (int i = 0; i < COUNTREF_COUNTS; ++i)
ref.count[i] = 0;
ref.vivify = c->vivify;
int lits[COUNTREF_COUNTS] = {0};
for (int i = 0; i != std::min (COUNTREF_COUNTS, c->size); ++i) {
int best = 0;
unsigned best_count = 0;
for (auto lit : *c) {
LOG ("to find best number of occurrences for literal %d, looking at "
"literal %d",
i, lit);
for (int j = 0; j != i; ++j) {
LOG ("comparing %d with literal %d", lit, lits[j]);
if (lits[j] == lit)
goto CONTINUE_WITH_NEXT_LITERAL;
}
{
const int64_t lit_count = internal->noccs (lit);
CADICAL_assert (lit_count);
LOG ("checking literal %s with %" PRId64 " occurrences",
LOGLIT (lit), lit_count);
if (lit_count <= best_count)
continue;
best_count = lit_count;
best = lit;
}
CONTINUE_WITH_NEXT_LITERAL:;
}
CADICAL_assert (best);
CADICAL_assert (best_count);
CADICAL_assert (best_count < UINT32_MAX);
ref.count[i] =
(((uint64_t) best_count) << 32) + (uint64_t) internal->vlit (best);
LOG ("final count at position %d is %s - %u: %" PRIu64, i,
LOGLIT (best), best_count, ref.count[i]);
lits[i] = best;
}
return ref;
}
/*------------------------------------------------------------------------*/
inline void
Internal::vivify_prioritize_leftovers (char tag, size_t prioritized,
std::vector<Clause *> &schedule) {
if (prioritized) {
PHASE ("vivify", stats.vivifications,
"[phase %c] leftovers of %zu clause", tag, prioritized);
} else {
PHASE ("vivify", stats.vivifications,
"[phase %c] prioritizing all clause", tag);
for (auto c : schedule)
c->vivify = true;
}
#ifdef CADICAL_QUIET
(void) tag;
#endif
const size_t max = opts.vivifyschedmax;
if (schedule.size () > max) {
if (prioritized) {
// put the left-overs first to be kept
std::stable_partition (begin (schedule), end (schedule),
[] (Clause *c) { return c->vivify; });
}
schedule.resize (max);
}
// let's try to save a bit of memory
shrink_vector (schedule);
}
void Internal::vivify_initialize (Vivifier &vivifier, int64_t &ticks) {
const int tier1 = vivifier.tier1_limit;
const int tier2 = vivifier.tier2_limit;
// Count the number of occurrences of literals in all clauses,
// particularly binary clauses, which are usually responsible
// for most of the propagations.
//
init_noccs ();
// Disconnect all watches since we sort literals within clauses.
//
CADICAL_assert (watching ());
#if 0
clear_watches ();
#endif
size_t prioritized_irred = 0, prioritized_tier1 = 0,
prioritized_tier2 = 0, prioritized_tier3 = 0;
for (const auto &c : clauses) {
++ticks;
if (c->size == 2)
continue; // see also (NO-BINARY) above
if (!consider_to_vivify_clause (c))
continue;
// This computes an approximation of the Jeroslow Wang heuristic
// score
//
// nocc (L) = sum 2^(12-|C|)
// L in C in F
//
// but we cap the size at 12, that is all clauses of size 12 and
// larger contribute '1' to the score, which allows us to use 'long'
// numbers. See the example above (search for '@1').
//
const int shift = 12 - c->size;
const int64_t score = shift < 1 ? 1 : (1l << shift); // @4
for (const auto lit : *c) {
noccs (lit) += score;
}
LOG (c, "putting clause in candidates");
if (!c->redundant)
vivifier.schedule_irred.push_back (c),
prioritized_irred += (c->vivify);
else if (c->glue <= tier1)
vivifier.schedule_tier1.push_back (c),
prioritized_tier1 += (c->vivify);
else if (c->glue <= tier2)
vivifier.schedule_tier2.push_back (c),
prioritized_tier2 += (c->vivify);
else
vivifier.schedule_tier3.push_back (c),
prioritized_tier3 += (c->vivify);
++ticks;
}
vivify_prioritize_leftovers ('x', prioritized_irred,
vivifier.schedule_irred);
vivify_prioritize_leftovers ('u', prioritized_tier1,
vivifier.schedule_tier1);
vivify_prioritize_leftovers ('v', prioritized_tier2,
vivifier.schedule_tier2);
vivify_prioritize_leftovers ('w', prioritized_tier3,
vivifier.schedule_tier3);
if (opts.vivifyflush) {
clear_watches ();
for (auto &sched : vivifier.schedules) {
// approximation for schedule
ticks += 1 + cache_lines (sched.size (), sizeof (Clause *));
for (const auto &c : sched) {
// Literals in scheduled clauses are sorted with their highest score
// literals first (as explained above in the example at '@2'). This
// is also needed in the prefix subsumption checking below. We do an
// approximation below that is done only in the vivify_ref structure
// below.
//
sort (c->begin (), c->end (), vivify_more_noccs (this));
// ++ticks;
}
// Flush clauses subsumed by another clause with the same prefix,
// which also includes flushing syntactically identical clauses.
//
flush_vivification_schedule (sched, ticks);
}
// approximation for schedule
// ticks += 1 + cache_lines (clauses.size (), sizeof (Clause *));
connect_watches (); // watch all relevant clauses
}
vivify_propagate (ticks);
PHASE ("vivify", stats.vivifications,
"[phase %c] leftovers out of %zu clauses", 'u',
vivifier.schedule_tier1.size ());
}
inline std::vector<vivify_ref> &current_refs_schedule (Vivifier &vivifier) {
return vivifier.refs_schedule;
}
inline std::vector<Clause *> &current_schedule (Vivifier &vivifier) {
switch (vivifier.tier) {
case Vivify_Mode::TIER1:
return vivifier.schedule_tier1;
break;
case Vivify_Mode::TIER2:
return vivifier.schedule_tier2;
break;
case Vivify_Mode::TIER3:
return vivifier.schedule_tier3;
break;
default:
return vivifier.schedule_irred;
break;
}
#if defined(WIN32) && !defined(__MINGW32__)
__assume(false);
#else
__builtin_unreachable ();
#endif
}
struct vivify_refcount_rank {
int offset;
vivify_refcount_rank (int j) : offset (j) {
CADICAL_assert (offset < COUNTREF_COUNTS);
}
typedef uint64_t Type;
Type operator() (const vivify_ref &a) const { return a.count[offset]; }
};
struct vivify_refcount_smaller {
int offset;
vivify_refcount_smaller (int j) : offset (j) {
CADICAL_assert (offset < COUNTREF_COUNTS);
}
bool operator() (const vivify_ref &a, const vivify_ref &b) const {
const auto s = vivify_refcount_rank (offset) (a);
const auto t = vivify_refcount_rank (offset) (b);
return s < t;
}
};
struct vivify_inversesize_rank {
vivify_inversesize_rank () {}
typedef uint64_t Type;
Type operator() (const vivify_ref &a) const { return ~a.size; }
};
struct vivify_inversesize_smaller {
vivify_inversesize_smaller () {}
bool operator() (const vivify_ref &a, const vivify_ref &b) const {
const auto s = vivify_inversesize_rank () (a);
const auto t = vivify_inversesize_rank () (b);
return s < t;
}
};
/*------------------------------------------------------------------------*/
// There are two modes of vivification, one using all clauses and one
// focusing on irredundant clauses only. The latter variant working on
// irredundant clauses only can also remove irredundant asymmetric
// tautologies (clauses subsumed through unit propagation), which in
// redundant mode is incorrect (due to propagating over redundant clauses).
void Internal::vivify_round (Vivifier &vivifier, int64_t ticks_limit) {
if (unsat)
return;
if (terminated_asynchronously ())
return;
auto &refs_schedule = current_refs_schedule (vivifier);
auto &schedule = current_schedule (vivifier);
PHASE ("vivify", stats.vivifications,
"starting %c vivification round ticks limit %" PRId64
" with %zu clauses",
vivifier.tag, ticks_limit, schedule.size ());
CADICAL_assert (watching ());
int64_t ticks = 1 + schedule.size ();
// Sort candidates, with first to be tried candidate clause last, i.e.,
// many occurrences and high score literals) as in the example explained
// above (search for '@3').
//
if (vivifier.tier != Vivify_Mode::IRREDUNDANT ||
irredundant () / 10 < redundant ()) {
// Literals in scheduled clauses are sorted with their highest score
// literals first (as explained above in the example at '@2'). This is
// also needed in the prefix subsumption checking below. We do an
// approximation below that is done only in the vivify_ref structure
// below.
//
// first build the schedule with vivifier_refs
const auto end_schedule = end (schedule);
refs_schedule.resize (schedule.size ());
std::transform (begin (schedule), end_schedule, begin (refs_schedule),
[&] (Clause *c) { return create_ref (this, c); });
// now sort by size
MSORT (opts.radixsortlim, refs_schedule.begin (), refs_schedule.end (),
vivify_inversesize_rank (), vivify_inversesize_smaller ());
// now (stable) sort by number of occurrences
for (int i = 0; i < COUNTREF_COUNTS; ++i) {
const int offset = COUNTREF_COUNTS - 1 - i;
MSORT (opts.radixsortlim, refs_schedule.begin (),
refs_schedule.end (), vivify_refcount_rank (offset),
vivify_refcount_smaller (offset));
}
// force left-overs at the end
std::stable_partition (begin (refs_schedule), end (refs_schedule),
[] (vivify_ref c) { return !c.vivify; });
std::transform (begin (refs_schedule), end (refs_schedule),
begin (schedule),
[] (vivify_ref c) { return c.clause; });
refs_schedule.clear ();
} else {
// skip sorting but still put clauses with the vivify tag at the end to
// be done first Kissat does this implicitely by going twice over all
// clauses
std::stable_partition (begin (schedule), end (schedule),
[] (Clause *c) { return !c->vivify; });
}
// Remember old values of counters to summarize after each round with
// verbose messages what happened in that round.
//
int64_t checked = stats.vivifychecks;
int64_t subsumed = stats.vivifysubs;
int64_t strengthened = stats.vivifystrs;
int64_t units = stats.vivifyunits;
int64_t scheduled = schedule.size ();
stats.vivifysched += scheduled;
PHASE ("vivify", stats.vivifications,
"scheduled %" PRId64 " clauses to be vivified %.0f%%", scheduled,
percent (scheduled, stats.current.irredundant));
// Limit the number of propagations during vivification as in 'probe'.
//
const int64_t limit = ticks_limit - stats.ticks.vivify;
CADICAL_assert (limit >= 0);
// the clauses might still contain set literals, so propagation since the
// beginning
propagated2 = propagated = 0;
if (!unsat && !propagate ()) {
LOG ("propagation after connecting watches in inconsistency");
learn_empty_clause ();
}
vivifier.ticks = ticks;
int retry = 0;
while (!unsat && !terminated_asynchronously () && !schedule.empty () &&
vivifier.ticks < limit) {
Clause *c = schedule.back (); // Next candidate.
schedule.pop_back ();
if (vivify_clause (vivifier, c) && !c->garbage && c->size > 2 &&
retry < opts.vivifyretry) {
++retry;
schedule.push_back (c);
} else
retry = 0;
}
if (level)
backtrack_without_updating_phases ();
if (!unsat) {
int64_t still_need_to_be_vivified = schedule.size ();
#if 0
// in the current round we have new_clauses_to_vivify @ leftovers from previous round There are
// now two possibilities: (i) we consider all clauses as leftovers, or (ii) only the leftovers
// from previous round are considered leftovers.
//
// CaDiCaL had the first version before. If
// commented out we go to the second version.
for (auto c : schedule)
c->vivify = true;
#elif 1
// if we have gone through all the leftovers (the next candidate
// is not one), all the current clauses are leftovers for the next
// round
if (!schedule.empty () && !schedule.back ()->vivify)
for (auto c : schedule)
c->vivify = true;
#else
// do nothing like in kissat and use the candidates for next time.
#endif
// Preference clauses scheduled but not vivified yet next time.
//
if (still_need_to_be_vivified)
PHASE ("vivify", stats.vivifications,
"still need to vivify %" PRId64 " clauses %.02f%% of %" PRId64
" scheduled",
still_need_to_be_vivified,
percent (still_need_to_be_vivified, scheduled), scheduled);
else {
PHASE ("vivify", stats.vivifications,
"no previously not yet vivified clause left");
}
erase_vector (schedule); // Reclaim memory early.
}
if (!unsat) {
// Since redundant clause were disconnected during propagating vivified
// units in redundant mode, and further irredundant clauses are
// arbitrarily sorted, we have to propagate all literals again after
// connecting the first two literals in the clauses, in order to
// reestablish the watching invariant.
//
propagated2 = propagated = 0;
if (!propagate ()) {
LOG ("propagating vivified units leads to conflict");
learn_empty_clause ();
}
}
checked = stats.vivifychecks - checked;
subsumed = stats.vivifysubs - subsumed;
strengthened = stats.vivifystrs - strengthened;
units = stats.vivifyunits - units;
PHASE ("vivify", stats.vivifications,
"checked %" PRId64 " clauses %.02f%% of %" PRId64
" scheduled using %" PRIu64 " ticks",
checked, percent (checked, scheduled), scheduled, vivifier.ticks);
if (units)
PHASE ("vivify", stats.vivifications,
"found %" PRId64 " units %.02f%% of %" PRId64 " checked", units,
percent (units, checked), checked);
if (subsumed)
PHASE ("vivify", stats.vivifications,
"subsumed %" PRId64 " clauses %.02f%% of %" PRId64 " checked",
subsumed, percent (subsumed, checked), checked);
if (strengthened)
PHASE ("vivify", stats.vivifications,
"strengthened %" PRId64 " clauses %.02f%% of %" PRId64
" checked",
strengthened, percent (strengthened, checked), checked);
stats.subsumed += subsumed;
stats.strengthened += strengthened;
stats.ticks.vivify += vivifier.ticks;
bool unsuccessful = !(subsumed + strengthened + units);
report (vivifier.tag, unsuccessful);
}
void set_vivifier_mode (Vivifier &vivifier, Vivify_Mode tier) {
vivifier.tier = tier;
switch (tier) {
case Vivify_Mode::TIER1:
vivifier.tag = 'u';
break;
case Vivify_Mode::TIER2:
vivifier.tag = 'v';
break;
case Vivify_Mode::TIER3:
vivifier.tag = 'w';
break;
default:
CADICAL_assert (tier == Vivify_Mode::IRREDUNDANT);
vivifier.tag = 'x';
break;
}
}
/*------------------------------------------------------------------------*/
void Internal::compute_tier_limits (Vivifier &vivifier) {
if (!opts.vivifycalctier) {
vivifier.tier1_limit = 2;
vivifier.tier2_limit = 6;
return;
}
vivifier.tier1_limit = tier1[false];
vivifier.tier2_limit = tier2[false];
}
/*------------------------------------------------------------------------*/
bool Internal::vivify () {
if (unsat)
return false;
if (terminated_asynchronously ())
return false;
if (!opts.vivify)
return false;
if (!stats.current.irredundant)
return false;
if (level)
backtrack ();
CADICAL_assert (opts.vivify);
CADICAL_assert (!level);
SET_EFFORT_LIMIT (totallimit, vivify, true);
private_steps = true;
START_SIMPLIFIER (vivify, VIVIFY);
stats.vivifications++;
// the effort is normalized by dividing by sumeffort below, hence no need
// to multiply by 1e-3 (also making the precision better)
double tier1effort = !opts.vivifytier1 ? 0 : (double) opts.vivifytier1eff;
double tier2effort = !opts.vivifytier2 ? 0 : (double) opts.vivifytier2eff;
double tier3effort = !opts.vivifytier3 ? 0 : (double) opts.vivifytier3eff;
double irreffort =
delaying_vivify_irredundant.bumpreasons.delay () || !opts.vivifyirred
? 0
: (double) opts.vivifyirredeff;
double sumeffort = tier1effort + tier2effort + tier3effort + irreffort;
if (!stats.current.redundant)
tier1effort = tier2effort = tier3effort = 0;
if (!sumeffort)
sumeffort = irreffort = 1;
int64_t total = totallimit - stats.ticks.vivify;
PHASE ("vivify", stats.vivifications,
"vivification limit of %" PRId64 " ticks", total);
Vivifier vivifier (Vivify_Mode::TIER1);
compute_tier_limits (vivifier);
if (vivifier.tier1_limit == vivifier.tier2_limit) {
tier1effort += tier2effort;
tier2effort = 0;
LOG ("vivification tier1 matches tier2 "
"thus using tier2 budget for tier1");
}
int64_t init_ticks = 0;
// Refill the schedule every time. Unchecked clauses are 'saved' by
// setting their 'vivify' bit, such that they can be tried next time.
// There are two things to denote: the option 'vivifyonce' does what it is
// supposed to do, and it works because the ticks are kept for the next
// schedule. Also, we limit the size of the schedule to limit the cost of
// sorting.
//
// TODO: After limiting, the cost we fixed some heuristics bug, so maybe
// we could increase the limit.
//
// TODO: count against ticks.vivify directly instead of this unholy
// shifting.
vivify_initialize (vivifier, init_ticks);
stats.ticks.vivify += init_ticks;
int64_t limit = stats.ticks.vivify;
const double shared_effort = (double) init_ticks / 4.0;
if (opts.vivifytier1) {
set_vivifier_mode (vivifier, Vivify_Mode::TIER1);
if (limit < stats.ticks.vivify)
limit = stats.ticks.vivify;
const double effort = (total * tier1effort) / sumeffort;
CADICAL_assert (std::numeric_limits<int64_t>::max () - (int64_t) effort >=
limit);
limit += effort;
if (limit - shared_effort > stats.ticks.vivify) {
limit -= shared_effort;
CADICAL_assert (limit >= 0);
vivify_round (vivifier, limit);
} else {
LOG ("building the schedule already used our entire ticks budget for "
"tier1");
}
}
if (!unsat && tier2effort) {
// save memory (well, not really as we
// already reached the peak memory)
erase_vector (vivifier.schedule_tier1);
if (limit < stats.ticks.vivify)
limit = stats.ticks.vivify;
const double effort = (total * tier2effort) / sumeffort;
CADICAL_assert (std::numeric_limits<int64_t>::max () - (int64_t) effort >=
limit);
limit += effort;
if (limit - shared_effort > stats.ticks.vivify) {
limit -= shared_effort;
CADICAL_assert (limit >= 0);
set_vivifier_mode (vivifier, Vivify_Mode::TIER2);
vivify_round (vivifier, limit);
} else {
LOG ("building the schedule already used our entire ticks budget for "
"tier2");
}
}
if (!unsat && tier3effort) {
erase_vector (vivifier.schedule_tier2);
if (limit < stats.ticks.vivify)
limit = stats.ticks.vivify;
const double effort = (total * tier3effort) / sumeffort;
CADICAL_assert (std::numeric_limits<int64_t>::max () - (int64_t) effort >=
limit);
limit += effort;
if (limit - shared_effort > stats.ticks.vivify) {
limit -= shared_effort;
CADICAL_assert (limit >= 0);
set_vivifier_mode (vivifier, Vivify_Mode::TIER3);
vivify_round (vivifier, limit);
} else {
LOG ("building the schedule already used our entire ticks budget for "
"tier3");
}
}
if (!unsat && irreffort) {
erase_vector (vivifier.schedule_tier3);
if (limit < stats.ticks.vivify)
limit = stats.ticks.vivify;
const double effort = (total * irreffort) / sumeffort;
CADICAL_assert (std::numeric_limits<int64_t>::max () - (int64_t) effort >=
limit);
limit += effort;
if (limit - shared_effort > stats.ticks.vivify) {
limit -= shared_effort;
CADICAL_assert (limit >= 0);
set_vivifier_mode (vivifier, Vivify_Mode::IRREDUNDANT);
const int old = stats.vivifystrirr;
const int old_tried = stats.vivifychecks;
vivify_round (vivifier, limit);
if (stats.vivifychecks - old_tried == 0 ||
(float) (stats.vivifystrirr - old) /
(float) (stats.vivifychecks - old_tried) <
0.01) {
delaying_vivify_irredundant.bumpreasons.bump_delay ();
} else {
delaying_vivify_irredundant.bumpreasons.reduce_delay ();
}
} else {
delaying_vivify_irredundant.bumpreasons.bump_delay ();
LOG ("building the schedule already used our entire ticks budget for "
"irredundant");
}
}
reset_noccs ();
STOP_SIMPLIFIER (vivify, VIVIFY);
private_steps = false;
CADICAL_assert (!ignore);
return true;
}
} // namespace CaDiCaL
ABC_NAMESPACE_IMPL_END