use std::sync::Arc;
use std::time::Instant;
use crate::diagnostics::diag;
use crate::score::{BUILT_FROM_THIS_FORMULA, vtree_cost};
use crate::vtree::Vtree;
use super::super::TreeDecomposition;
use super::super::best::BestBy;
use super::algo::{ConversionInput, Converter, root_bags};
use super::meta::BagMetadata;
use super::reading::{BINARIZATIONS, Binarization, FixedReading, PLACES, Reading, Root, RootPick};
const ROOT_CAP: usize = 20;
const UNSCORED_BINARIZATION: Binarization = Binarization::Balanced;
const SCREENED_ROOTS: usize = 3;
#[derive(Clone, Default)]
pub(crate) struct TdConversionMeta {
pub meta: Option<Arc<BagMetadata>>,
}
#[derive(Clone, Copy)]
pub(crate) struct ConversionRequest<'a> {
pub spec: Option<&'a str>,
pub reading: Reading,
pub effort_scale: f64,
pub deadline: Option<Instant>,
pub real_deadline: Option<Instant>,
pub trace: bool,
}
impl<'a> ConversionRequest<'a> {
pub(crate) fn open(reading: Reading, deadline: Option<Instant>) -> ConversionRequest<'static> {
ConversionRequest {
spec: None,
reading,
effort_scale: 1.0,
deadline,
real_deadline: None,
trace: false,
}
}
pub(crate) fn nested(&self) -> ConversionRequest<'a> {
ConversionRequest {
spec: None,
trace: false,
..*self
}
}
}
struct ConversionReport {
winner: FixedReading,
cost: Option<f64>,
done: usize,
planned: usize,
}
impl ConversionReport {
fn emit(&self, spec: &str) {
diag!(
"[conversion] {spec}: {} cost={} readings={}/{}",
self.winner,
self.cost
.map_or_else(|| "-".to_string(), |cost| format!("{cost:.2}")),
self.done,
self.planned,
);
}
}
pub(crate) fn convert(
input: ConversionInput<'_>,
request: ConversionRequest<'_>,
) -> (Vtree, TdConversionMeta) {
assert!(
!input.td.adjacency().is_empty(),
"convert: empty tree decomposition (num_vars={}); callers must short-circuit \
0-variable formulas before vtree construction",
input.num_vars,
);
let reading_units: u64 = if crate::decompose::meter::is_armed() {
input.num_vars as u64
+ input.td.adjacency().len() as u64
+ input.formula.map_or(0, |f| {
f.clauses
.iter()
.map(|c| c.literals.len() as u64)
.sum::<u64>()
})
} else {
0
};
let scored = input.formula.is_some();
let roots = candidate_roots(input.td, request.reading.root, scored);
let places = axis(request.reading.place, PLACES, scored, PLACES[0].1);
let binarizations = axis(
request.reading.binarize,
BINARIZATIONS,
scored,
UNSCORED_BINARIZATION,
);
let planned =
roots.len() + (places.len() * binarizations.len() - 1) * roots.len().min(SCREENED_ROOTS);
let mut search = Search {
converter: Converter::new(input),
request,
best: BestBy::new(),
winner: FixedReading {
root: roots[0],
place: places[0],
binarize: binarizations[0],
},
best_score: None,
done: 0,
reading_units,
};
let mut screened: Vec<(RootPick, f64)> = Vec::with_capacity(roots.len());
for &root in &roots {
let Some(score) = search.offer(FixedReading {
root,
place: places[0],
binarize: binarizations[0],
}) else {
break;
};
screened.push((root, score));
}
screened.sort_by(|a, b| a.1.total_cmp(&b.1));
screened.truncate(SCREENED_ROOTS);
'pairs: for &place in &places {
for &binarize in &binarizations {
if (place, binarize) == (places[0], binarizations[0]) {
continue;
}
for &(root, _) in &screened {
if search
.offer(FixedReading {
root,
place,
binarize,
})
.is_none()
{
break 'pairs;
}
}
}
}
let report = ConversionReport {
winner: search.winner,
cost: search.best_score.filter(|_| scored),
done: search.done,
planned,
};
if let Some(spec) = request.spec {
report.emit(spec);
}
let (vtree, meta) = search.best.into_best().expect("at least one reading").0;
assert_eq!(
vtree.num_leaves(),
input.num_vars,
"the conversion produced a malformed vtree: {} leaves for a {}-variable formula",
vtree.num_leaves(),
input.num_vars,
);
(
vtree,
TdConversionMeta {
meta: Some(Arc::new(meta)),
},
)
}
struct Search<'a, 'b> {
converter: Converter<'a>,
request: ConversionRequest<'b>,
best: BestBy<(Vtree, BagMetadata), f64>,
winner: FixedReading,
best_score: Option<f64>,
done: usize,
reading_units: u64,
}
impl Search<'_, '_> {
fn offer(&mut self, reading: FixedReading) -> Option<f64> {
if self.best.has_candidate()
&& (crate::budget::expired(self.request.deadline)
|| self
.request
.real_deadline
.is_some_and(|end| Instant::now() >= end))
{
return None;
}
crate::decompose::meter::charge(self.reading_units);
let started = Instant::now();
let built = self.converter.build(reading);
let score = self
.converter
.input
.formula
.map(|f| vtree_cost(&built.0, f).expect(BUILT_FROM_THIS_FORMULA))
.unwrap_or(0.0);
if self.request.trace
&& let Some(spec) = self.request.spec
{
diag!(
"[conversion] reading {spec} {reading} cost={score} ms={}",
started.elapsed().as_millis(),
);
}
if self.best_score.is_none_or(|best| score < best) {
self.winner = reading;
self.best_score = Some(score);
}
self.best.offer(built, score);
self.done += 1;
Some(score)
}
}
fn candidate_roots(td: &TreeDecomposition, named: Option<Root>, scored: bool) -> Vec<RootPick> {
let mut roots: Vec<RootPick> = Vec::new();
let mut taken: Vec<usize> = Vec::new();
if named != Some(Root::Leaf) {
roots.push(RootPick::First);
taken.extend(root_bags(td, RootPick::First));
}
if named.is_none() {
let centroid = root_bags(td, RootPick::Centroid);
if centroid != taken {
roots.push(RootPick::Centroid);
taken.extend(centroid);
}
}
if matches!(named, None | Some(Root::Leaf)) {
for bag in 0..td.adjacency().len() {
if roots.len() >= ROOT_CAP {
break;
}
if td.adjacency()[bag].len() == 1 && !taken.contains(&bag) {
roots.push(RootPick::Leaf(bag));
}
}
}
if named == Some(Root::Centroid) {
roots.push(RootPick::Centroid);
}
if roots.is_empty() {
roots.push(RootPick::First);
}
if !scored {
roots.truncate(1);
}
roots
}
fn axis<T: Copy>(
named: Option<T>,
table: &[(&'static str, T)],
scored: bool,
unscored: T,
) -> Vec<T> {
match named {
Some(v) => vec![v],
None if !scored => vec![unscored],
None => table.iter().map(|(_, v)| *v).collect(),
}
}