use crate::cnf::CnfFormula;
use crate::score::{vtree_clause_load_per_node, vtree_context_width_per_node};
use crate::vtree::{Vtree, VtreeIdx};
#[derive(Clone, Debug, Default)]
pub struct VtreeDotAnnotations {
nodes: Vec<NodeAnnotation>,
}
#[derive(Clone, Debug, Default)]
struct NodeAnnotation {
label: Option<String>,
heat: Option<f64>,
}
impl VtreeDotAnnotations {
pub fn new(num_nodes: usize) -> Self {
VtreeDotAnnotations {
nodes: vec![NodeAnnotation::default(); num_nodes],
}
}
pub fn set_heat(&mut self, node: VtreeIdx, heat: f64) {
let heat = if heat.is_nan() {
0.0
} else {
heat.clamp(0.0, 1.0)
};
self.nodes[node.idx()].heat = Some(heat);
}
pub fn set_label(&mut self, node: VtreeIdx, label: impl Into<String>) {
self.nodes[node.idx()].label = Some(label.into());
}
pub fn heat(&self, node: VtreeIdx) -> Option<f64> {
self.nodes.get(node.idx()).and_then(|n| n.heat)
}
pub fn label(&self, node: VtreeIdx) -> Option<&str> {
self.nodes.get(node.idx()).and_then(|n| n.label.as_deref())
}
}
pub fn vtree_to_dot(vtree: &Vtree, ann: Option<&VtreeDotAnnotations>) -> String {
let mut dot = String::from("graph vtree {\n rankdir=TB;\n");
for (t, var) in vtree.leaf_bottomup() {
dot.push_str(&format!(
" v{} [shape=box, label=\"X{}\"{}];\n",
t.0,
subscript(var.to_dimacs() as u32),
decoration(ann, t),
));
}
for (t, _left, _right) in vtree.internal_bottomup() {
dot.push_str(&format!(
" v{} [shape=circle, label=\"{}\"{}];\n",
t.0,
t.0,
decoration(ann, t),
));
}
for (t, left, right) in vtree.internal_bottomup() {
dot.push_str(&format!(" v{} -- v{};\n", t.0, left.0));
dot.push_str(&format!(" v{} -- v{};\n", t.0, right.0));
}
dot.push_str("}\n");
dot
}
const FLAT_LOAD_HEAT: f64 = 0.25;
pub fn annotate_from_cnf(
vtree: &Vtree,
formula: &CnfFormula,
show_mask: Option<&crate::cnf::ShowMask>,
) -> VtreeDotAnnotations {
let load = vtree_clause_load_per_node(vtree, formula);
let width = vtree_context_width_per_node(vtree, formula, show_mask);
let max_load = load.iter().copied().max().unwrap_or(0);
let min_loaded = load.iter().copied().filter(|&c| c > 0).min();
let flat = min_loaded == Some(max_load);
let mut ann = VtreeDotAnnotations::new(vtree.num_nodes());
for (i, &c) in load.iter().enumerate() {
let heat = if max_load == 0 || c == 0 {
0.0
} else if flat {
FLAT_LOAD_HEAT
} else {
c as f64 / max_load as f64
};
ann.set_heat(VtreeIdx(i as u32), heat);
}
for (t, _left, _right) in vtree.internal_bottomup() {
ann.set_label(t, format!("c={} w={}", load[t.idx()], width[t.idx()]));
}
ann
}
fn decoration(ann: Option<&VtreeDotAnnotations>, node: VtreeIdx) -> String {
let Some(ann) = ann else { return String::new() };
let mut out = String::new();
if let Some(heat) = ann.heat(node) {
let (fill, font) = heatmap_color(heat);
out.push_str(&format!(
", style=filled, fillcolor=\"{fill}\", fontcolor=\"{font}\""
));
}
if let Some(label) = ann.label(node) {
out.push_str(&format!(
", xlabel=<<FONT COLOR=\"#888888\" POINT-SIZE=\"8\">{}</FONT>>",
escape_html(label),
));
}
out
}
fn heatmap_color(t: f64) -> (String, &'static str) {
let (r, g, b) = if t < 0.5 {
lerp_rgb((0xff, 0xff, 0xb2), (0xfd, 0x8d, 0x3c), t * 2.0)
} else {
lerp_rgb((0xfd, 0x8d, 0x3c), (0x80, 0x00, 0x26), (t - 0.5) * 2.0)
};
let fontcolor = if t > 0.65 { "white" } else { "black" };
(format!("#{r:02x}{g:02x}{b:02x}"), fontcolor)
}
fn lerp_rgb(lo: (u8, u8, u8), hi: (u8, u8, u8), t: f64) -> (u8, u8, u8) {
let lerp = |a: u8, b: u8| (a as f64 + t * (b as f64 - a as f64)).round() as u8;
(lerp(lo.0, hi.0), lerp(lo.1, hi.1), lerp(lo.2, hi.2))
}
fn escape_html(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
_ => out.push(c),
}
}
out
}
fn subscript(n: u32) -> String {
const SUBSCRIPT_DIGITS: [char; 10] = ['₀', '₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉'];
if n == 0 {
return SUBSCRIPT_DIGITS[0].to_string();
}
let mut digits = Vec::new();
let mut rem = n;
while rem > 0 {
digits.push(SUBSCRIPT_DIGITS[(rem % 10) as usize]);
rem /= 10;
}
digits.reverse();
digits.into_iter().collect()
}
#[cfg(test)]
mod tests;