use crate::dme_algorithm::{NodeIdx, SkewAnalysis, Tree};
pub struct ClockTreeVisualizer {
pub margin: u32,
pub node_radius: u32,
pub wire_width: u32,
pub sink_color: String,
pub internal_color: String,
pub root_color: String,
pub wire_color: String,
pub text_color: String,
}
impl Default for ClockTreeVisualizer {
fn default() -> Self {
Self {
margin: 50,
node_radius: 8,
wire_width: 2,
sink_color: "#4CAF50".into(),
internal_color: "#2196F3".into(),
root_color: "#F44336".into(),
wire_color: "#666666".into(),
text_color: "#333333".into(),
}
}
}
impl ClockTreeVisualizer {
pub fn new() -> Self {
Self::default()
}
#[allow(clippy::too_many_arguments)]
pub fn visualize_tree(
&self,
tree: &Tree,
root: NodeIdx,
sinks: &[crate::dme_algorithm::Sink],
filename: &str,
width: u32,
height: u32,
analysis: Option<&SkewAnalysis>,
) -> String {
let (min_x, min_y, max_x, max_y) = calculate_bounds(tree, root, sinks);
let range_x = (max_x - min_x).max(1) as f64;
let range_y = (max_y - min_y).max(1) as f64;
let m = self.margin as f64;
let sx = (width as f64 - 2.0 * m) / range_x;
let sy = (height as f64 - 2.0 * m) / range_y;
let scale = sx.min(sy);
let sc = |x: i32, y: i32| -> (f64, f64) {
(
m + (x as f64 - min_x as f64) * scale,
m + (y as f64 - min_y as f64) * scale,
)
};
let mut svg = String::new();
svg.push_str(&format!(
r#"<svg width="{}" height="{}" xmlns="http://www.w3.org/2000/svg">"#,
width, height
));
svg.push_str(&format!(
r#"<style>.nl{{font:{}px sans-serif;fill:{}}}.dl{{font:{}px sans-serif;fill:{}}}</style>"#,
10, self.text_color, 8, self.text_color
));
svg.push_str(r#"<rect width="100%" height="100%" fill="white"/>"#);
svg.push_str(r#"<g class="clock-tree">"#);
svg.push_str(&draw_wires(
tree,
root,
&sc,
&self.wire_color,
self.wire_width,
));
svg.push_str(&draw_nodes(
tree,
root,
sinks,
&sc,
self.root_color.as_str(),
self.internal_color.as_str(),
self.sink_color.as_str(),
self.node_radius,
));
if let Some(a) = analysis {
svg.push_str(&create_analysis_box(a));
}
svg.push_str("</g></svg>");
if !filename.is_empty() {
let _ = std::fs::write(filename, &svg);
eprintln!("Clock tree SVG saved to {}", filename);
}
svg
}
}
pub struct TreeData {
pub tree: Tree,
pub root: NodeIdx,
pub sinks: Vec<crate::dme_algorithm::Sink>,
pub analysis: Option<SkewAnalysis>,
pub title: String,
}
impl TreeData {
pub fn new(
tree: Tree,
root: NodeIdx,
sinks: Vec<crate::dme_algorithm::Sink>,
analysis: Option<SkewAnalysis>,
title: &str,
) -> Self {
TreeData {
tree,
root,
sinks,
analysis,
title: title.to_string(),
}
}
}
pub fn create_comparison_visualization(
trees_data: &[TreeData],
filename: &str,
width: u32,
height: u32,
) -> String {
assert!(!trees_data.is_empty(), "No tree data provided");
let num = trees_data.len();
let cols = num.min(2) as u32;
let rows = ((num as u32) + cols - 1) / cols;
let sub_w = width / cols;
let sub_h = height / rows;
let mut svg = String::new();
svg.push_str(&format!(
r#"<svg width="{}" height="{}" xmlns="http://www.w3.org/2000/svg">"#,
width, height
));
svg.push_str(r#"<rect width="100%" height="100%" fill="white"/>"#);
svg.push_str(
"<style>.nl{font:8px sans-serif;fill:#333}.dl{font:7px sans-serif;fill:#666}</style>",
);
let viz = ClockTreeVisualizer {
margin: 40,
node_radius: 6,
wire_width: 2,
..Default::default()
};
for (i, td) in trees_data.iter().enumerate() {
let row = (i as u32) / cols;
let col = (i as u32) % cols;
let ox = col * sub_w;
let oy = row * sub_h;
svg.push_str(&format!("<text x=\"{}\" y=\"{}\" font-family=\"sans-serif\" font-size=\"14\" font-weight=\"bold\" fill=\"{}333\" text-anchor=\"middle\">{}</text>", ox + sub_w / 2, oy + 20, '#', td.title));
let inner = viz.visualize_tree(
&td.tree,
td.root,
&td.sinks,
"",
sub_w - 20,
sub_h - 40,
td.analysis.as_ref(),
);
if let Some(start) = inner.find(r#"<g class="clock-tree">"#) {
let body_start = start + r#"<g class="clock-tree">"#.len();
let mut depth = 1u32;
let mut end = body_start;
for (i, _b) in inner.as_bytes()[body_start..].iter().enumerate() {
if inner[body_start + i..].starts_with("</g>") {
depth -= 1;
if depth == 0 {
end = body_start + i;
break;
}
continue;
}
if inner[body_start + i..].starts_with("<g ")
|| inner[body_start + i..].starts_with("<g>")
{
depth += 1;
}
}
let content = &inner[body_start..end];
svg.push_str(&format!(
r#"<g transform="translate({}, {})">"#,
ox + 10,
oy + 40
));
svg.push_str(content);
svg.push_str("</g>");
}
}
svg.push_str("</svg>");
if !filename.is_empty() {
let _ = std::fs::write(filename, &svg);
eprintln!("Comparison SVG saved to {}", filename);
}
svg
}
pub fn create_delay_model_comparison(linear: TreeData, elmore: TreeData, filename: &str) -> String {
create_comparison_visualization(&[linear, elmore], filename, 1200, 600)
}
fn calculate_bounds(
tree: &Tree,
root: NodeIdx,
sinks: &[crate::dme_algorithm::Sink],
) -> (i32, i32, i32, i32) {
let mut min_x = i32::MAX;
let mut min_y = i32::MAX;
let mut max_x = i32::MIN;
let mut max_y = i32::MIN;
let mut stack = vec![root];
while let Some(idx) = stack.pop() {
let n = tree.get(idx);
min_x = min_x.min(n.position.xcoord);
min_y = min_y.min(n.position.ycoord);
max_x = max_x.max(n.position.xcoord);
max_y = max_y.max(n.position.ycoord);
if let Some(r) = n.right {
stack.push(r);
}
if let Some(l) = n.left {
stack.push(l);
}
}
for sink in sinks {
min_x = min_x.min(sink.position.xcoord);
min_y = min_y.min(sink.position.ycoord);
max_x = max_x.max(sink.position.xcoord);
max_y = max_y.max(sink.position.ycoord);
}
if min_x == i32::MAX {
return (0, 0, 100, 100);
}
let padding = ((max_x - min_x).max(max_y - min_y) as f64 * 0.1).max(10.0) as i32;
(
min_x - padding,
min_y - padding,
max_x + padding,
max_y + padding,
)
}
fn sink_positions(sinks: &[crate::dme_algorithm::Sink]) -> std::collections::HashSet<(i32, i32)> {
sinks
.iter()
.map(|s| (s.position.xcoord, s.position.ycoord))
.collect()
}
fn draw_wires(
tree: &Tree,
root: NodeIdx,
sc: &dyn Fn(i32, i32) -> (f64, f64),
color: &str,
width: u32,
) -> String {
let mut out = String::new();
let mut stack = vec![root];
while let Some(idx) = stack.pop() {
let n = tree.get(idx);
if let Some(p) = n.parent {
let pb = tree.get(p);
let (x1, y1) = sc(pb.position.xcoord, pb.position.ycoord);
let (x2, y2) = sc(n.position.xcoord, n.position.ycoord);
out.push_str(&format!(
r#"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{}" stroke-width="{}" stroke-linecap="round"/>"#,
x1, y1, x2, y2, color, width
));
if n.wire_length > 0 {
let mx = (x1 + x2) / 2.0;
let my = (y1 + y2) / 2.0;
out.push_str(&format!(
r#"<text x="{}" y="{}" class="dl" text-anchor="middle">{}</text>"#,
mx,
my - 5.0,
n.wire_length
));
}
}
if let Some(r) = n.right {
stack.push(r);
}
if let Some(l) = n.left {
stack.push(l);
}
}
out
}
#[allow(clippy::too_many_arguments)]
fn draw_nodes(
tree: &Tree,
root: NodeIdx,
sinks: &[crate::dme_algorithm::Sink],
sc: &dyn Fn(i32, i32) -> (f64, f64),
root_color: &str,
internal_color: &str,
sink_color: &str,
radius: u32,
) -> String {
let sink_set = sink_positions(sinks);
let mut out = String::new();
let mut stack = vec![(root, 0u32)];
while let Some((idx, _depth)) = stack.pop() {
let n = tree.get(idx);
let (x, y) = sc(n.position.xcoord, n.position.ycoord);
let is_root = n.parent.is_none();
let is_sink = sink_set.contains(&(n.position.xcoord, n.position.ycoord));
let (color, r) = if is_root {
(root_color, radius + 2)
} else if is_sink {
(sink_color, radius)
} else {
(internal_color, radius - 2)
};
let r = r as f64;
out.push_str(&format!(
"<circle cx=\"{}\" cy=\"{}\" r=\"{}\" fill=\"{}\" stroke=\"{}333\" stroke-width=\"1\"/>",
x, y, r, color, '#'
));
out.push_str(&format!(
r#"<text x="{}" y="{}" class="nl" text-anchor="middle">{}</text>"#,
x,
y - r - 5.0,
n.name
));
out.push_str(&format!(
r#"<text x="{}" y="{}" class="dl" text-anchor="middle">d:{:.1}</text>"#,
x,
y + r + 12.0,
n.delay
));
if is_sink {
out.push_str(&format!(
r#"<text x="{}" y="{}" class="dl" text-anchor="middle">c:{:.1}</text>"#,
x,
y + r + 22.0,
n.capacitance
));
}
if let Some(rn) = n.right {
stack.push((rn, _depth + 1));
}
if let Some(ln) = n.left {
stack.push((ln, _depth + 1));
}
}
out
}
fn create_analysis_box(analysis: &SkewAnalysis) -> String {
let mut s = String::new();
s.push_str(r#"<g class="analysis-info">"#);
s.push_str(&format!(
"<rect x=\"10\" y=\"10\" width=\"220\" height=\"140\" fill=\"white\" stroke=\"{}ccc\" stroke-width=\"1\" rx=\"5\"/>",
'#'
));
s.push_str(&format!(
"<rect x=\"10\" y=\"10\" width=\"220\" height=\"20\" fill=\"#f0f0f0\" stroke=\"{}ccc\" stroke-width=\"1\" rx=\"5\"/>",
'#'
));
s.push_str(&format!(
"<text x=\"20\" y=\"25\" font-family=\"sans-serif\" font-size=\"12\" font-weight=\"bold\" fill=\"{}333\">Clock Tree Analysis</text>",
'#'
));
s.push_str(&format!(
"<text x=\"20\" y=\"45\" font-family=\"monospace\" font-size=\"11\" fill=\"{}333\">",
'#'
));
let lines = [
format!("Delay Model: {}", analysis.delay_model),
format!("Max Delay: {:.3}", analysis.max_delay),
format!("Min Delay: {:.3}", analysis.min_delay),
format!("Skew: {:.3}", analysis.skew),
format!("Total WL: {}", analysis.total_wirelength),
format!("Sinks: {}", analysis.sink_delays.len()),
];
for (i, line) in lines.iter().enumerate() {
s.push_str(&format!(
r#"<tspan x="20" y="{}">{}</tspan>"#,
45 + (i as u32 + 1) * 16,
line
));
}
s.push_str("</text></g>");
s
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dme_algorithm::*;
use crate::Point;
fn sample_sinks() -> Vec<Sink> {
vec![
Sink::new("s1", Point::new(0, 0), 1.0),
Sink::new("s2", Point::new(10, 0), 1.0),
Sink::new("s3", Point::new(10, 10), 1.0),
Sink::new("s4", Point::new(0, 10), 1.0),
]
}
fn build_test_tree(sinks: Vec<Sink>) -> (DMEAlgorithm, NodeIdx) {
let calc = Box::new(LinearDelayCalculator::new(0.5, 0.1));
let mut dme = DMEAlgorithm::new(sinks, calc);
let root = dme.build_clock_tree();
(dme, root)
}
#[test]
fn test_visualizer_produces_valid_svg() {
let sinks = sample_sinks();
let (dme, root) = build_test_tree(sinks.clone());
let analysis = dme.analyze_skew(root);
let viz = ClockTreeVisualizer::new();
let svg = viz.visualize_tree(dme.get_tree(), root, &sinks, "", 400, 300, Some(&analysis));
assert!(svg.starts_with("<svg"));
assert!(svg.ends_with("</svg>"));
assert!(svg.contains("Clock Tree Analysis"));
assert!(svg.contains("Max Delay"));
assert!(svg.contains("d:"));
}
#[test]
fn test_visualizer_svg_contains_all_nodes() {
let sinks = sample_sinks();
let (dme, root) = build_test_tree(sinks.clone());
let analysis = dme.analyze_skew(root);
let viz = ClockTreeVisualizer::new();
let svg = viz.visualize_tree(dme.get_tree(), root, &sinks, "", 400, 300, Some(&analysis));
for sink in &sinks {
assert!(svg.contains(&sink.name), "Missing node: {}", sink.name);
}
}
#[test]
fn test_visualizer_skew_within_two_percent() {
let sinks = sample_sinks();
let (dme, root) = build_test_tree(sinks.clone());
let analysis = dme.analyze_skew(root);
assert!(analysis.max_delay > 0.0);
let pct = analysis.skew / analysis.max_delay * 100.0;
assert!(
pct < 2.0,
"Skew {:.4} ({:.2}%) exceeds 2%",
analysis.skew,
pct
);
}
#[test]
fn test_visualizer_clock_tree_group_wrapper() {
let sinks = sample_sinks();
let (dme, root) = build_test_tree(sinks.clone());
let analysis = dme.analyze_skew(root);
let viz = ClockTreeVisualizer::new();
let svg = viz.visualize_tree(dme.get_tree(), root, &sinks, "", 400, 300, Some(&analysis));
assert!(svg.contains(r#"<g class="clock-tree">"#));
assert!(svg.contains("</g>"));
}
#[test]
fn test_create_comparison_visualization() {
let sinks = sample_sinks();
let (dme, root) = build_test_tree(sinks.clone());
let analysis = dme.analyze_skew(root);
let td = TreeData::new(
dme.get_tree().clone(),
root,
sinks,
Some(analysis),
"Test Tree",
);
let svg = create_comparison_visualization(&[td], "", 800, 400);
assert!(svg.starts_with("<svg"));
assert!(svg.ends_with("</svg>"));
assert!(svg.contains("Test Tree"));
}
#[test]
fn test_create_delay_model_comparison() {
use crate::dme_algorithm::ElmoreDelayCalculator;
let sinks = sample_sinks();
let calc = Box::new(LinearDelayCalculator::new(0.5, 0.1));
let mut dme = DMEAlgorithm::new(sinks.clone(), calc);
let root = dme.build_clock_tree();
let analysis = dme.analyze_skew(root);
let ecalc = Box::new(ElmoreDelayCalculator::new(0.1, 0.1));
let mut edme = DMEAlgorithm::new(sinks.clone(), ecalc);
let eroot = edme.build_clock_tree();
let eanalysis = edme.analyze_skew(eroot);
let linear = TreeData::new(
dme.get_tree().clone(),
root,
sinks.clone(),
Some(analysis),
"Linear Delay",
);
let elmore = TreeData::new(
edme.get_tree().clone(),
eroot,
sinks,
Some(eanalysis),
"Elmore Delay",
);
let svg = create_delay_model_comparison(linear, elmore, "");
assert!(svg.contains("Linear Delay"));
assert!(svg.contains("Elmore Delay"));
}
#[test]
fn test_visualizer_generates_svg_file() {
let sinks = sample_sinks();
let (dme, root) = build_test_tree(sinks.clone());
let analysis = dme.analyze_skew(root);
let viz = ClockTreeVisualizer::new();
let path = "test_clock_tree.svg";
let svg = viz.visualize_tree(
dme.get_tree(),
root,
&sinks,
path,
800,
600,
Some(&analysis),
);
assert!(std::path::Path::new(path).exists());
let saved = std::fs::read_to_string(path).unwrap();
assert_eq!(saved, svg);
let _ = std::fs::remove_file(path);
}
}