pub mod components;
use std::path::{Path, PathBuf};
use num_rational::BigRational;
use serde::{Deserialize, Serialize};
use crate::component::VtreeBuild;
use crate::vtree::VarId;
use crate::cnf::{
Clause, CnfFormula, CnfMeta, Literal, Mode, Original, Reduced, ShowSet, Weights,
rational_string,
};
use crate::config::{Chain, RunConfig};
use crate::diagnostics::diag;
use crate::error::VitriError;
use crate::preprocess::arjun::{
ArjunKeep, ArjunProjResult, ArjunResult, ArjunWeightedProjResult, ArjunWeightedResult,
arjun_keep_reduction, run_arjun_anytime, run_arjun_projected_anytime,
run_arjun_weighted_anytime, run_arjun_weighted_projected_anytime,
};
use crate::preprocess::projected::{ProjectedReduction, strengthen_and_bve};
use crate::preprocess::simplify::{
DveBudget, OriginalFate, SimplifiedFormula, SimplifyConfig, SimplifyPurpose, simplify,
};
use crate::preprocess::weighted_lift::{self, DveVerdict};
use crate::preprocess::{OriginalMap, VarMap};
mod compile_chain;
mod count_chain;
mod plumbing;
mod projection_chain;
mod stage;
use compile_chain::compile_preserving_bundle;
use count_chain::count_preserving_bundle_with_stage1;
use plumbing::{
DotFor, ensure_dir, original_weights, preprocess_config, refuted, to_json_pretty, weight_table,
write_file, write_vtree_files,
};
use projection_chain::projection_preserving_bundle;
pub const REDUCED_CNF_NAME: &str = "reduced.cnf";
pub const PREPROCESS_RECORD_NAME: &str = "preprocess.json";
pub const VTREE_NAME: &str = "vtree.vtree";
pub use crate::cnf::weights::LiteralWeight;
mod mode_token {
use crate::cnf::Mode;
use serde::de::Error as _;
use serde::{Deserialize, Deserializer, Serializer};
pub(super) fn serialize<S: Serializer>(mode: &Mode, ser: S) -> Result<S::Ok, S::Error> {
ser.serialize_str(mode.token())
}
pub(super) fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<Mode, D::Error> {
let token = String::deserialize(de)?;
Mode::parse_mode(&token).ok_or_else(|| D::Error::custom(format!("unknown mode {token:?}")))
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PreprocessRecord {
pub format: String,
#[serde(with = "mode_token")]
pub mode: Mode,
pub count_lift_pow2: u32,
pub weight_lift: String,
pub original_num_vars: u32,
pub reduced_to_original_dimacs: VarMap<Reduced, Original>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub original_to_reduced_dimacs: Option<OriginalMap>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub forced_literals_original_dimacs: Vec<i32>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub free_vars_original_dimacs: Vec<u32>,
pub unsat: bool,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::cnf::show_set::dimacs"
)]
pub show_vars_reduced_dimacs: Option<ShowSet<Reduced>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reduced_weights: Option<Vec<LiteralWeight>>,
}
enum RecordLift {
Pow2(u32),
Weight(BigRational),
}
impl RecordLift {
const NEUTRAL_WEIGHT: &'static str = "1/1";
fn neutral() -> Self {
RecordLift::Pow2(0)
}
fn into_fields(self) -> (u32, String) {
match self {
RecordLift::Pow2(k) => (k, RecordLift::NEUTRAL_WEIGHT.to_string()),
RecordLift::Weight(w) => (0, rational_string(&w)),
}
}
}
pub const RECORD_FORMAT_TAG: &str = "vitri-preprocess-v1";
impl PreprocessRecord {
fn new(
mode: Mode,
lift: RecordLift,
original_num_vars: u32,
reduced_to_original_dimacs: VarMap<Reduced, Original>,
) -> Self {
let (count_lift_pow2, weight_lift) = lift.into_fields();
PreprocessRecord {
format: RECORD_FORMAT_TAG.to_string(),
mode,
count_lift_pow2,
weight_lift,
original_num_vars,
reduced_to_original_dimacs,
original_to_reduced_dimacs: None,
forced_literals_original_dimacs: Vec::new(),
free_vars_original_dimacs: Vec::new(),
unsat: false,
show_vars_reduced_dimacs: None,
reduced_weights: None,
}
}
pub fn lift(&self) -> String {
if self.weight_lift == RecordLift::NEUTRAL_WEIGHT {
format!("2^{}", self.count_lift_pow2)
} else {
self.weight_lift.clone()
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum StageOutcome {
Ran,
Skipped(SkipReason),
GaveUp,
Discarded(DiscardReason),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum SkipReason {
NotRequested,
NothingToDo,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum DiscardReason {
NotSmaller,
NoProjectionGain,
WeightedUnusable,
NonInjectiveMap,
}
impl DiscardReason {
pub(super) fn phrase(self) -> &'static str {
match self {
DiscardReason::NotSmaller => "it grew the clause count",
DiscardReason::NoProjectionGain => "it did not minimize the projection",
DiscardReason::WeightedUnusable => "lossy or inert",
DiscardReason::NonInjectiveMap => "non-injective variable map",
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct StageReport {
pub simplify: Option<StageOutcome>,
pub arjun: Option<StageOutcome>,
pub sbva: Option<StageOutcome>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct CountLift {
pub simplify_pow2: u32,
pub arjun_pow2: u32,
}
impl CountLift {
pub fn total_pow2(self) -> u32 {
self.simplify_pow2 + self.arjun_pow2
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct PreprocessTelemetry {
pub total_ms: u64,
pub simplify_ms: Option<u64>,
pub backbone_ms: Option<u64>,
pub equivalence_ms: Option<u64>,
pub dve_ms: Option<u64>,
pub arjun_ms: Option<u64>,
pub backbone_found: usize,
pub backbone_probes: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum PreprocessPhase {
Backbone,
Equivalence,
Dve,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct ProbeDecisionCounts {
pub completed: usize,
pub satisfiable: usize,
pub unsatisfiable: usize,
pub unknown: usize,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct PreprocessPhaseTrace {
pub phase: PreprocessPhase,
pub budget_ms: u64,
pub budget_units: u64,
pub spent_units: u64,
pub probes: ProbeDecisionCounts,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct DveDecisionTrace {
pub rounds: usize,
pub aggressive_passes: usize,
pub defined_eliminated: usize,
pub equivalence_eliminated: usize,
pub budget_hit: bool,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct PreprocessDecisionTrace {
pub total_units: u64,
pub phases: Vec<PreprocessPhaseTrace>,
pub dve: DveDecisionTrace,
}
impl PreprocessTelemetry {
fn from_simplified(simplified: &SimplifiedFormula, attempted: bool) -> Self {
let measured = simplified.telemetry;
PreprocessTelemetry {
simplify_ms: attempted.then_some(measured.total_ms),
backbone_ms: measured.backbone_ms,
equivalence_ms: measured.equivalence_ms,
dve_ms: measured.dve_ms,
backbone_found: measured.backbone_found,
backbone_probes: measured.backbone_probes,
..PreprocessTelemetry::default()
}
}
}
#[derive(Clone, Debug)]
pub struct PreprocessBundle {
pub reduced: CnfFormula,
pub record: PreprocessRecord,
pub stages: StageReport,
pub count_lift: CountLift,
pub telemetry: PreprocessTelemetry,
pub decision_trace: Option<PreprocessDecisionTrace>,
pub arjun_input: Option<CnfFormula>,
pub independent_support_reduced: Option<crate::cnf::ShowSet<crate::cnf::Reduced>>,
pub learnt_clauses_reduced_dimacs: Vec<Vec<i32>>,
}
#[derive(Debug)]
pub struct BundlePaths {
pub reduced_cnf: PathBuf,
pub record: PathBuf,
}
pub fn preprocess(
formula: &CnfFormula,
meta: &CnfMeta,
config: &RunConfig,
) -> Result<PreprocessBundle, VitriError> {
config.validate()?;
preprocess_anchored(formula, meta, &config.anchored(std::time::Instant::now()))
}
fn preprocess_anchored(
formula: &CnfFormula,
meta: &CnfMeta,
config: &RunConfig,
) -> Result<PreprocessBundle, VitriError> {
preprocess_anchored_with_checkpoint(formula, meta, config).map(|outcome| outcome.bundle)
}
struct PreprocessOutcome {
bundle: PreprocessBundle,
count_stage1: Option<count_chain::CountStage1>,
}
fn preprocess_anchored_with_checkpoint(
formula: &CnfFormula,
meta: &CnfMeta,
config: &RunConfig,
) -> Result<PreprocessOutcome, VitriError> {
let started = std::time::Instant::now();
if formula.num_vars == 0 {
return Err(VitriError::input(
"the formula declares no variables — nothing to build a vtree over",
));
}
let resolved = config.resolve_mode(meta)?;
let mode = resolved.mode;
config.refuse_inert(mode)?;
for n in &resolved.notices {
diag!("{n}");
}
let (mut bundle, count_stage1) = match Chain::for_mode(mode) {
Chain::Compile => (compile_preserving_bundle(formula, meta, config), None),
Chain::Projection => (
projection_preserving_bundle(formula, meta, config, mode)?,
None,
),
Chain::Count => {
let (bundle, stage1) =
count_preserving_bundle_with_stage1(formula, meta, config, mode)?;
(bundle, Some(stage1))
}
};
if matches!(
config.preprocess_clock,
crate::config::PreprocessClock::Deterministic { .. }
) && bundle.decision_trace.is_none()
{
bundle.decision_trace = Some(PreprocessDecisionTrace::default());
}
bundle.telemetry.total_ms = started.elapsed().as_millis() as u64;
Ok(PreprocessOutcome {
bundle,
count_stage1,
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RetryBudget {
deadline: std::time::Instant,
arjun_budget: std::time::Duration,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct FrontendRetryConfig {
pub arjun_sbva: Option<crate::preprocess::ArjunSbva>,
pub vtree_spec: Option<String>,
}
impl RetryBudget {
pub fn new(
deadline: std::time::Instant,
arjun_budget: std::time::Duration,
) -> Result<Self, VitriError> {
if arjun_budget.is_zero() {
return Err(VitriError::config(
"a frontend retry needs a non-zero Arjun budget",
));
}
Ok(Self {
deadline,
arjun_budget,
})
}
pub fn deadline(self) -> std::time::Instant {
self.deadline
}
pub fn arjun_budget(self) -> std::time::Duration {
self.arjun_budget
}
}
pub struct FrontendSession<'a> {
formula: &'a CnfFormula,
meta: &'a CnfMeta,
config: RunConfig,
selection: crate::decompose::SelectionCtx,
source_profile: crate::score::StructureProfile,
count_stage1: Option<count_chain::CountStage1>,
prepared: bool,
}
impl std::fmt::Debug for FrontendSession<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FrontendSession")
.field("formula", self.formula)
.field("meta", self.meta)
.field("config", &self.config)
.field("selection", &self.selection)
.field("source_profile", &self.source_profile)
.field("has_count_stage1", &self.count_stage1.is_some())
.field("prepared", &self.prepared)
.finish()
}
}
fn retain_count_stage1(mode: Mode, arjun: Option<&StageOutcome>) -> bool {
mode == Mode::Mc && arjun == Some(&StageOutcome::Ran)
}
fn retry_produced_reduction(arjun: Option<&StageOutcome>) -> bool {
arjun == Some(&StageOutcome::Ran)
}
impl FrontendSession<'_> {
fn build_run(
&self,
preprocessed: PreprocessBundle,
config: &RunConfig,
) -> Result<VitriRun, VitriError> {
if preprocessed.record.unsat {
return Ok(VitriRun {
source_profile: self.source_profile,
preprocessed,
vtree: RunVtree::Refuted,
});
}
if preprocessed.reduced.num_vars == 0 {
return Ok(VitriRun {
source_profile: self.source_profile,
preprocessed,
vtree: RunVtree::FullyResolved,
});
}
let selection = run_selection(
&self.selection,
self.source_profile,
preprocessed.record.show_vars_reduced_dimacs.as_ref(),
preprocessed.reduced.num_vars,
);
let built =
crate::component::build_vtree_anchored(&preprocessed.reduced, config, &selection)?;
Ok(VitriRun {
source_profile: self.source_profile,
preprocessed,
vtree: RunVtree::Built(built),
})
}
pub fn retry(
&self,
budget: RetryBudget,
retry: &FrontendRetryConfig,
) -> Result<Option<VitriRun>, VitriError> {
if !self.prepared {
return Err(VitriError::config(
"FrontendSession::retry requires a completed primary prepare",
));
}
if let Some(vtree_spec) = retry.vtree_spec.as_deref() {
crate::spec::validate_vtree_spec(vtree_spec)?;
}
let Some(stage1) = self.count_stage1.as_ref() else {
return Ok(None);
};
let now = std::time::Instant::now();
let deadline = self
.config
.deadline
.map_or(budget.deadline, |run_deadline| {
run_deadline.min(budget.deadline)
});
if deadline <= now {
return Ok(None);
}
let mut retry_config = self.config.clone();
retry_config.deadline = Some(deadline);
retry_config.arjun_budget = crate::config::ArjunBudget::Exact(budget.arjun_budget);
if let Some(vtree_spec) = retry.vtree_spec.as_deref() {
retry_config.vtree_spec = vtree_spec.to_owned();
}
if let Some(sbva) = retry.arjun_sbva {
retry_config.arjun.sbva = sbva;
}
let preprocessed = count_chain::finish_count_preserving_attempt(stage1, &retry_config)?;
if !retry_produced_reduction(preprocessed.stages.arjun.as_ref()) {
return Ok(None);
}
self.build_run(preprocessed, &retry_config).map(Some)
}
pub fn prepare(&mut self) -> Result<VitriRun, VitriError> {
if self.prepared {
return Err(VitriError::config(
"FrontendSession::prepare may be called at most once",
));
}
self.prepared = true;
let outcome = preprocess_anchored_with_checkpoint(self.formula, self.meta, &self.config)?;
let preprocessed = outcome.bundle;
if retain_count_stage1(preprocessed.record.mode, preprocessed.stages.arjun.as_ref()) {
self.count_stage1 = outcome.count_stage1;
}
self.build_run(preprocessed, &self.config)
}
}
pub fn frontend<'a>(
formula: &'a CnfFormula,
meta: &'a CnfMeta,
config: &RunConfig,
selection: &crate::decompose::SelectionCtx,
) -> Result<FrontendSession<'a>, VitriError> {
frontend_at(formula, meta, config, selection, std::time::Instant::now())
}
fn frontend_at<'a>(
formula: &'a CnfFormula,
meta: &'a CnfMeta,
config: &RunConfig,
selection: &crate::decompose::SelectionCtx,
now: std::time::Instant,
) -> Result<FrontendSession<'a>, VitriError> {
config.validate()?;
selection.goatd.validate()?;
Ok(FrontendSession {
formula,
meta,
config: config.anchored(now),
selection: selection.clone(),
source_profile: crate::score::StructureProfile::measure(formula),
count_stage1: None,
prepared: false,
})
}
#[derive(Debug)]
pub struct VitriRun {
pub source_profile: crate::score::StructureProfile,
pub preprocessed: PreprocessBundle,
pub vtree: RunVtree,
}
#[derive(Debug)]
pub enum RunVtree {
Built(VtreeBuild),
FullyResolved,
Refuted,
}
impl VitriRun {
pub fn built(&self) -> Option<&VtreeBuild> {
match &self.vtree {
RunVtree::Built(b) => Some(b),
RunVtree::FullyResolved | RunVtree::Refuted => None,
}
}
pub fn write_to_dir(
&self,
dir: &Path,
options: components::ComponentWriteOptions,
) -> Result<RunPaths, VitriError> {
let bundle = self.preprocessed.write_to_dir(dir)?;
let Some(build) = self.built() else {
return Ok(RunPaths {
bundle,
vtree: None,
});
};
let vtree = build.write_to_dir(
dir,
&self.preprocessed.reduced,
self.preprocessed.record.show_vars_reduced_dimacs.as_ref(),
options,
)?;
Ok(RunPaths {
bundle,
vtree: Some(vtree),
})
}
}
#[derive(Debug)]
pub struct RunPaths {
pub bundle: BundlePaths,
pub vtree: Option<VtreeFiles>,
}
#[derive(Debug)]
pub struct VtreeFiles {
pub vtree: PathBuf,
pub dot: Option<PathBuf>,
pub components: ComponentFiles,
}
#[derive(Debug)]
pub struct ComponentFiles {
pub manifest: components::ComponentsManifest,
pub paths: components::ComponentPaths,
}
pub fn run(
formula: &CnfFormula,
meta: &CnfMeta,
config: &RunConfig,
selection: &crate::decompose::SelectionCtx,
) -> Result<VitriRun, VitriError> {
frontend(formula, meta, config, selection)?.prepare()
}
fn run_selection(
selection: &crate::decompose::SelectionCtx,
source_profile: crate::score::StructureProfile,
show: Option<&crate::cnf::ShowSet<crate::cnf::Reduced>>,
num_vars: u32,
) -> crate::decompose::SelectionCtx {
let mut selection = selection.clone().with_show(show, num_vars);
selection.source_profile = Some(source_profile);
selection
}
#[cfg(test)]
mod tests;