use super::PipelineOutput;
use super::cadical_ffi::note_solver_unavailable;
use super::equivalence;
use super::pipelines::{ClauseCounts, Stage, StageOutcome, diff_stats, unsat_stats};
use super::probe_engine::ProbeEngine;
use super::unit_propagation;
use crate::cnf::{Clause, CnfFormula};
use crate::diagnostics::diag;
#[derive(Clone, Debug, Default)]
pub(crate) struct BackboneStats {
pub backbone_found: usize,
pub backbone_probes: usize,
pub backbone_ms: Option<u64>,
pub equivalence_ms: Option<u64>,
}
pub(super) fn stage_probe(
formula: &CnfFormula,
backbone_budget: std::time::Duration,
equiv_budget: Option<std::time::Duration>,
deadline: Option<std::time::Instant>,
meter: &mut super::meter::PreprocessMeter,
) -> StageOutcome {
let input = ClauseCounts::of(&formula.clauses);
let partial_stats = |bb_count: usize,
bb_probes: usize,
backbone_ms: Option<u64>,
equivalence_ms: Option<u64>| BackboneStats {
backbone_found: bb_count,
backbone_probes: bb_probes,
backbone_ms,
equivalence_ms,
};
let mut f = formula.clone();
let Some(mut engine) = ProbeEngine::new(&f) else {
note_solver_unavailable("probe", "the stage is skipped");
return StageOutcome {
formula: f,
stats: diff_stats(input, input, 0),
unsat: false,
mapping: None,
backbone: Some(partial_stats(0, 0, None, None)),
};
};
let backbone_budget = meter.clamp(backbone_budget, deadline);
let bb = engine.run_backbone_with_meter(backbone_budget, meter);
if bb.unsat {
return StageOutcome::refuted(
CnfFormula::contradiction(formula.num_vars),
unsat_stats(input, 0),
)
.with_backbone(partial_stats(
0,
bb.probes_completed,
Some(bb.elapsed_ms),
None,
));
}
let bb_count = bb.forced.len();
let bb_probes = bb.probes_completed;
if bb_count > 0 || bb.flippable_eliminated > 0 {
diag!(
"[backbone] {} forced vars ({}/{} probed, {} fixed, {} flippable-eliminated, {} model-eliminated, SAT solve {}ms)",
bb_count,
bb_probes,
f.num_vars,
bb.fixed_found,
bb.flippable_eliminated,
bb.model_eliminated,
bb.solve_ms
);
}
if bb_count > 0 {
for lit in &bb.forced {
f.clauses.push(Clause::new(vec![*lit]));
}
let (propagated_clauses, propagated_forced) =
unit_propagation::propagate(&f.clauses, f.num_vars);
let mut clauses = propagated_clauses;
for &lit in &propagated_forced {
clauses.push(Clause::new(vec![lit]));
}
let propagated = CnfFormula {
num_vars: f.num_vars,
clauses,
};
if propagated.is_refuted() {
return StageOutcome::refuted(
CnfFormula::contradiction(formula.num_vars),
unsat_stats(input, bb_count + propagated_forced.len()),
)
.with_backbone(partial_stats(
bb_count,
bb_probes,
Some(bb.elapsed_ms),
None,
));
}
f = propagated;
}
let (eq2, mapping2) = equivalence::extract_equivalences_with_mapping(&f);
if eq2.is_unsat {
return StageOutcome::refuted(
CnfFormula::contradiction(formula.num_vars),
unsat_stats(input, bb_count),
)
.with_backbone(partial_stats(
bb_count,
bb_probes,
Some(bb.elapsed_ms),
None,
));
}
if eq2.num_equivalences > 0 {
diag!(
"[post-backbone-tarjan] {} new equiv classes, {} → {} clauses",
eq2.num_equivalences,
f.clauses.len(),
eq2.formula.clauses.len()
);
}
f = eq2.formula;
if let Some(m) = mapping2.as_ref() {
engine.ingest_tarjan_equivs(m);
}
let mut equivalence_ms = None;
if let Some(equiv_budget) = equiv_budget {
let equiv_budget = meter.clamp(equiv_budget, deadline);
let eq_result = engine.run_equiv_with_meter(equiv_budget, &mapping2, meter);
equivalence_ms = Some(eq_result.elapsed_ms);
if eq_result.unsat {
return StageOutcome::refuted(
CnfFormula::contradiction(formula.num_vars),
unsat_stats(input, bb_count),
)
.with_backbone(partial_stats(
bb_count,
bb_probes,
Some(bb.elapsed_ms),
equivalence_ms,
));
}
if !eq_result.equivalences.is_empty() {
diag!(
"[sat-equiv-probing] {} equivalences ({} probes)",
eq_result.equivalences.len(),
eq_result.probes_completed
);
for &(l1, l2) in &eq_result.equivalences {
f.clauses.push(Clause::new(vec![l1.negated(), l2]));
f.clauses.push(Clause::new(vec![l1, l2.negated()]));
}
}
}
let backbone_stats = BackboneStats {
backbone_found: bb_count,
backbone_probes: bb_probes,
backbone_ms: Some(bb.elapsed_ms),
equivalence_ms,
};
let stats = diff_stats(input, ClauseCounts::of(&f.clauses), bb_count);
StageOutcome {
formula: f,
stats,
unsat: false,
mapping: mapping2,
backbone: Some(backbone_stats),
}
}
#[cfg(test)]
pub(crate) fn preprocess_backbone_eq_iter(
formula: &CnfFormula,
backbone_budget: std::time::Duration,
equiv_budget: Option<std::time::Duration>,
deadline: Option<std::time::Instant>,
) -> PipelineOutput {
let mut meter = super::meter::PreprocessMeter::new(crate::config::PreprocessClock::WallClock);
preprocess_backbone_eq_iter_with_meter(
formula,
backbone_budget,
equiv_budget,
deadline,
&mut meter,
)
}
pub(crate) fn preprocess_backbone_eq_iter_with_meter(
formula: &CnfFormula,
backbone_budget: std::time::Duration,
equiv_budget: Option<std::time::Duration>,
deadline: Option<std::time::Instant>,
meter: &mut super::meter::PreprocessMeter,
) -> PipelineOutput {
let original = ClauseCounts::of(&formula.clauses);
let p = super::pipelines::run_pipeline_with_meter(
formula,
&[
Stage::Tarjan,
Stage::Probe {
backbone: backbone_budget,
equiv: equiv_budget,
},
],
deadline,
meter,
);
let bb_stats = p.backbone.unwrap_or_default();
if p.formula.is_refuted() {
return PipelineOutput {
formula: CnfFormula::contradiction(formula.num_vars),
stats: unsat_stats(original, p.stats.forced_vars),
mapping: None,
backbone: Some(bb_stats),
};
}
let eq_iter =
super::pipelines::preprocess_eq_iter_with_mapping_and_meter(&p.formula, deadline, meter);
let combined = diff_stats(
original,
ClauseCounts::of(&eq_iter.formula.clauses),
bb_stats.backbone_found + eq_iter.stats.forced_vars,
);
PipelineOutput {
stats: combined,
backbone: Some(bb_stats),
..eq_iter
}
}