Skip to main content

physdes/
dme_visualizer.rs

1//! SVG visualizer for clock trees generated by the DME algorithm.
2//!
3//! Provides `ClockTreeVisualizer` for rendering clock trees as scalable SVG
4//! graphics with node coloring (root/internal/sink), wire length labels,
5//! delay and capacitance annotations, and an optional analysis info panel.
6
7use crate::dme_algorithm::{NodeIdx, SkewAnalysis, Tree};
8
9/// SVG visualizer for DME clock trees.
10///
11/// Color-codes nodes by type (root=red, internal=blue, sinks=green),
12/// draws parent-child wires with length labels, and overlays delay/capacitance
13/// info. Optionally displays a skew analysis panel.
14pub struct ClockTreeVisualizer {
15    pub margin: u32,
16    pub node_radius: u32,
17    pub wire_width: u32,
18    pub sink_color: String,
19    pub internal_color: String,
20    pub root_color: String,
21    pub wire_color: String,
22    pub text_color: String,
23}
24
25impl Default for ClockTreeVisualizer {
26    fn default() -> Self {
27        Self {
28            margin: 50,
29            node_radius: 8,
30            wire_width: 2,
31            sink_color: "#4CAF50".into(),
32            internal_color: "#2196F3".into(),
33            root_color: "#F44336".into(),
34            wire_color: "#666666".into(),
35            text_color: "#333333".into(),
36        }
37    }
38}
39
40impl ClockTreeVisualizer {
41    pub fn new() -> Self {
42        Self::default()
43    }
44
45    #[allow(clippy::too_many_arguments)]
46    /// Produce an SVG string visualizing the clock tree rooted at `root`.
47    ///
48    /// * `tree` — the arena-allocated clock tree (from `DMEAlgorithm::get_tree()`)
49    /// * `root` — root node index (returned by `DMEAlgorithm::build_clock_tree`)
50    /// * `sinks` — original sink list (used to identify leaf nodes)
51    /// * `filename` — if non-empty, save the SVG to this path
52    /// * `width`, `height` — SVG canvas dimensions
53    /// * `analysis` — optional `SkewAnalysis` to display in an info panel
54    pub fn visualize_tree(
55        &self,
56        tree: &Tree,
57        root: NodeIdx,
58        sinks: &[crate::dme_algorithm::Sink],
59        filename: &str,
60        width: u32,
61        height: u32,
62        analysis: Option<&SkewAnalysis>,
63    ) -> String {
64        let (min_x, min_y, max_x, max_y) = calculate_bounds(tree, root, sinks);
65
66        let range_x = (max_x - min_x).max(1) as f64;
67        let range_y = (max_y - min_y).max(1) as f64;
68        let m = self.margin as f64;
69        let sx = (width as f64 - 2.0 * m) / range_x;
70        let sy = (height as f64 - 2.0 * m) / range_y;
71        let scale = sx.min(sy);
72
73        let sc = |x: i32, y: i32| -> (f64, f64) {
74            (
75                m + (x as f64 - min_x as f64) * scale,
76                m + (y as f64 - min_y as f64) * scale,
77            )
78        };
79
80        let mut svg = String::new();
81        svg.push_str(&format!(
82            r#"<svg width="{}" height="{}" xmlns="http://www.w3.org/2000/svg">"#,
83            width, height
84        ));
85        svg.push_str(&format!(
86            r#"<style>.nl{{font:{}px sans-serif;fill:{}}}.dl{{font:{}px sans-serif;fill:{}}}</style>"#,
87            10, self.text_color, 8, self.text_color
88        ));
89        svg.push_str(r#"<rect width="100%" height="100%" fill="white"/>"#);
90        svg.push_str(r#"<g class="clock-tree">"#);
91
92        svg.push_str(&draw_wires(
93            tree,
94            root,
95            &sc,
96            &self.wire_color,
97            self.wire_width,
98        ));
99        svg.push_str(&draw_nodes(
100            tree,
101            root,
102            sinks,
103            &sc,
104            self.root_color.as_str(),
105            self.internal_color.as_str(),
106            self.sink_color.as_str(),
107            self.node_radius,
108        ));
109
110        if let Some(a) = analysis {
111            svg.push_str(&create_analysis_box(a));
112        }
113
114        svg.push_str("</g></svg>");
115
116        if !filename.is_empty() {
117            let _ = std::fs::write(filename, &svg);
118            eprintln!("Clock tree SVG saved to {}", filename);
119        }
120        svg
121    }
122}
123
124/// Data for a single tree in a comparison visualization.
125pub struct TreeData {
126    pub tree: Tree,
127    pub root: NodeIdx,
128    pub sinks: Vec<crate::dme_algorithm::Sink>,
129    pub analysis: Option<SkewAnalysis>,
130    pub title: String,
131}
132
133impl TreeData {
134    pub fn new(
135        tree: Tree,
136        root: NodeIdx,
137        sinks: Vec<crate::dme_algorithm::Sink>,
138        analysis: Option<SkewAnalysis>,
139        title: &str,
140    ) -> Self {
141        TreeData {
142            tree,
143            root,
144            sinks,
145            analysis,
146            title: title.to_string(),
147        }
148    }
149}
150
151/// Create a side-by-side comparison SVG of multiple clock trees.
152///
153/// `trees_data` — list of trees to render; laid out in a grid (up to 2 columns).
154pub fn create_comparison_visualization(
155    trees_data: &[TreeData],
156    filename: &str,
157    width: u32,
158    height: u32,
159) -> String {
160    assert!(!trees_data.is_empty(), "No tree data provided");
161
162    let num = trees_data.len();
163    let cols = num.min(2) as u32;
164    let rows = ((num as u32) + cols - 1) / cols;
165    let sub_w = width / cols;
166    let sub_h = height / rows;
167
168    let mut svg = String::new();
169    svg.push_str(&format!(
170        r#"<svg width="{}" height="{}" xmlns="http://www.w3.org/2000/svg">"#,
171        width, height
172    ));
173    svg.push_str(r#"<rect width="100%" height="100%" fill="white"/>"#);
174    svg.push_str(
175        "<style>.nl{font:8px sans-serif;fill:#333}.dl{font:7px sans-serif;fill:#666}</style>",
176    );
177
178    let viz = ClockTreeVisualizer {
179        margin: 40,
180        node_radius: 6,
181        wire_width: 2,
182        ..Default::default()
183    };
184
185    for (i, td) in trees_data.iter().enumerate() {
186        let row = (i as u32) / cols;
187        let col = (i as u32) % cols;
188        let ox = col * sub_w;
189        let oy = row * sub_h;
190
191        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));
192
193        let inner = viz.visualize_tree(
194            &td.tree,
195            td.root,
196            &td.sinks,
197            "",
198            sub_w - 20,
199            sub_h - 40,
200            td.analysis.as_ref(),
201        );
202
203        // Extract content between <g class="clock-tree"> and its matching </g>
204        if let Some(start) = inner.find(r#"<g class="clock-tree">"#) {
205            let body_start = start + r#"<g class="clock-tree">"#.len();
206            let mut depth = 1u32;
207            let mut end = body_start;
208            for (i, _b) in inner.as_bytes()[body_start..].iter().enumerate() {
209                if inner[body_start + i..].starts_with("</g>") {
210                    depth -= 1;
211                    if depth == 0 {
212                        end = body_start + i;
213                        break;
214                    }
215                    continue;
216                }
217                if inner[body_start + i..].starts_with("<g ")
218                    || inner[body_start + i..].starts_with("<g>")
219                {
220                    depth += 1;
221                }
222            }
223            let content = &inner[body_start..end];
224            svg.push_str(&format!(
225                r#"<g transform="translate({}, {})">"#,
226                ox + 10,
227                oy + 40
228            ));
229            svg.push_str(content);
230            svg.push_str("</g>");
231        }
232    }
233
234    svg.push_str("</svg>");
235
236    if !filename.is_empty() {
237        let _ = std::fs::write(filename, &svg);
238        eprintln!("Comparison SVG saved to {}", filename);
239    }
240    svg
241}
242
243/// Convenience wrapper: compare linear vs Elmore delay model trees side by side.
244pub fn create_delay_model_comparison(linear: TreeData, elmore: TreeData, filename: &str) -> String {
245    create_comparison_visualization(&[linear, elmore], filename, 1200, 600)
246}
247
248// --- helper helpers (work with &Tree + NodeIdx) ---
249
250fn calculate_bounds(
251    tree: &Tree,
252    root: NodeIdx,
253    sinks: &[crate::dme_algorithm::Sink],
254) -> (i32, i32, i32, i32) {
255    let mut min_x = i32::MAX;
256    let mut min_y = i32::MAX;
257    let mut max_x = i32::MIN;
258    let mut max_y = i32::MIN;
259
260    let mut stack = vec![root];
261    while let Some(idx) = stack.pop() {
262        let n = tree.get(idx);
263        min_x = min_x.min(n.position.xcoord);
264        min_y = min_y.min(n.position.ycoord);
265        max_x = max_x.max(n.position.xcoord);
266        max_y = max_y.max(n.position.ycoord);
267        if let Some(r) = n.right {
268            stack.push(r);
269        }
270        if let Some(l) = n.left {
271            stack.push(l);
272        }
273    }
274    for sink in sinks {
275        min_x = min_x.min(sink.position.xcoord);
276        min_y = min_y.min(sink.position.ycoord);
277        max_x = max_x.max(sink.position.xcoord);
278        max_y = max_y.max(sink.position.ycoord);
279    }
280
281    if min_x == i32::MAX {
282        return (0, 0, 100, 100);
283    }
284
285    let padding = ((max_x - min_x).max(max_y - min_y) as f64 * 0.1).max(10.0) as i32;
286    (
287        min_x - padding,
288        min_y - padding,
289        max_x + padding,
290        max_y + padding,
291    )
292}
293
294fn sink_positions(sinks: &[crate::dme_algorithm::Sink]) -> std::collections::HashSet<(i32, i32)> {
295    sinks
296        .iter()
297        .map(|s| (s.position.xcoord, s.position.ycoord))
298        .collect()
299}
300
301fn draw_wires(
302    tree: &Tree,
303    root: NodeIdx,
304    sc: &dyn Fn(i32, i32) -> (f64, f64),
305    color: &str,
306    width: u32,
307) -> String {
308    let mut out = String::new();
309    let mut stack = vec![root];
310    while let Some(idx) = stack.pop() {
311        let n = tree.get(idx);
312        if let Some(p) = n.parent {
313            let pb = tree.get(p);
314            let (x1, y1) = sc(pb.position.xcoord, pb.position.ycoord);
315            let (x2, y2) = sc(n.position.xcoord, n.position.ycoord);
316            out.push_str(&format!(
317                r#"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{}" stroke-width="{}" stroke-linecap="round"/>"#,
318                x1, y1, x2, y2, color, width
319            ));
320            if n.wire_length > 0 {
321                let mx = (x1 + x2) / 2.0;
322                let my = (y1 + y2) / 2.0;
323                out.push_str(&format!(
324                    r#"<text x="{}" y="{}" class="dl" text-anchor="middle">{}</text>"#,
325                    mx,
326                    my - 5.0,
327                    n.wire_length
328                ));
329            }
330        }
331        if let Some(r) = n.right {
332            stack.push(r);
333        }
334        if let Some(l) = n.left {
335            stack.push(l);
336        }
337    }
338    out
339}
340
341#[allow(clippy::too_many_arguments)]
342fn draw_nodes(
343    tree: &Tree,
344    root: NodeIdx,
345    sinks: &[crate::dme_algorithm::Sink],
346    sc: &dyn Fn(i32, i32) -> (f64, f64),
347    root_color: &str,
348    internal_color: &str,
349    sink_color: &str,
350    radius: u32,
351) -> String {
352    let sink_set = sink_positions(sinks);
353    let mut out = String::new();
354    let mut stack = vec![(root, 0u32)];
355    while let Some((idx, _depth)) = stack.pop() {
356        let n = tree.get(idx);
357        let (x, y) = sc(n.position.xcoord, n.position.ycoord);
358        let is_root = n.parent.is_none();
359        let is_sink = sink_set.contains(&(n.position.xcoord, n.position.ycoord));
360
361        let (color, r) = if is_root {
362            (root_color, radius + 2)
363        } else if is_sink {
364            (sink_color, radius)
365        } else {
366            (internal_color, radius - 2)
367        };
368
369        let r = r as f64;
370        out.push_str(&format!(
371            "<circle cx=\"{}\" cy=\"{}\" r=\"{}\" fill=\"{}\" stroke=\"{}333\" stroke-width=\"1\"/>",
372            x, y, r, color, '#'
373        ));
374        out.push_str(&format!(
375            r#"<text x="{}" y="{}" class="nl" text-anchor="middle">{}</text>"#,
376            x,
377            y - r - 5.0,
378            n.name
379        ));
380        out.push_str(&format!(
381            r#"<text x="{}" y="{}" class="dl" text-anchor="middle">d:{:.1}</text>"#,
382            x,
383            y + r + 12.0,
384            n.delay
385        ));
386        if is_sink {
387            out.push_str(&format!(
388                r#"<text x="{}" y="{}" class="dl" text-anchor="middle">c:{:.1}</text>"#,
389                x,
390                y + r + 22.0,
391                n.capacitance
392            ));
393        }
394
395        if let Some(rn) = n.right {
396            stack.push((rn, _depth + 1));
397        }
398        if let Some(ln) = n.left {
399            stack.push((ln, _depth + 1));
400        }
401    }
402    out
403}
404
405fn create_analysis_box(analysis: &SkewAnalysis) -> String {
406    let mut s = String::new();
407    s.push_str(r#"<g class="analysis-info">"#);
408    s.push_str(&format!(
409        "<rect x=\"10\" y=\"10\" width=\"220\" height=\"140\" fill=\"white\" stroke=\"{}ccc\" stroke-width=\"1\" rx=\"5\"/>",
410        '#'
411    ));
412    s.push_str(&format!(
413        "<rect x=\"10\" y=\"10\" width=\"220\" height=\"20\" fill=\"#f0f0f0\" stroke=\"{}ccc\" stroke-width=\"1\" rx=\"5\"/>",
414        '#'
415    ));
416    s.push_str(&format!(
417        "<text x=\"20\" y=\"25\" font-family=\"sans-serif\" font-size=\"12\" font-weight=\"bold\" fill=\"{}333\">Clock Tree Analysis</text>",
418        '#'
419    ));
420    s.push_str(&format!(
421        "<text x=\"20\" y=\"45\" font-family=\"monospace\" font-size=\"11\" fill=\"{}333\">",
422        '#'
423    ));
424
425    let lines = [
426        format!("Delay Model: {}", analysis.delay_model),
427        format!("Max Delay: {:.3}", analysis.max_delay),
428        format!("Min Delay: {:.3}", analysis.min_delay),
429        format!("Skew: {:.3}", analysis.skew),
430        format!("Total WL: {}", analysis.total_wirelength),
431        format!("Sinks: {}", analysis.sink_delays.len()),
432    ];
433    for (i, line) in lines.iter().enumerate() {
434        s.push_str(&format!(
435            r#"<tspan x="20" y="{}">{}</tspan>"#,
436            45 + (i as u32 + 1) * 16,
437            line
438        ));
439    }
440
441    s.push_str("</text></g>");
442    s
443}
444
445#[cfg(test)]
446mod tests {
447    use super::*;
448    use crate::dme_algorithm::*;
449    use crate::Point;
450
451    fn sample_sinks() -> Vec<Sink> {
452        vec![
453            Sink::new("s1", Point::new(0, 0), 1.0),
454            Sink::new("s2", Point::new(10, 0), 1.0),
455            Sink::new("s3", Point::new(10, 10), 1.0),
456            Sink::new("s4", Point::new(0, 10), 1.0),
457        ]
458    }
459
460    fn build_test_tree(sinks: Vec<Sink>) -> (DMEAlgorithm, NodeIdx) {
461        let calc = Box::new(LinearDelayCalculator::new(0.5, 0.1));
462        let mut dme = DMEAlgorithm::new(sinks, calc);
463        let root = dme.build_clock_tree();
464        (dme, root)
465    }
466
467    #[test]
468    fn test_visualizer_produces_valid_svg() {
469        let sinks = sample_sinks();
470        let (dme, root) = build_test_tree(sinks.clone());
471        let analysis = dme.analyze_skew(root);
472        let viz = ClockTreeVisualizer::new();
473        let svg = viz.visualize_tree(dme.get_tree(), root, &sinks, "", 400, 300, Some(&analysis));
474
475        assert!(svg.starts_with("<svg"));
476        assert!(svg.ends_with("</svg>"));
477        assert!(svg.contains("Clock Tree Analysis"));
478        assert!(svg.contains("Max Delay"));
479        assert!(svg.contains("d:"));
480    }
481
482    #[test]
483    fn test_visualizer_svg_contains_all_nodes() {
484        let sinks = sample_sinks();
485        let (dme, root) = build_test_tree(sinks.clone());
486        let analysis = dme.analyze_skew(root);
487        let viz = ClockTreeVisualizer::new();
488        let svg = viz.visualize_tree(dme.get_tree(), root, &sinks, "", 400, 300, Some(&analysis));
489
490        for sink in &sinks {
491            assert!(svg.contains(&sink.name), "Missing node: {}", sink.name);
492        }
493    }
494
495    #[test]
496    fn test_visualizer_skew_within_two_percent() {
497        let sinks = sample_sinks();
498        let (dme, root) = build_test_tree(sinks.clone());
499        let analysis = dme.analyze_skew(root);
500
501        assert!(analysis.max_delay > 0.0);
502        let pct = analysis.skew / analysis.max_delay * 100.0;
503        assert!(
504            pct < 2.0,
505            "Skew {:.4} ({:.2}%) exceeds 2%",
506            analysis.skew,
507            pct
508        );
509    }
510
511    #[test]
512    fn test_visualizer_clock_tree_group_wrapper() {
513        let sinks = sample_sinks();
514        let (dme, root) = build_test_tree(sinks.clone());
515        let analysis = dme.analyze_skew(root);
516        let viz = ClockTreeVisualizer::new();
517        let svg = viz.visualize_tree(dme.get_tree(), root, &sinks, "", 400, 300, Some(&analysis));
518        assert!(svg.contains(r#"<g class="clock-tree">"#));
519        assert!(svg.contains("</g>"));
520    }
521
522    #[test]
523    fn test_create_comparison_visualization() {
524        let sinks = sample_sinks();
525        let (dme, root) = build_test_tree(sinks.clone());
526        let analysis = dme.analyze_skew(root);
527
528        let td = TreeData::new(
529            dme.get_tree().clone(),
530            root,
531            sinks,
532            Some(analysis),
533            "Test Tree",
534        );
535        let svg = create_comparison_visualization(&[td], "", 800, 400);
536        assert!(svg.starts_with("<svg"));
537        assert!(svg.ends_with("</svg>"));
538        assert!(svg.contains("Test Tree"));
539    }
540
541    #[test]
542    fn test_create_delay_model_comparison() {
543        use crate::dme_algorithm::ElmoreDelayCalculator;
544
545        let sinks = sample_sinks();
546        let calc = Box::new(LinearDelayCalculator::new(0.5, 0.1));
547        let mut dme = DMEAlgorithm::new(sinks.clone(), calc);
548        let root = dme.build_clock_tree();
549        let analysis = dme.analyze_skew(root);
550
551        let ecalc = Box::new(ElmoreDelayCalculator::new(0.1, 0.1));
552        let mut edme = DMEAlgorithm::new(sinks.clone(), ecalc);
553        let eroot = edme.build_clock_tree();
554        let eanalysis = edme.analyze_skew(eroot);
555
556        let linear = TreeData::new(
557            dme.get_tree().clone(),
558            root,
559            sinks.clone(),
560            Some(analysis),
561            "Linear Delay",
562        );
563        let elmore = TreeData::new(
564            edme.get_tree().clone(),
565            eroot,
566            sinks,
567            Some(eanalysis),
568            "Elmore Delay",
569        );
570        let svg = create_delay_model_comparison(linear, elmore, "");
571        assert!(svg.contains("Linear Delay"));
572        assert!(svg.contains("Elmore Delay"));
573    }
574
575    #[test]
576    fn test_visualizer_generates_svg_file() {
577        let sinks = sample_sinks();
578        let (dme, root) = build_test_tree(sinks.clone());
579        let analysis = dme.analyze_skew(root);
580        let viz = ClockTreeVisualizer::new();
581        let path = "test_clock_tree.svg";
582        let svg = viz.visualize_tree(
583            dme.get_tree(),
584            root,
585            &sinks,
586            path,
587            800,
588            600,
589            Some(&analysis),
590        );
591
592        assert!(std::path::Path::new(path).exists());
593        let saved = std::fs::read_to_string(path).unwrap();
594        assert_eq!(saved, svg);
595        let _ = std::fs::remove_file(path);
596    }
597}