"""
Clock Tree Visualization Module
This module provides functions to visualize clock trees generated by the DME algorithm
using SVG format for clear, scalable graphics.
"""
import doctest
from typing import Any, Callable, Dict, List, Optional, Tuple
from physdes.cts.dme_algorithm import Sink
from physdes.point import Point
[docs]
class ClockTree3dVisualizer:
"""Visualizes clock trees in SVG format"""
[docs]
def __init__(
self,
margin: int = 50,
node_radius: int = 8,
wire_width: int = 2,
sink_color: str = "#4CAF50",
internal_color: str = "#2196F3",
root_color: str = "#F44336",
wire_color: str = "#666666",
text_color: str = "#333333",
):
"""
Initialize the visualizer with styling parameters
Args:
margin: Margin around the drawing
node_radius: Radius of node circles
wire_width: Width of wire lines
sink_color: Color for sink nodes
internal_color: Color for internal nodes
root_color: Color for root node
wire_color: Color for wires
text_color: Color for text labels
Examples:
>>> viz = ClockTree3dVisualizer(margin=10, node_radius=5)
>>> viz.margin
10
>>> viz.node_radius
5
"""
self.margin = margin
self.node_radius = node_radius
self.wire_width = wire_width
self.sink_color = sink_color
self.internal_color = internal_color
self.root_color = root_color
self.wire_color = wire_color
self.text_color = text_color
[docs]
def visualize_tree3d(
self,
root: Any,
sinks: List[Any],
filename: str = "clock_tree3d.svg",
width: int = 800,
height: int = 600,
analysis: Optional[Dict[str, Any]] = None,
) -> str:
"""
Create an SVG visualization of the clock tree3d
Args:
root: Root node of the clock tree3d
sinks: List of original sink objects
filename: Output filename
width: SVG width
height: SVG height
analysis: Optional analysis results from DME algorithm
Returns:
SVG string content
Examples:
>>> from physdes.cts.dme_algorithm import TreeNode, Sink
>>> from physdes.point import Point
>>> s1 = TreeNode(name="s1", position=Point(Point(10, 0), 20))
>>> s2 = TreeNode(name="s2", position=Point(Point(30, 0), 40))
>>> root = TreeNode(name="n1", position=Point(Point(20, 0), 30), left=s1, right=s2)
>>> s1.parent = root
>>> s2.parent = root
>>> sinks = [Sink("s1", Point(Point(10, 0), 20)), Sink("s2", Point(Point(30, 0), 40))]
>>> viz = ClockTree3dVisualizer()
>>> svg = viz.visualize_tree3d(root, sinks, filename="", width=200, height=200)
>>> '<circle cx="75.0" cy="75.0"' in svg
True
>>> '<circle cx="125.0" cy="125.0"' in svg
True
>>> '<circle cx="100.0" cy="100.0"' in svg
True
"""
# Collect all nodes and calculate bounds
all_nodes = self._collect_all_nodes(root)
min_x, min_y, max_x, max_y = self._calculate_bounds(all_nodes, sinks)
# Scale coordinates to fit SVG canvas
scale_x = (width - 2 * self.margin) / (max_x - min_x) if max_x > min_x else 1
scale_y = (height - 2 * self.margin) / (max_y - min_y) if max_y > min_y else 1
scale = min(scale_x, scale_y) # Maintain aspect ratio
def scale_coord(x_coord: float, y_coord: float) -> tuple[float, float]:
scaled_x = (x_coord - min_x) * scale + self.margin
scaled_y = (y_coord - min_y) * scale + self.margin
return scaled_x, scaled_y
# Create SVG content
svg_content = [
f'<svg width="{width}" height="{height}" xmlns="http://www.w3.org/2000/svg">',
"<style>",
" .node-label { font: 10px sans-serif; fill: #333; }",
" .delay-label { font: 8px sans-serif; fill: #666; }",
" .wire-label { font: 9px sans-serif; fill: #444; }",
" .analysis-label { font: 12px sans-serif; fill: #333; }",
"</style>",
'<rect width="100%" height="100%" fill="white"/>',
'<g class="clock-tree3d">',
]
# Draw wires first (so they appear behind nodes)
svg_content.extend(self._draw_wires3d(root, scale_coord))
# Draw nodes
svg_content.extend(self._draw_nodes(root, sinks, scale_coord))
# Add analysis information if provided
if analysis:
svg_content.extend(self._create_analysis_box(analysis, width))
# Close SVG
svg_content.extend(["</g>", "</svg>"])
svg_string = "\n".join(svg_content)
# Save to file
if filename:
with open(filename, "w") as f:
f.write(svg_string)
print(f"Clock tree3d visualization saved to {filename}")
return svg_string
def _collect_all_nodes(self, root: Any) -> List[Any]:
"""Collect all nodes in the tree3d"""
nodes = []
def collect(node: Any) -> None:
if node:
nodes.append(node)
collect(node.left)
collect(node.right)
collect(root)
return nodes
def _calculate_bounds(
self, nodes: List, sinks: List
) -> Tuple[float, float, float, float]:
"""Calculate the bounding box of all nodes and sinks"""
all_points = []
# Add tree3d nodes
for node in nodes:
all_points.append((node.position.xcoord.xcoord, node.position.ycoord))
# Add original sinks (in case some are not in the tree3d)
for sink in sinks:
all_points.append((sink.position.xcoord.xcoord, sink.position.ycoord))
if not all_points:
return 0, 0, 100, 100
min_x = min(x_coord for x_coord, y_coord in all_points)
max_x = max(x_coord for x_coord, y_coord in all_points)
min_y = min(y_coord for x_coord, y_coord in all_points)
max_y = max(y_coord for x_coord, y_coord in all_points)
# Add some padding
padding = max((max_x - min_x) * 0.1, (max_y - min_y) * 0.1, 10)
return (min_x - padding, min_y - padding, max_x + padding, max_y + padding)
def _draw_wires3d(
self, root: Any, scale_coord: Callable[[Any, Any], Tuple[float, float]]
) -> List[str]:
"""Draw all wires in the clock tree3d"""
svg_elements = []
def draw_wires_recursive(node: Any) -> None:
if not node:
return
if node.parent:
# Draw wire from parent to current node
x_start, y_start = scale_coord(
node.parent.position.xcoord.xcoord, node.parent.position.ycoord
)
x_end, y_end = scale_coord(
node.position.xcoord.xcoord, node.position.ycoord
)
svg_elements.append(
f'<line x1="{x_start}" y1="{y_start}" x2="{x_end}" y2="{y_end}" stroke="{self.wire_color}" stroke-width="{self.wire_width}" stroke-linecap="round"/>'
)
# Add wire length label
mid_x = (x_start + x_end) / 2
mid_y = (y_start + y_end) / 2
if hasattr(node, "wire_length") and node.wire_length > 0:
svg_elements.append(
f'<text x="{mid_x}" y="{mid_y - 5}" class="wire-label" text-anchor="middle">{node.wire_length:.1f}</text>'
)
draw_wires_recursive(node.left)
draw_wires_recursive(node.right)
draw_wires_recursive(root)
return svg_elements
def _draw_nodes(
self,
root: Any,
sinks: List[Any],
scale_coord: Callable[[Any, Any], Tuple[float, float]],
) -> List[str]:
"""Draw all nodes in the clock tree3d"""
svg_elements = []
sink_positions = {
(sink.position.xcoord.xcoord, sink.position.ycoord) for sink in sinks
}
def draw_nodes_recursive(node: Any, depth: int = 0) -> None:
if not node:
return
x_pos, y_pos = scale_coord(
node.position.xcoord.xcoord, node.position.ycoord
)
# Determine node type and color
is_sink = (
node.position.xcoord.xcoord,
node.position.ycoord,
) in sink_positions
is_root = node.parent is None
if is_root:
color = self.root_color
radius = self.node_radius + 2
elif is_sink:
color = self.sink_color
radius = self.node_radius
else:
color = self.internal_color
radius = self.node_radius - 2
# Draw node circle
svg_elements.append(
f'<circle cx="{x_pos}" cy="{y_pos}" r="{radius}" fill="{color}" stroke="#333" stroke-width="1"/>'
)
# Draw node label
label_y_offset = -radius - 5
svg_elements.append(
f'<text x="{x_pos}" y="{y_pos + label_y_offset}" class="node-label" text-anchor="middle">{node.name}</text>'
)
# Draw delay information if available
if hasattr(node, "delay"):
delay_y_offset = radius + 12
svg_elements.append(
f'<text x="{x_pos}" y="{y_pos + delay_y_offset}" class="delay-label" text-anchor="middle">d:{node.delay:.1f}</text>'
)
# Draw capacitance information for sinks
if is_sink and hasattr(node, "capacitance"):
cap_y_offset = radius + 22
svg_elements.append(
f'<text x="{x_pos}" y="{y_pos + cap_y_offset}" class="delay-label" text-anchor="middle">c:{node.capacitance:.1f}</text>'
)
draw_nodes_recursive(node.left, depth + 1)
draw_nodes_recursive(node.right, depth + 1)
draw_nodes_recursive(root)
return svg_elements
def _create_analysis_box(
self, analysis: Dict[str, Any], svg_width: int
) -> List[str]:
"""Create analysis information box"""
delay_model = analysis.get("delay_model", "Unknown")
analysis_text = [
"Clock Tree Analysis",
f"Delay Model: {delay_model}",
f"Max Delay: {analysis.get('max_delay', 0):.3f}",
f"Min Delay: {analysis.get('min_delay', 0):.3f}",
f"Skew: {analysis.get('skew', 0):.3f}",
f"Total Wirelength: {analysis.get('total_wirelength', 0):.1f}",
f"Sinks: {len(analysis.get('sink_delays', []))}",
]
analysis_box = [
'<g class="analysis-info">',
'<rect x="10" y="10" width="220" height="140" fill="white" stroke="#ccc" stroke-width="1" rx="5"/>',
'<rect x="10" y="10" width="220" height="20" fill="#f0f0f0" stroke="#ccc" stroke-width="1" rx="5"/>',
'<text x="20" y="25" font-family="sans-serif" font-size="12" font-weight="bold" fill="#333">Clock Tree Analysis</text>',
'<text x="20" y="45" font-family="monospace" font-size="11" fill="#333">',
]
for idx, text in enumerate(analysis_text[1:]): # Skip title line
analysis_box.append(
f'<tspan x="20" y="{45 + (idx + 1) * 16}">{text}</tspan>'
)
analysis_box.append("</text>")
analysis_box.append("</g>")
return analysis_box
[docs]
def create_interactive_svg(
root: Any,
sinks: List[Any],
analysis: Optional[Dict[str, Any]],
filename: str = "clock_tree3d_interactive.svg",
width: int = 1000,
height: int = 700,
) -> str:
"""
Create an interactive SVG with additional information and styling
Args:
root: Root node of clock tree3d
sinks: List of sink objects
analysis: Skew analysis results
filename: Output filename
width: SVG width
height: SVG height
Returns:
SVG string content
"""
visualizer = ClockTree3dVisualizer(
margin=60,
node_radius=10,
wire_width=3,
sink_color="#2E7D32",
internal_color="#1565C0",
root_color="#C62828",
wire_color="#455A64",
text_color="#263238",
)
svg_content = visualizer.visualize_tree3d(
root, sinks, filename, width, height, analysis
)
return svg_content
[docs]
def create_comparison_visualization(
trees_data: List[Dict[str, Any]],
filename: str = "clock_tree3d_comparison.svg",
width: int = 1200,
height: int = 800,
) -> str:
"""
Create a comparison visualization of multiple clock trees
Args:
trees_data: List of dictionaries containing:
- 'tree': root node
- 'sinks': list of sinks
- 'analysis': analysis results
- 'title': descriptive title
filename: Output filename
width: SVG width
height: SVG height
Returns:
SVG string content
Examples:
>>> from physdes.cts.dme_algorithm import TreeNode, Sink
>>> from physdes.point import Point
>>> tree1 = TreeNode("root1", Point(Point(50,0), 50))
>>> tree2 = TreeNode("root2", Point(Point(150,0), 50))
>>> sinks = [Sink("s1", Point(Point(10,0), 20)), Sink("s2", Point(Point(30,0), 40))]
>>> analysis = {"skew": 0.1, "max_delay": 5.0, "total_wirelength": 100}
>>> data = [
... {"tree": tree1, "sinks": sinks, "analysis": analysis, "title": "Linear Model"},
... {"tree": tree2, "sinks": sinks, "analysis": analysis, "title": "Elmore Model"}
... ]
>>> svg = create_comparison_visualization(data, "", 800, 400)
>>> "Linear Model" in svg
True
"""
if not trees_data:
raise ValueError("No tree data provided for comparison")
num_trees = len(trees_data)
cols = min(2, num_trees) # Maximum 2 columns
rows = (num_trees + cols - 1) // cols
sub_width = width // cols
sub_height = height // rows
svg_content = [
f'<svg width="{width}" height="{height}" xmlns="http://www.w3.org/2000/svg">',
"<style>",
" .node-label { font: 8px sans-serif; fill: #333; }",
" .delay-label { font: 7px sans-serif; fill: #666; }",
" .title { font: 14px sans-serif; fill: #333; font-weight: bold; }",
" .comparison-label { font: 10px sans-serif; fill: #333; }",
"</style>",
'<rect width="100%" height="100%" fill="white"/>',
]
visualizer = ClockTree3dVisualizer(
margin=40,
node_radius=6,
wire_width=2,
sink_color="#4CAF50",
internal_color="#2196F3",
root_color="#F44336",
)
for i, tree_data in enumerate(trees_data):
row = i // cols
col = i % cols
offset_x = col * sub_width
offset_y = row * sub_height
# Add title for this subplot
svg_content.append(
f'<text x="{offset_x + sub_width // 2}" y="{offset_y + 20}" class="title" text-anchor="middle">{tree_data.get("title", f"Tree {i + 1}")}</text>'
)
# Create a temporary SVG for this tree
temp_svg = visualizer.visualize_tree3d(
tree_data["tree"],
tree_data["sinks"],
"", # No filename for temporary use
sub_width - 20,
sub_height - 40,
tree_data.get("analysis"),
)
# Extract the main content (between <g class="clock-tree3d"> and </g>)
lines = temp_svg.split("\n")
start_idx = -1
end_idx = -1
for line_idx, line in enumerate(lines):
if '<g class="clock-tree3d">' in line:
start_idx = line_idx
elif (
start_idx != -1
and "</g>" in line
and "clock-tree3d" not in lines[line_idx - 1]
):
end_idx = line_idx
break
if start_idx == -1 or end_idx == -1:
raise ValueError("Could not parse temporary SVG content")
tree_content = lines[start_idx + 1 : end_idx] # Skip <g> and </g>
# Remove the background rect if present
if (
tree_content
and '<rect width="100%" height="100%" fill="white"/>' in tree_content[0]
):
tree_content = tree_content[1:]
# Add transformed group
svg_content.append(
f'<g transform="translate({offset_x + 10}, {offset_y + 40})">'
)
svg_content.extend(tree_content)
svg_content.append("</g>")
svg_content.append("</g>") # why?
svg_content.append("</svg>")
svg_string = "\n".join(svg_content)
if filename:
with open(filename, "w") as f:
f.write(svg_string)
print(f"Comparison visualization saved to {filename}")
return svg_string
[docs]
def create_delay_model_comparison3d(
linear_tree_data: Dict[str, Any],
elmore_tree_data: Dict[str, Any],
filename: str = "delay_model_comparison3d.svg",
) -> str:
"""
Create a specialized comparison between linear and Elmore delay models
Args:
linear_tree_data: Data for linear delay model tree
elmore_tree_data: Data for Elmore delay model tree
filename: Output filename
Returns:
SVG string content
"""
linear_tree_data["title"] = "Linear Delay Model"
elmore_tree_data["title"] = "Elmore Delay Model"
return create_comparison_visualization(
[linear_tree_data, elmore_tree_data], filename, width=1200, height=600
)
# Example usage function
[docs]
def visualize_example_tree3d() -> Tuple[str, str, str]:
"""Example function demonstrating clock tree3d visualization with different delay models"""
from physdes.cts.dme_algorithm import (
DMEAlgorithm,
ElmoreDelayCalculator,
LinearDelayCalculator,
)
# Generate example clock tree3d with both delay models
example_sinks = [
Sink("s1", Point(Point(-100, 0), 40), 1.0),
Sink("s2", Point(Point(-60, 0), 60), 1.0),
Sink("s3", Point(Point(0, 0), 40), 1.0),
Sink("s4", Point(Point(20, 0), 20), 1.0),
Sink("s5", Point(Point(-20, 0), -20), 1.0),
Sink("s6", Point(Point(-30, 0), -50), 1.0),
Sink("s7", Point(Point(-100, 0), -40), 1.0),
Sink("s8", Point(Point(-100, 0), 0), 1.0),
]
print("=== Generating Clock Trees with Different Delay Models ===")
source = Point(Point(40, 0), -10)
# Linear delay model
linear_calc = LinearDelayCalculator(delay_per_unit=0.5, capacitance_per_unit=0.2)
dme_linear = DMEAlgorithm(example_sinks, linear_calc, source)
clock_tree_linear = dme_linear.build_clock_tree()
analysis_linear = dme_linear.analyze_skew(clock_tree_linear)
# Elmore delay model
elmore_calc = ElmoreDelayCalculator(unit_resistance=0.1, unit_capacitance=0.2)
dme_elmore = DMEAlgorithm(example_sinks, elmore_calc, source)
clock_tree_elmore = dme_elmore.build_clock_tree()
analysis_elmore = dme_elmore.analyze_skew(clock_tree_elmore)
# Create individual visualizations
visualizer = ClockTree3dVisualizer()
# Linear model visualization
linear_svg = visualizer.visualize_tree3d(
clock_tree_linear,
example_sinks,
"linear_model_clock_tree3d.svg",
analysis=analysis_linear,
)
# Elmore model visualization
elmore_svg = visualizer.visualize_tree3d(
clock_tree_elmore,
example_sinks,
"elmore_model_clock_tree3d.svg",
analysis=analysis_elmore,
)
# Comparison visualization
linear_data = {
"tree": clock_tree_linear,
"sinks": example_sinks,
"analysis": analysis_linear,
"title": "Linear Delay Model",
}
elmore_data = {
"tree": clock_tree_elmore,
"sinks": example_sinks,
"analysis": analysis_elmore,
"title": "Elmore Delay Model",
}
comparison_svg = create_delay_model_comparison3d(linear_data, elmore_data)
print("Visualizations created:")
print("- linear_model_clock_tree3d.svg: Linear delay model")
print("- elmore_model_clock_tree3d.svg: Elmore delay model")
print("- delay_model_comparison3d.svg: Side-by-side comparison")
return linear_svg, elmore_svg, comparison_svg
if __name__ == "__main__":
# Run example visualization
linear_svg, elmore_svg, comparison_svg = visualize_example_tree3d()
doctest.testmod()