use std::sync::Arc;
use rustc_hash::FxHashMap;
use super::EMPTY_FORMULA;
use super::best::BestBy;
use crate::cnf::CnfFormula;
use crate::score::{clause_lca_nodes, load_stats};
use crate::vtree::{VarId, Vtree, VtreeArena, VtreeIdx};
mod geometry;
mod layout;
mod tree;
pub(crate) use geometry::*;
use layout::*;
use tree::*;
const SEED: u64 = 42;
const FORCE1D_ROUNDS: usize = 30;
const EPS: f64 = 1e-9;
pub(super) const PRIM_LIMIT: usize = 20_000;
const KNN_K: usize = 8;
const CO_CLAUSE_CAP: usize = 64;
pub(crate) const MAX_DIM: usize = 8;
const JACOBI_SWEEPS: usize = 30;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum ForceMode {
Mst,
Cut,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum RootRule {
Merge,
Balance,
Hybrid,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum OrientRule {
X,
Small,
Big,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum WeightRule {
Euclid,
Co,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum ClauseWeight {
Uniform,
Short,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum InitMode {
Rand,
Force1d,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) struct ForceConfig {
pub mode: ForceMode,
pub root: RootRule,
pub orient: OrientRule,
pub weight: WeightRule,
pub clause_weight: ClauseWeight,
pub dim: usize,
pub fb: u8,
pub seeds: u8,
pub init: InitMode,
}
impl ForceConfig {
pub(crate) fn new(mode: ForceMode) -> Self {
ForceConfig {
mode,
root: RootRule::Merge,
orient: OrientRule::X,
weight: WeightRule::Euclid,
clause_weight: ClauseWeight::Uniform,
dim: 2,
fb: 0,
seeds: 1,
init: InitMode::Rand,
}
}
}
pub(super) fn max_internal_load(vtree: &Vtree, loads: &[u32]) -> u32 {
let mut m = 0;
for (idx, &load) in loads.iter().enumerate() {
if load > 0 && !vtree.node(VtreeIdx(idx as u32)).is_leaf() {
m = m.max(load);
}
}
m
}
pub(crate) fn vtree_from_force(
formula: &CnfFormula,
cfg: ForceConfig,
) -> Result<Arc<Vtree>, String> {
let n = formula.num_vars as usize;
if n == 0 {
return Err(EMPTY_FORMULA.to_string());
}
debug_assert!(
(2..=MAX_DIM).contains(&cfg.dim),
"force dim out of range: {}",
cfg.dim
);
let d = cfg.dim;
let inc = build_incidence(formula);
let build_tree = |layout: &[Vec<f64>]| -> (VtreeArena, VtreeIdx) {
let mut nodes = VtreeArena::with_capacity(2 * n - 1);
let root = match cfg.mode {
ForceMode::Mst => mst_tree(layout, &cfg, &inc, &mut nodes),
ForceMode::Cut => {
let all: Vec<u32> = (0..n as u32).collect();
cut_tree(layout, d, &all, &mut nodes)
}
};
(nodes, root)
};
let build_for_seed = |seed: u64| -> (Arc<Vtree>, u32) {
let layout0 = force_layout(n, &inc, seed, &cfg, None, None);
let (nodes0, root0) = build_tree(&layout0);
let vtree0 = Arc::new(Vtree::from_nodes(
nodes0.into_nodes(),
root0,
formula.num_vars,
));
let (mut prev_lca, mut prev_loads) = clause_lca_nodes(&vtree0, formula);
let mut best: BestBy<Arc<Vtree>, u32> = BestBy::new();
best.offer(vtree0.clone(), max_internal_load(&vtree0, &prev_loads));
if cfg.fb == 0 {
return best.into_best().expect("the round-zero layout was offered");
}
let mut prev_vtree = vtree0;
let mut prev_layout = layout0;
let mut extra_w = vec![1.0f64; inc.nc];
for _ in 0..cfg.fb {
let stats = load_stats(&prev_loads, |t| !prev_vtree.node(t).is_leaf());
if stats.count > 0 && stats.mean > EPS {
let thresh = stats.mean + stats.stddev;
for (c, w) in extra_w.iter_mut().enumerate() {
let node = prev_lca[c];
if !prev_vtree.node(node).is_leaf() && (prev_loads[node.idx()] as f64) > thresh
{
let factor = ((prev_loads[node.idx()] as f64) / stats.mean).min(4.0);
*w *= factor;
}
}
}
let layout = force_layout(n, &inc, seed, &cfg, Some(&extra_w), Some(&prev_layout));
let (nodes, root) = build_tree(&layout);
let vtree = Arc::new(Vtree::from_nodes(
nodes.into_nodes(),
root,
formula.num_vars,
));
let (lca, loads) = clause_lca_nodes(&vtree, formula);
best.offer(vtree.clone(), max_internal_load(&vtree, &loads));
prev_lca = lca;
prev_loads = loads;
prev_vtree = vtree;
prev_layout = layout;
}
best.into_best().expect("the round-zero layout was offered")
};
let mut best: BestBy<Arc<Vtree>, u32> = BestBy::new();
for i in 0..(cfg.seeds as u64).max(1) {
let (vtree, load) = build_for_seed(SEED + i);
best.offer(vtree, load);
}
Ok(best
.into_best()
.map(|(vtree, _)| vtree)
.expect("at least one restart"))
}
pub const MAX_EMBEDDING_DIM: usize = MAX_DIM;
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct Embedding {
pub dim: usize,
pub coords: Vec<f64>,
}
impl Embedding {
pub fn num_vars(&self) -> u32 {
(self.coords.len() / self.dim) as u32
}
pub fn position(&self, v: VarId) -> &[f64] {
let start = v.idx() * self.dim;
&self.coords[start..start + self.dim]
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct EmbeddingOptions {
pub dim: usize,
}
impl Default for EmbeddingOptions {
fn default() -> Self {
EmbeddingOptions { dim: 2 }
}
}
pub fn embed(
formula: &CnfFormula,
options: &EmbeddingOptions,
) -> Result<Embedding, crate::error::VitriError> {
let n = formula.num_vars as usize;
if n == 0 {
return Err(crate::error::VitriError::input(EMPTY_FORMULA));
}
if !(2..=MAX_EMBEDDING_DIM).contains(&options.dim) {
return Err(crate::error::VitriError::input(format!(
"embedding dimension is {} but the accepted range is 2 to {MAX_EMBEDDING_DIM}",
options.dim,
)));
}
let mut cfg = ForceConfig::new(ForceMode::Mst);
cfg.dim = options.dim;
let layout = force_layout(n, &build_incidence(formula), SEED, &cfg, None, None);
Ok(Embedding {
dim: options.dim,
coords: layout.into_iter().flatten().collect(),
})
}
#[cfg(test)]
mod tests;