use crate::candidates::CandidateRankMetric;
use crate::cnf::CnfFormula;
use crate::decompose::goatd::candidate_param;
use crate::decompose::{
BagMetadata, ConversionRequest, FcBudget, GraphKind, Reading, TdConversion, WallCapMode,
convert_td,
};
use crate::diagnostics::diag;
use crate::score::StructureProfile;
use crate::score::agg::{AggModel, AggScore, agg_score};
use crate::score::{VtreeScores, vtree_max_clause_load};
use crate::vtree::Vtree;
use std::sync::Arc;
pub(super) const PORTFOLIO_HEAVY_MAX_VARS: u32 = 500_000;
pub(super) struct ScoredCandidate {
pub(super) sel_metric: f64,
pub(super) stats: VtreeScores,
pub(super) agg: Option<AggScore>,
pub(super) name: &'static str,
pub(super) param: Option<&'static str>,
pub(super) vtree: Arc<Vtree>,
pub(super) meta: Option<Arc<BagMetadata>>,
}
pub(super) struct Incumbent {
pub(super) scores: Option<VtreeScores>,
pub(super) stddev: f64,
pub(super) cost: f64,
pub(super) vtree: Option<Arc<Vtree>>,
pub(super) meta: Option<Arc<BagMetadata>>,
pub(super) name: &'static str,
pub(super) param: Option<&'static str>,
}
impl Default for Incumbent {
fn default() -> Self {
Incumbent {
scores: None,
stddev: f64::MAX,
cost: f64::MAX,
vtree: None,
meta: None,
name: "none",
param: None,
}
}
}
impl Incumbent {
pub(super) fn adopt(
&mut self,
stats: &VtreeScores,
vtree: Arc<Vtree>,
meta: Option<Arc<BagMetadata>>,
name: &'static str,
param: Option<&'static str>,
) {
*self = Incumbent {
scores: Some(*stats),
stddev: stats.clause_load_stddev,
cost: stats.cost,
vtree: Some(vtree),
meta,
name,
param,
};
}
}
pub(super) struct TraceRow {
pub(super) family: &'static str,
pub(super) param: String,
pub(super) stddev: f64,
pub(super) mcl: u32,
pub(super) peak_context_width_all: u32,
pub(super) cost: f64,
pub(super) built: bool,
}
impl TraceRow {
pub(super) fn from_scores(
family: &'static str,
param: String,
scores: &VtreeScores,
built: bool,
) -> Self {
Self {
family,
param,
stddev: scores.clause_load_stddev,
mcl: scores.max_clause_load,
peak_context_width_all: scores.peak_context_width_all,
cost: scores.cost,
built,
}
}
}
pub(super) struct CatalogEntry {
pub(super) name: &'static str,
pub(super) param: Option<&'static str>,
pub(super) td_based: bool,
pub(super) gate: Gate,
pub(super) offers: u32,
pub(super) build: fn(&Inputs, &mut RunState) -> Vec<TdConversion>,
}
impl CatalogEntry {
pub(super) fn published_specs(&self) -> impl Iterator<Item = String> + '_ {
(0..self.offers as usize)
.map(|index| candidate_spec(self.name, candidate_param(index).or(self.param)))
}
}
pub(super) fn work_ms_since(start: std::time::Instant) -> u64 {
crate::decompose::meter::now()
.saturating_duration_since(start)
.as_millis() as u64
}
pub(super) fn outspent(remaining_ms: Option<i64>, was: Option<u64>) -> bool {
remaining_ms
.zip(was)
.is_some_and(|(left, was)| left > 0 && (left as u64) <= was)
}
pub(super) fn candidate_spec(name: &str, param: Option<&str>) -> String {
crate::spec::spec_string(name, param)
}
pub(super) enum Gate {
Always,
FromInputs(fn(&Inputs) -> bool),
FromDerived(fn(&Inputs, &Derived) -> bool),
}
pub(super) struct Inputs<'a> {
pub(super) formula: &'a CnfFormula,
pub(super) source_profile: Option<StructureProfile>,
pub(super) seed: u64,
pub(super) peak_mode: bool,
pub(super) show_mask: Option<&'a crate::cnf::ShowMask>,
pub(super) trace: bool,
pub(super) flowcutter_cap_ms: Option<i64>,
pub(super) t_build: std::time::Instant,
pub(super) deadline: Option<std::time::Instant>,
pub(super) candidate_capacity: usize,
pub(super) peak_tolerance: f64,
pub(super) goatd: crate::decompose::goatd::GoatdKnobs,
pub(super) rank_metric: CandidateRankMetric,
pub(super) effort_scale: f64,
pub(super) reading: Reading,
pub(super) conversion_trace: bool,
pub(super) prefer: Option<&'a super::CandidatePreference>,
pub(super) score_agg: Option<&'a AggModel>,
}
impl<'a> Inputs<'a> {
pub(super) fn conversion(&self, spec: &'static str) -> ConversionRequest<'static> {
ConversionRequest {
spec: Some(spec),
reading: self.reading,
effort_scale: self.effort_scale,
deadline: self.deadline,
real_deadline: None,
trace: self.conversion_trace,
}
}
pub(super) fn prefers(&self, entry: &CatalogEntry, index: usize) -> bool {
self.prefer.is_some_and(|p| {
(index == 0 && p.name() == entry.name)
|| p.name() == candidate_spec(entry.name, candidate_param(index).or(entry.param))
})
}
}
pub(super) struct RunState {
pub(super) reduced_steps: i64,
pub(super) iters: i32,
pub(super) cand_cap_ms: Option<i64>,
pub(super) cand_wall_ms: Option<i64>,
pub(super) behind_schedule: bool,
pub(super) flowcutter_incidence_td_cache: Option<crate::decompose::TreeDecomposition>,
pub(super) best: Incumbent,
pub(super) trace_rows: Vec<TraceRow>,
pub(super) cands: Vec<ScoredCandidate>,
pub(super) hypergraph_bisect_040_built: bool,
pub(super) preferred: Option<ScoredCandidate>,
}
pub(super) struct Derived {
pub(super) coloring_like: bool,
pub(super) hypergraph_bisect_gen_gate: bool,
}
impl Derived {
pub(super) fn compute(inp: &Inputs, run: &RunState) -> Derived {
let formula = inp.formula;
let num_vars = inp.num_vars();
let coloring_like = if num_vars <= PORTFOLIO_HEAVY_MAX_VARS {
let profile = StructureProfile::measure(formula);
let coloring_like = coloring_like_for_selection(profile, inp.source_profile);
if inp.trace {
diag!(
"[coloring] occ_cv={:.4} width_cv={:.4} source_width_cv={} \
coloring_like={} num_vars={num_vars}",
profile.var_occurrence_cv,
profile.clause_width_cv,
inp.source_profile
.map(|p| format!("{:.4}", p.clause_width_cv))
.unwrap_or_else(|| "none".to_owned()),
coloring_like as u8,
);
}
coloring_like
} else {
false
};
let best_mcl = run
.best
.vtree
.as_ref()
.map(|v| vtree_max_clause_load(v, formula));
Derived {
coloring_like,
hypergraph_bisect_gen_gate: best_mcl.is_none_or(|mcl| mcl > formula.num_vars / 5),
}
}
}
pub(super) fn coloring_like_for_selection(
built: StructureProfile,
source: Option<StructureProfile>,
) -> bool {
built.coloring_like
|| source.is_some_and(|source| {
crate::cnf::stats::coloring_like_predicate(
built.var_occurrence_cv,
source.clause_width_cv,
)
})
}
impl Inputs<'_> {
pub(super) fn num_vars(&self) -> u32 {
self.formula.num_vars
}
fn cap_tripped(&self) -> bool {
self.flowcutter_cap_ms
.is_some_and(|cap| (work_ms_since(self.t_build) as i64) > cap)
}
pub(super) fn remaining_ms(&self) -> Option<i64> {
self.deadline
.map(|d| i64::try_from(crate::budget::remaining(d).as_millis()).unwrap_or(i64::MAX))
}
pub(super) fn out_of_time(&self) -> bool {
self.remaining_ms().is_some_and(|r| r <= 0)
}
pub(super) fn fair_share_ms(&self, n_remaining: usize) -> Option<i64> {
self.remaining_ms()
.map(|r| (r / n_remaining.max(1) as i64).max(1))
}
}
impl RunState {
pub(super) fn new(reduced_steps: i64, iters: i32) -> RunState {
RunState {
reduced_steps,
iters,
cand_cap_ms: None,
cand_wall_ms: None,
behind_schedule: false,
flowcutter_incidence_td_cache: None,
best: Incumbent::default(),
trace_rows: Vec::new(),
cands: Vec::new(),
hypergraph_bisect_040_built: false,
preferred: None,
}
}
pub(super) fn fc_time_cap_ms(&self, inp: &Inputs) -> Option<i64> {
let share = if self.behind_schedule {
self.cand_cap_ms
} else {
None
};
[self.cand_wall_ms, share, inp.flowcutter_cap_ms]
.into_iter()
.flatten()
.min()
}
pub(super) fn fc_cap_mode(&self, inp: &Inputs) -> WallCapMode {
if self.behind_schedule || inp.flowcutter_cap_ms.is_some() {
WallCapMode::Tight
} else {
WallCapMode::BoundOnly
}
}
fn fc_budget(&self, inp: &Inputs) -> FcBudget {
match self.fc_time_cap_ms(inp) {
None => FcBudget::Steps {
steps: self.reduced_steps,
iters: self.iters,
},
Some(timeout_ms) => FcBudget::Timed {
timeout_ms,
patience_ms: 0,
iters: self.iters,
steps: self.reduced_steps,
cap_mode: self.fc_cap_mode(inp),
},
}
}
fn goatd_budget_ms(&self) -> Option<u64> {
self.cand_cap_ms.map(|cap| cap as u64)
}
pub(super) fn fold(
&mut self,
inp: &Inputs,
entry: &CatalogEntry,
index: usize,
built: TdConversion,
) {
let TdConversion { vtree, td } = built;
let param = candidate_param(index).or(entry.param);
let meta = if entry.td_based { td.meta } else { None };
let formula = inp.formula;
let (stats, agg) = if let Some(model) = inp.score_agg {
let (stats, score) = agg_score(&vtree, formula, model, inp.show_mask)
.expect(crate::score::BUILT_FROM_THIS_FORMULA);
(stats, Some(score))
} else {
(
VtreeScores::compute(&vtree, formula, inp.show_mask)
.expect(crate::score::BUILT_FROM_THIS_FORMULA),
None,
)
};
let sel_metric = inp.rank_metric.value(&stats);
if inp.trace && entry.td_based {
diag!(
"[portfolio] cand {:18} stddev={:8.2} peak_ctx={:5} peak_context_width_show={:>5} cost={:.2}",
candidate_spec(entry.name, candidate_param(index)),
stats.clause_load_stddev,
stats.peak_context_width_all,
stats
.peak_context_width_show
.map(|s| s.to_string())
.unwrap_or_else(|| "-".to_string()),
stats.cost,
);
}
if self.preferred.is_none() && inp.prefers(entry, index) {
self.preferred = Some(ScoredCandidate {
sel_metric,
stats,
agg: agg.clone(),
name: entry.name,
param,
vtree: Arc::clone(&vtree),
meta: meta.clone(),
});
}
if inp.peak_mode || inp.candidate_capacity > 1 || inp.score_agg.is_some() {
self.cands.push(ScoredCandidate {
sel_metric,
stats,
agg,
name: entry.name,
param,
vtree: Arc::clone(&vtree),
meta: meta.clone(),
});
}
if !inp.peak_mode {
if inp.trace {
self.trace_rows.push(TraceRow::from_scores(
entry.name,
param.unwrap_or("-").to_string(),
&stats,
true,
));
if entry.name == "hypergraph-bisect" && entry.param == Some("imbalance=0.40") {
self.hypergraph_bisect_040_built = true;
}
}
if stats.cost < self.best.cost {
self.best.adopt(&stats, vtree, meta, entry.name, param);
}
}
}
}
pub(super) fn build_fc_inc(inp: &Inputs, run: &mut RunState) -> Vec<TdConversion> {
let formula = inp.formula;
run.flowcutter_incidence_td_cache = crate::decompose::flowcutter::flowcutter_td(
formula,
GraphKind::Incidence,
run.fc_budget(inp),
)
.ok();
let vtree = run
.flowcutter_incidence_td_cache
.as_ref()
.map(|td| convert_td(formula, td, inp.conversion("flowcutter-incidence")));
if inp.num_vars() > PORTFOLIO_HEAVY_MAX_VARS {
run.flowcutter_incidence_td_cache = None;
}
vtree.into_iter().collect()
}
pub(super) fn build_fc_pri(inp: &Inputs, run: &mut RunState) -> Vec<TdConversion> {
let formula = inp.formula;
crate::decompose::flowcutter::flowcutter_td(formula, GraphKind::Primal, run.fc_budget(inp))
.ok()
.map(|td| convert_td(formula, &td, inp.conversion("flowcutter-primal")))
.into_iter()
.collect()
}
pub(super) fn gate_goatd(inp: &Inputs) -> bool {
if !inp.cap_tripped() {
true
} else {
if inp.trace {
diag!(
"[portfolio] cap tripped ({}ms) \u{2192} skip goatd",
work_ms_since(inp.t_build)
);
}
false
}
}
pub(super) fn build_goatd(inp: &Inputs, run: &mut RunState) -> Vec<TdConversion> {
crate::decompose::goatd::vtrees_from_goatd_refined(
inp.formula,
crate::decompose::GraphKind::Incidence,
inp.seed,
run.goatd_budget_ms(),
inp.goatd,
inp.trace,
inp.conversion("goatd-incidence"),
)
.unwrap_or_default()
}
pub(super) fn build_goatd_primal(inp: &Inputs, run: &mut RunState) -> Vec<TdConversion> {
crate::decompose::goatd::vtrees_from_goatd_refined(
inp.formula,
crate::decompose::GraphKind::Primal,
inp.seed,
run.goatd_budget_ms(),
inp.goatd,
inp.trace,
inp.conversion("goatd-primal"),
)
.unwrap_or_default()
}
pub(super) fn gate_force(inp: &Inputs) -> bool {
inp.num_vars() <= PORTFOLIO_HEAVY_MAX_VARS && !inp.cap_tripped()
}
pub(super) fn build_force(inp: &Inputs, _run: &mut RunState) -> Vec<TdConversion> {
let cfg = crate::decompose::ForceConfig::new(crate::decompose::ForceMode::Mst);
crate::decompose::vtree_from_force(inp.formula, cfg)
.ok()
.map(TdConversion::bare)
.into_iter()
.collect()
}
pub(super) fn gate_hypergraph_bisect(inp: &Inputs, derived: &Derived) -> bool {
inp.num_vars() <= PORTFOLIO_HEAVY_MAX_VARS
&& derived.coloring_like
&& (inp.peak_mode || derived.hypergraph_bisect_gen_gate)
}
pub(super) fn build_hypergraph_bisect(inp: &Inputs, _run: &mut RunState) -> Vec<TdConversion> {
let dials = crate::decompose::BisectDials {
imbalance: crate::decompose::multilevel_hg_bisect::IMBALANCE_PORTFOLIO_RELAXED,
base_seed: 0,
effort_scale: inp.effort_scale,
};
crate::decompose::multilevel_hg_bisect::vtree_from_hg_bisect(inp.formula, dials)
.ok()
.map(TdConversion::bare)
.into_iter()
.collect()
}
pub(super) fn gate_guided_bisect(inp: &Inputs, derived: &Derived) -> bool {
derived.coloring_like && inp.num_vars() <= PORTFOLIO_HEAVY_MAX_VARS
}
pub(super) fn build_guided_bisect(inp: &Inputs, run: &mut RunState) -> Vec<TdConversion> {
run.flowcutter_incidence_td_cache
.as_ref()
.and_then(|td| {
crate::decompose::guided_bisect_from_incidence_td(
inp.formula,
td,
inp.conversion("guided-bisect"),
)
.ok()
})
.into_iter()
.collect()
}