use std::collections::HashMap;
use num_rational::BigRational;
use num_traits::One;
use crate::cnf::{Clause, CnfFormula, Literal, Original, VarId, Weights, parse_weight};
use crate::preprocess::dve::types::DveFate;
use crate::preprocess::equivalence::EquivMapping;
use crate::preprocess::renumber::Renumber;
use crate::preprocess::simplify::{
DveReduction, EquivReduction, SimplifiedFormula, SimplifyTelemetry, Stripped, VariableStripping,
};
use crate::preprocess::weighted_lift::*;
use crate::tests::common::lit;
use crate::tests::pmc_oracle::brute_force_wmc;
fn w(text: &str) -> BigRational {
parse_weight(text).expect("an exact rational")
}
fn bare(num_vars: u32) -> CnfFormula {
CnfFormula {
num_vars,
clauses: Vec::new(),
}
}
fn record(original: CnfFormula) -> SimplifiedFormula {
SimplifiedFormula {
original,
equiv_reduced: None,
dve_reduced: None,
preprocessed: None,
stripped: None,
telemetry: SimplifyTelemetry::default(),
decision_trace: None,
}
}
fn weight_of(table: &Weights<Original>, var: u32, positive: bool) -> BigRational {
let (wn, wp) = &table[VarId(var)];
if positive { wp.clone() } else { wn.clone() }
}
fn stripped_and_reduced() -> SimplifiedFormula {
SimplifiedFormula {
original: CnfFormula {
num_vars: 5,
clauses: vec![
Clause::new(vec![lit(0, true)]),
Clause::new(vec![lit(2, true), lit(3, false)]),
Clause::new(vec![lit(2, false), lit(3, true)]),
Clause::new(vec![lit(2, true), lit(4, true)]),
],
},
stripped: Some(Stripped {
formula: CnfFormula {
num_vars: 3,
clauses: vec![
Clause::new(vec![lit(0, true), lit(1, false)]),
Clause::new(vec![lit(0, false), lit(1, true)]),
Clause::new(vec![lit(0, true), lit(2, true)]),
],
},
removed: VariableStripping {
backbone: vec![(VarId(0), true)],
dead: vec![VarId(1)],
renumbering: Renumber::of_kept(5, [VarId(2), VarId(3), VarId(4)]),
},
}),
equiv_reduced: Some(EquivReduction {
formula: CnfFormula {
num_vars: 2,
clauses: vec![Clause::new(vec![lit(0, true), lit(1, true)])],
},
mapping: EquivMapping {
var_to_rep: vec![lit(0, true), lit(0, true), lit(2, true)],
rep_to_equivs: HashMap::from([(VarId(0), vec![lit(1, true)])]),
representatives: vec![VarId(0), VarId(2)],
},
renumbering: Renumber::of_kept(3, [VarId(0), VarId(2)]),
}),
dve_reduced: None,
preprocessed: None,
telemetry: SimplifyTelemetry::default(),
decision_trace: None,
}
}
fn five_var_weights() -> Weights<Original> {
Weights::<Original>::from_dimacs_pairs(
&[
(1, w("2")),
(-1, w("3")),
(2, w("7")),
(-2, w("5")),
(3, w("3")),
(-3, w("2")),
(4, w("11")),
(-4, w("5")),
(5, w("5")),
(-5, w("2")),
],
5,
)
}
#[test]
fn the_weighted_lift_reproduces_the_original_weighted_count() {
let simplified = stripped_and_reduced();
let orig_w = five_var_weights();
let folded = folded_weights(&simplified, &orig_w);
let reduced_wmc = brute_force_wmc(simplified.reduced_formula(), |v, positive| {
let original = simplified.reduced_var_to_original(v as usize);
weight_of(&folded, original as u32, positive)
});
let original_wmc = brute_force_wmc(&simplified.original, |v, positive| {
weight_of(&orig_w, v, positive)
});
assert_eq!(
reduced_wmc * weighted_lift(&simplified, &orig_w, &folded),
original_wmc,
"the lifted weighted count is not the one the original formula has",
);
}
#[test]
fn a_backbone_literal_costs_the_weight_of_its_own_polarity() {
let mut simplified = record(bare(2));
simplified.stripped = Some(Stripped {
formula: bare(0),
removed: VariableStripping {
backbone: vec![(VarId(0), true), (VarId(1), false)],
dead: Vec::new(),
renumbering: Renumber::of_kept(2, []),
},
});
let orig_w = Weights::<Original>::from_dimacs_pairs(
&[(1, w("2")), (-1, w("3")), (2, w("7")), (-2, w("5"))],
2,
);
assert_eq!(
stripped_correction(&simplified, &orig_w),
w("10"),
"each backbone literal must be charged the weight of the value it takes",
);
}
#[test]
fn a_dead_variable_costs_the_sum_of_its_two_weights() {
let mut simplified = record(bare(2));
simplified.stripped = Some(Stripped {
formula: bare(0),
removed: VariableStripping {
backbone: Vec::new(),
dead: vec![VarId(0), VarId(1)],
renumbering: Renumber::of_kept(2, []),
},
});
let orig_w = Weights::<Original>::from_dimacs_pairs(
&[(1, w("2")), (-1, w("3")), (2, w("7")), (-2, w("5"))],
2,
);
assert_eq!(
stripped_correction(&simplified, &orig_w),
w("60"),
"an unconstrained variable costs both of its weights, not one",
);
}
fn with_equivalence(partner: Literal) -> SimplifiedFormula {
let mut simplified = record(bare(3));
simplified.equiv_reduced = Some(EquivReduction {
formula: bare(2),
mapping: EquivMapping {
var_to_rep: vec![
Literal::pos(VarId(0)),
Literal::new(VarId(0), partner.positive),
Literal::pos(VarId(2)),
],
rep_to_equivs: HashMap::from([(VarId(0), vec![partner])]),
representatives: vec![VarId(0), VarId(2)],
},
renumbering: Renumber::of_kept(3, [VarId(0), VarId(2)]),
});
simplified
}
fn three_var_weights() -> Weights<Original> {
Weights::<Original>::from_dimacs_pairs(
&[(1, w("3")), (-1, w("2")), (2, w("11")), (-2, w("5"))],
3,
)
}
#[test]
fn an_equivalence_partner_multiplies_into_its_representative_rather_than_the_scalar() {
let simplified = with_equivalence(Literal::pos(VarId(1)));
let orig_w = three_var_weights();
let folded = folded_weights(&simplified, &orig_w);
assert_eq!(
folded.as_pairs()[0],
(w("10"), w("33")),
"the partner's weights must land on its representative",
);
assert_eq!(
stripped_correction(&simplified, &orig_w),
BigRational::one(),
"a folded partner must not also be charged as a scalar",
);
}
#[test]
fn an_anti_equivalent_partner_folds_with_its_polarities_swapped() {
let simplified = with_equivalence(Literal::neg(VarId(1)));
let orig_w = three_var_weights();
let folded = folded_weights(&simplified, &orig_w);
assert_eq!(
folded.as_pairs()[0],
(w("22"), w("15")),
"an anti-equivalent partner's weights must fold crosswise",
);
}
fn with_dve(fates: Vec<DveFate>) -> SimplifiedFormula {
let survivors: Vec<VarId> = fates
.iter()
.enumerate()
.filter(|(_, fate)| **fate == DveFate::Kept)
.map(|(j, _)| VarId(j as u32))
.collect();
let mut simplified = record(bare(fates.len() as u32));
simplified.dve_reduced = Some(DveReduction {
formula: bare(survivors.len() as u32),
renumbering: Renumber::of_kept(fates.len(), survivors),
fates,
});
simplified
}
#[test]
fn a_defined_variable_with_unequal_weights_makes_the_whole_stage_unsupported() {
let simplified = with_dve(vec![DveFate::Kept, DveFate::Defined]);
let equal = Weights::<Original>::from_dimacs_pairs(&[(2, w("7")), (-2, w("7"))], 2);
let unequal = Weights::<Original>::from_dimacs_pairs(&[(2, w("7")), (-2, w("5"))], 2);
assert_eq!(
dve_eligibility(&simplified, &equal),
DveEligibility::Supported {
defined: 1,
free: 0,
},
"an equal-weight definition costs that one weight and is payable",
);
assert_eq!(
dve_eligibility(&simplified, &unequal),
DveEligibility::Unsupported,
"a definition whose value decides its weight has no scalar to charge",
);
}
#[test]
fn an_equivalence_chain_ending_at_an_eliminated_variable_is_unsupported() {
let landed = vec![
DveFate::Kept,
DveFate::Equiv {
rep: Literal::pos(VarId(0)),
},
];
let stranded = vec![
DveFate::Free,
DveFate::Equiv {
rep: Literal::pos(VarId(0)),
},
];
let uniform = Weights::<Original>::from_dimacs_pairs(&[], 2);
assert_eq!(
dve_equiv_survivor(&landed, 1),
Some(Literal::pos(VarId(0))),
"a chain ending at a surviving variable folds onto it",
);
assert_eq!(
dve_equiv_survivor(&stranded, 1),
None,
"a chain ending at an eliminated variable has no survivor",
);
assert_eq!(
dve_eligibility(&with_dve(stranded), &uniform),
DveEligibility::Unsupported,
"the stranded chain must take the whole stage down with it",
);
}
#[test]
fn a_chain_of_equivalences_composes_its_polarities() {
let fates = [
DveFate::Kept,
DveFate::Equiv {
rep: Literal::neg(VarId(0)),
},
DveFate::Equiv {
rep: Literal::neg(VarId(1)),
},
DveFate::Equiv {
rep: Literal::pos(VarId(2)),
},
];
assert_eq!(
dve_equiv_survivor(&fates, 1),
Some(Literal::neg(VarId(0))),
"one hop keeps the hop's own polarity",
);
assert_eq!(
dve_equiv_survivor(&fates, 2),
Some(Literal::pos(VarId(0))),
"v2 ≡ ¬v1 and v1 ≡ ¬v0, so v2 ≡ v0",
);
assert_eq!(
dve_equiv_survivor(&fates, 3),
Some(Literal::pos(VarId(0))),
"v3 ≡ v2 ≡ v0",
);
}
#[test]
fn a_residual_left_by_an_unfrozen_run_is_reverted_and_says_why() {
let simplified = with_dve(vec![DveFate::Kept, DveFate::Kept, DveFate::Free]);
let uniform = Weights::<Original>::from_dimacs_pairs(&[], 3);
let verdict = dve_verdict(&simplified, &uniform, false);
let DveVerdict::Revert(reason) = verdict else {
panic!("a residual formula must not be kept without the freeze: {verdict:?}");
};
assert!(
reason.contains("residual"),
"the reason must name what was left behind: {reason}",
);
assert_eq!(
dve_verdict(&simplified, &uniform, true),
DveVerdict::Keep {
defined: 0,
free: 1,
residual: 2,
},
"the freeze is what makes a residual worth keeping",
);
}