use crate::cnf::Clause;
use crate::cnf::CnfFormula;
use crate::cnf::{Reduced, ShowSet};
use crate::score::*;
use crate::tests::common::lit;
use crate::tests::score_fixture::{fixture_formula, fixture_vtree};
use crate::vtree::VarId;
#[test]
fn fixture_metrics_match_hand_computation() {
let formula = fixture_formula();
let vtree = fixture_vtree();
let show = ShowSet::<Reduced>::from_zero_based([0, 2]).mask(4);
let stats = VtreeScores::compute(&vtree, &formula, Some(&show)).expect("covering vtree");
assert_eq!(stats.max_clause_load, 2, "max_clause_load");
assert!(
(stats.clause_load_stddev - (1.0f64 / 3.0).sqrt()).abs() < 1e-12,
"clause_load_stddev: {}",
stats.clause_load_stddev
);
assert_eq!(stats.peak_context_width_all, 2, "peak_context_width_all");
assert_eq!(
stats.peak_context_width_show,
Some(1),
"peak_context_width_show"
);
let expected_cost = 4.8 + 9.0 * 18.0f64.log2() / 5.0 + 3.0 * 6.0f64.log2() / 40.0;
assert!(
(stats.cost - expected_cost).abs() < 1e-12,
"cost: {}",
stats.cost
);
}
#[test]
fn scoring_a_formula_the_vtree_does_not_cover_is_a_mismatch() {
let wider = CnfFormula {
num_vars: 6,
clauses: vec![Clause::new(vec![lit(0, true), lit(5, true)])],
};
let vtree = fixture_vtree();
assert!(matches!(
VtreeScores::compute(&vtree, &wider, None),
Err(crate::error::VitriError::Mismatch { .. })
));
assert!(matches!(
vtree_cost(&vtree, &wider),
Err(crate::error::VitriError::Mismatch { .. })
));
}
#[test]
fn a_formula_declaring_more_variables_than_it_uses_still_scores() {
let mut wider = fixture_formula();
wider.num_vars = 9;
wider
.clauses
.push(Clause::new(vec![lit(1, true), lit(3, true)]));
let vtree = fixture_vtree();
VtreeScores::compute(&vtree, &wider, None).expect("no clause names a variable the vtree lacks");
vtree_cost(&vtree, &wider).expect("no clause names a variable the vtree lacks");
}
#[test]
fn the_mismatch_message_names_the_dimacs_variable_the_vtree_lacks() {
let wider = CnfFormula {
num_vars: 6,
clauses: vec![Clause::new(vec![lit(0, true), lit(5, true)])],
};
let err = VtreeScores::compute(&fixture_vtree(), &wider, None)
.map(|_| ())
.expect_err("the vtree has no leaf for that variable")
.to_string();
assert!(
err.contains('6'),
"the message must name the offending variable as the file spells it (6), got: {err}",
);
assert!(
err.contains('4'),
"the message must name how many variables the vtree indexes (4), got: {err}",
);
}
#[test]
fn an_empty_clause_contributes_to_no_score() {
let formula = fixture_formula();
let mut with_empty = formula.clone();
with_empty.clauses.push(Clause::new(Vec::new()));
let vtree = fixture_vtree();
let show = ShowSet::<Reduced>::from_zero_based([0, 2]).mask(4);
assert_eq!(
VtreeScores::compute(&vtree, &with_empty, Some(&show)).expect("covering vtree"),
VtreeScores::compute(&vtree, &formula, Some(&show)).expect("covering vtree"),
);
}
#[test]
fn a_unit_clause_loads_its_own_leaf_but_crosses_no_cut() {
let formula = fixture_formula();
let mut with_unit = formula.clone();
with_unit.clauses.push(Clause::new(vec![lit(0, true)]));
let vtree = fixture_vtree();
let leaf = vtree.leaf_of(VarId(0));
let before = vtree_clause_load_per_node(&vtree, &formula);
let after = vtree_clause_load_per_node(&vtree, &with_unit);
for node in 0..vtree.num_nodes() {
let expected = before[node] + u32::from(node == leaf.idx());
assert_eq!(
after[node], expected,
"node {node} carries the unit clause only if it is that variable's leaf",
);
}
assert_eq!(
VtreeScores::compute(&vtree, &with_unit, None)
.expect("covering vtree")
.peak_context_width_all,
VtreeScores::compute(&vtree, &formula, None)
.expect("covering vtree")
.peak_context_width_all,
"a unit clause ties no two variables together, so it widens no cut",
);
let only_unit = CnfFormula {
num_vars: 4,
clauses: vec![Clause::new(vec![lit(0, true)])],
};
assert_eq!(
vtree_cost(&vtree, &only_unit).expect("covering vtree"),
0.0,
"a formula with no crossing clause has zero cost",
);
}
#[test]
fn a_formula_with_no_clauses_scores_zero_in_every_metric() {
let empty = CnfFormula {
num_vars: 4,
clauses: Vec::new(),
};
let show = ShowSet::<Reduced>::from_zero_based([0, 2]).mask(4);
let scores = VtreeScores::compute(&fixture_vtree(), &empty, Some(&show)).expect("covering");
assert!(
!scores.clause_load_stddev.is_nan(),
"an empty formula must not score NaN",
);
assert_eq!(
scores,
VtreeScores {
clause_load_stddev: 0.0,
max_clause_load: 0,
peak_context_width_all: 0,
peak_context_width_show: Some(0),
cost: 0.0,
},
);
}
#[test]
fn an_all_hidden_show_mask_reports_a_zero_show_peak() {
let formula = fixture_formula();
let vtree = fixture_vtree();
let none_shown = ShowSet::<Reduced>::empty().mask(4);
assert_eq!(
VtreeScores::compute(&vtree, &formula, Some(&none_shown))
.expect("covering vtree")
.peak_context_width_show,
Some(0),
"a mask that keeps nothing is a projection onto nothing, not an absent mask",
);
assert_eq!(
VtreeScores::compute(&vtree, &formula, None)
.expect("covering vtree")
.peak_context_width_show,
None,
"the contrast: no mask at all is the non-projected reading",
);
let short = ShowSet::<Reduced>::from_zero_based([0]).mask(1);
assert_eq!(
VtreeScores::compute(&vtree, &formula, Some(&short))
.expect("covering vtree")
.peak_context_width_show,
Some(1),
"a mask shorter than the variable space hides the ids it omits",
);
}
mod structure_profile {
use crate::cnf::CnfFormula;
use crate::preprocess::ArjunSbva;
use crate::preprocess::arjun::arjun_sbva_skip;
use crate::score::StructureProfile;
fn formula(text: &str) -> CnfFormula {
CnfFormula::from_dimacs(text.as_bytes())
.expect("the fixture is well-formed DIMACS")
.0
}
fn uniform() -> CnfFormula {
formula("p cnf 4 4\n1 2 0\n2 3 0\n3 4 0\n4 1 0\n")
}
fn skewed() -> CnfFormula {
formula("p cnf 8 4\n1 2 0\n1 3 0\n1 4 5 6 7 8 0\n1 2 0\n")
}
#[test]
fn a_formula_with_no_spread_at_all_measures_as_uniform() {
let profile = StructureProfile::measure(&uniform());
assert_eq!(profile.clause_width_cv, 0.0);
assert_eq!(profile.var_occurrence_cv, 0.0);
assert!(
profile.coloring_like,
"a formula with no dispersion is the extreme case of near-uniform",
);
}
#[test]
fn a_formula_with_one_hub_variable_and_one_wide_clause_measures_as_skewed() {
let profile = StructureProfile::measure(&skewed());
assert!(profile.clause_width_cv > 0.0);
assert!(profile.var_occurrence_cv > 0.0);
assert!(
!profile.coloring_like,
"widths of 2, 2, 6, 2 are not a near-uniform spread, got {profile:?}",
);
}
#[test]
fn a_formula_with_a_single_clause_has_no_dispersion_to_report() {
let profile = StructureProfile::measure(&formula("p cnf 3 1\n1 2 3 0\n"));
assert_eq!(profile.clause_width_cv, 0.0);
assert_eq!(profile.var_occurrence_cv, 0.0);
}
#[test]
fn the_verdict_a_caller_reads_is_the_one_the_sbva_policy_acts_on() {
for fixture in [uniform(), skewed()] {
assert_eq!(
arjun_sbva_skip(&fixture, ArjunSbva::Auto),
StructureProfile::measure(&fixture).coloring_like,
"the policy and the published profile disagree about {fixture:?}",
);
}
}
}