1use crate::dme_algorithm::{NodeIdx, SkewAnalysis, Tree};
8
9pub 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 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
124pub 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
151pub 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 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
243pub fn create_delay_model_comparison(linear: TreeData, elmore: TreeData, filename: &str) -> String {
245 create_comparison_visualization(&[linear, elmore], filename, 1200, 600)
246}
247
248fn 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}