Source code for physdes.router.routing_visualizer

import doctest
from typing import TYPE_CHECKING, List, Optional

from physdes.interval import Interval
from physdes.point import Point
from physdes.router.routing_tree import NodeType

if TYPE_CHECKING:
    from physdes.router.routing_tree import GlobalRoutingTree, RoutingNode


[docs] def visualize_routing_tree_svg( tree: "GlobalRoutingTree", keepouts: Optional[List[Point[Interval[int], Interval[int]]]] = None, width: int = 800, height: int = 600, margin: int = 50, ) -> str: """ Visualize a GlobalRoutingTree in SVG format. Args: tree: GlobalRoutingTree instance to visualize width: SVG canvas width height: SVG canvas height margin: Margin around the drawing area Returns: SVG string representation Examples: >>> from physdes.point import Point >>> from physdes.router.routing_tree import GlobalRoutingTree >>> tree = GlobalRoutingTree(Point(0, 0)) >>> s1 = tree.insert_steiner_node(Point(1, 1)) >>> t1 = tree.insert_terminal_node(Point(2, 2), s1) >>> svg = visualize_routing_tree_svg(tree, width=200, height=200) >>> '<circle cx="50.0" cy="50.0"' in svg True >>> '<circle cx="100.0" cy="100.0"' in svg True >>> '<circle cx="150.0" cy="150.0"' in svg True """ # Calculate bounds to scale the coordinates all_nodes = list(tree.nodes.values()) if not all_nodes: return "<svg></svg>" # Get all coordinates to determine bounds all_x = [node.pt.xcoord for node in all_nodes] all_y = [node.pt.ycoord for node in all_nodes] min_x, max_x = min(all_x), max(all_x) min_y, max_y = min(all_y), max(all_y) # Add some padding to bounds range_x = max_x - min_x range_y = max_y - min_y if range_x == 0: range_x = 1 if range_y == 0: range_y = 1 # Scale factors scale_x = (width - 2 * margin) / range_x scale_y = (height - 2 * margin) / range_y scale = min(scale_x, scale_y) def scale_coords(x_coord: float, y_coord: float) -> tuple[float, float]: """Scale coordinates to fit SVG canvas""" scaled_x = margin + (x_coord - min_x) * scale scaled_y = margin + (y_coord - min_y) * scale return scaled_x, scaled_y svg_parts = [] # SVG header svg_parts.append( f'<svg width="{width}" height="{height}" xmlns="http://www.w3.org/2000/svg">' ) svg_parts.append('<rect width="100%" height="100%" fill="white"/>') # Draw connections first (so nodes appear on top) def draw_connections(node: "RoutingNode") -> None: for child in node.children: # Get scaled coordinates x_start, y_start = scale_coords(node.pt.xcoord, node.pt.ycoord) x_end, y_end = scale_coords(child.pt.xcoord, child.pt.ycoord) # Draw line svg_parts.append( f'<line x1="{x_start}" y1="{y_start}" x2="{x_end}" y2="{y_end}" stroke="black" stroke-width="2" marker-end="url(#arrowhead)"/>' ) for child in node.children: draw_connections(child) # Add arrowhead marker definition svg_parts.append("<defs>") svg_parts.append( '<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">' ) svg_parts.append('<polygon points="0 0, 10 3.5, 0 7" fill="black"/>') svg_parts.append("</marker>") svg_parts.append("</defs>") # Draw all connections starting from source draw_connections(tree.source) # Draw nodes for node in all_nodes: x_pos, y_pos = scale_coords(node.pt.xcoord, node.pt.ycoord) # Different colors and sizes for different node types if node.type == NodeType.SOURCE: color = "red" radius = 8 label = "S" elif node.type == NodeType.STEINER: color = "blue" radius = 6 label = f"S{node.id.split('_')[1]}" elif node.type == NodeType.TERMINAL: color = "green" radius = 6 label = f"T{node.id.split('_')[1]}" else: color = "gray" radius = 5 label = node.id # Draw node circle svg_parts.append( f'<circle cx="{x_pos}" cy="{y_pos}" r="{radius}" fill="{color}" stroke="black" stroke-width="1"/>' ) # Draw node label svg_parts.append( f'<text x="{x_pos + radius + 2}" y="{y_pos + 4}" font-family="Arial" font-size="10" fill="black">{label}</text>' ) # Draw coordinates svg_parts.append( f'<text x="{x_pos}" y="{y_pos - radius - 5}" font-family="Arial" font-size="8" fill="gray" text-anchor="middle">({node.pt.xcoord},{node.pt.ycoord})</text>' ) # Draw keepouts if keepouts is not None: for keepout in keepouts: x1, y1 = scale_coords(keepout.xcoord.lb, keepout.ycoord.lb) x2, y2 = scale_coords(keepout.xcoord.ub, keepout.ycoord.ub) rect_width = x2 - x1 rect_height = y2 - y1 color = "orange" svg_parts.append( f'<rect x="{x1}" y="{y1}" width="{rect_width}" height = "{rect_height}" fill="{color}" stroke="black" stroke-width="1"/>' ) # Add legend legend_y = 20 svg_parts.append( f'<text x="20" y="{legend_y}" font-family="Arial" font-size="12" font-weight="bold">Legend:</text>' ) legend_items = [ ("Source", "red", 20, legend_y + 20), ("Steiner", "blue", 20, legend_y + 40), ("Terminal", "green", 20, legend_y + 60), ] for text, color, x_pos, y_pos in legend_items: svg_parts.append( f'<circle cx="{x_pos}" cy="{y_pos - 4}" r="4" fill="{color}" stroke="black"/>' ) svg_parts.append( f'<text x="{x_pos + 10}" y="{y_pos}" font-family="Arial" font-size="10">{text}</text>' ) # Display statistics stats_y = legend_y + 90 svg_parts.append( f'<text x="20" y="{stats_y}" font-family="Arial" font-size="10" font-weight="bold">Statistics:</text>' ) svg_parts.append( f'<text x="20" y="{stats_y + 15}" font-family="Arial" font-size="9">Total Nodes: {len(tree.nodes)}</text>' ) svg_parts.append( f'<text x="20" y="{stats_y + 30}" font-family="Arial" font-size="9">Terminals: {len(tree.get_all_terminals())}</text>' ) svg_parts.append( f'<text x="20" y="{stats_y + 45}" font-family="Arial" font-size="9">Steiner: {len(tree.get_all_steiner_nodes())}</text>' ) svg_parts.append( f'<text x="20" y="{stats_y + 60}" font-family="Arial" font-size="9">Wirelength: {tree.calculate_total_wirelength():.2f}</text>' ) svg_parts.append("</svg>") return "\n".join(svg_parts)
[docs] def save_routing_tree_svg( tree: "GlobalRoutingTree", keepouts: Optional[List[Point[Interval[int], Interval[int]]]] = None, filename: str = "routing_tree.svg", width: int = 800, height: int = 600, ) -> None: """ Save the routing tree visualization as an SVG file. Args: tree: GlobalRoutingTree instance filename: Output filename width: SVG canvas width height: SVG canvas height """ svg_content = visualize_routing_tree_svg(tree, keepouts, width, height, 50) with open(filename, "w") as f: f.write(svg_content) print(f"Routing tree saved to {filename}")
[docs] def visualize_routing_tree3d_svg( tree3d: "GlobalRoutingTree", keepouts: Optional[ List[Point[Point[Interval[int], Interval[int]], Interval[int]]] ] = None, scale_z: int = 100, width: int = 800, height: int = 600, margin: int = 50, ) -> str: """ Visualize a GlobalRoutingTree in SVG format. .. svgbob:: :align: center +z ^ | | +-----> +x / v +y Args: tree3d: GlobalRoutingTree instance to visualize width: SVG canvas width height: SVG canvas height margin: Margin around the drawing area Returns: SVG string representation Examples: >>> from physdes.point import Point >>> from physdes.router.routing_tree import GlobalRoutingTree >>> tree = GlobalRoutingTree(Point(Point(0, 0), 0)) >>> s1 = tree.insert_steiner_node(Point(Point(1, 0), 1)) >>> t1 = tree.insert_terminal_node(Point(Point(2, 0), 2), s1) >>> svg = visualize_routing_tree3d_svg(tree, width=200, height=200) >>> '<circle cx="50.0" cy="50.0"' in svg True >>> '<circle cx="100.0" cy="100.0"' in svg True >>> '<circle cx="150.0" cy="150.0"' in svg True """ # Calculate bounds to scale the coordinates all_nodes = list(tree3d.nodes.values()) if not all_nodes: return "<svg></svg>" layer_colors = ["red", "orange", "blue", "green"] # Get all coordinates to determine bounds all_x = [node.pt.xcoord.xcoord for node in all_nodes] # all_z = [node.pt.xcoord.ycoord for node in all_nodes] all_y = [node.pt.ycoord for node in all_nodes] min_x, max_x = min(all_x), max(all_x) min_y, max_y = min(all_y), max(all_y) # Add some padding to bounds range_x = max_x - min_x range_y = max_y - min_y if range_x == 0: range_x = 1 if range_y == 0: range_y = 1 # Scale factors scale_x = (width - 2 * margin) / range_x scale_y = (height - 2 * margin) / range_y scale = min(scale_x, scale_y) def scale_coords(x_coord: float, y_coord: float) -> tuple[float, float]: """Scale coordinates to fit SVG canvas""" scaled_x = margin + (x_coord - min_x) * scale scaled_y = margin + (y_coord - min_y) * scale return scaled_x, scaled_y svg_parts = [] # SVG header svg_parts.append( f'<svg width="{width}" height="{height}" xmlns="http://www.w3.org/2000/svg">' ) svg_parts.append('<rect width="100%" height="100%" fill="white"/>') # Draw connections first (so nodes appear on top) def draw_connections(node: "RoutingNode") -> None: for child in node.children: # Get scaled coordinates x_start, y_start = scale_coords(node.pt.xcoord.xcoord, node.pt.ycoord) x_end, y_end = scale_coords(child.pt.xcoord.xcoord, child.pt.ycoord) color = layer_colors[ (child.pt.xcoord.ycoord // scale_z) % len(layer_colors) ] # Draw line svg_parts.append( f'<line x1="{x_start}" y1="{y_start}" x2="{x_end}" y2="{y_end}" stroke="{color}" stroke-width="2" marker-end="url(#arrowhead)"/>' ) for child in node.children: draw_connections(child) # Add arrowhead marker definition svg_parts.append("<defs>") svg_parts.append( '<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">' ) svg_parts.append('<polygon points="0 0, 10 3.5, 0 7" fill="black"/>') svg_parts.append("</marker>") svg_parts.append("</defs>") # Draw all connections starting from source draw_connections(tree3d.source) # Draw nodes for node in all_nodes: x_pos, y_pos = scale_coords(node.pt.xcoord.xcoord, node.pt.ycoord) # Different colors and sizes for different node types if node.type == NodeType.SOURCE: color = "red" radius = 8 label = "S" elif node.type == NodeType.STEINER: color = "blue" radius = 6 label = f"S{node.id.split('_')[1]}" elif node.type == NodeType.TERMINAL: color = "green" radius = 6 label = f"T{node.id.split('_')[1]}" else: color = "gray" radius = 5 label = node.id # Draw node circle svg_parts.append( f'<circle cx="{x_pos}" cy="{y_pos}" r="{radius}" fill="{color}" stroke="black" stroke-width="1"/>' ) # Draw node label svg_parts.append( f'<text x="{x_pos + radius + 2}" y="{y_pos + 4}" font-family="Arial" font-size="10" fill="black">{label}</text>' ) # Draw coordinates svg_parts.append( f'<text x="{x_pos}" y="{y_pos - radius - 5}" font-family="Arial" font-size="8" fill="gray" text-anchor="middle">({node.pt.xcoord.xcoord},{node.pt.ycoord})</text>' ) # Draw keepouts if keepouts is not None: for keepout in keepouts: x1, y1 = scale_coords(keepout.xcoord.xcoord.lb, keepout.ycoord.lb) x2, y2 = scale_coords(keepout.xcoord.xcoord.ub, keepout.ycoord.ub) rect_width = x2 - x1 rect_height = y2 - y1 color = "pink" svg_parts.append( f'<rect x="{x1}" y="{y1}" width="{rect_width}" height = "{rect_height}" fill="{color}" stroke="black" stroke-width="1"/>' ) # Add legend legend_y = 20 svg_parts.append( f'<text x="20" y="{legend_y}" font-family="Arial" font-size="12" font-weight="bold">Legend:</text>' ) legend_items = [ ("Source", "red", 20, legend_y + 20), ("Steiner", "blue", 20, legend_y + 40), ("Terminal", "green", 20, legend_y + 60), ] for text, color, x_pos, y_pos in legend_items: svg_parts.append( f'<circle cx="{x_pos}" cy="{y_pos - 4}" r="4" fill="{color}" stroke="black"/>' ) svg_parts.append( f'<text x="{x_pos + 10}" y="{y_pos}" font-family="Arial" font-size="10">{text}</text>' ) legend_items = [ ("Source", "red", 20, legend_y + 20), ("Steiner", "blue", 20, legend_y + 40), ("Terminal", "green", 20, legend_y + 60), ] for text, color, x_coord, y_pos in legend_items: svg_parts.append( f'<circle cx="{x_coord}" cy="{y_pos - 4}" r="4" fill="{color}" stroke="black"/>' ) svg_parts.append( f'<text x="{x_coord + 10}" y="{y_pos}" font-family="Arial" font-size="10">{text}</text>' ) # Display statistics stats_y = legend_y + 90 svg_parts.append( f'<text x="20" y="{stats_y}" font-family="Arial" font-size="10" font-weight="bold">Statistics:</text>' ) svg_parts.append( f'<text x="20" y="{stats_y + 15}" font-family="Arial" font-size="9">Total Nodes: {len(tree3d.nodes)}</text>' ) svg_parts.append( f'<text x="20" y="{stats_y + 30}" font-family="Arial" font-size="9">Terminals: {len(tree3d.get_all_terminals())}</text>' ) svg_parts.append( f'<text x="20" y="{stats_y + 45}" font-family="Arial" font-size="9">Steiner: {len(tree3d.get_all_steiner_nodes())}</text>' ) svg_parts.append( f'<text x="20" y="{stats_y + 60}" font-family="Arial" font-size="9">Wirelength: {tree3d.calculate_total_wirelength():.2f}</text>' ) svg_parts.append("</svg>") return "\n".join(svg_parts)
[docs] def save_routing_tree3d_svg( tree3d: "GlobalRoutingTree", keepouts: Optional[ List[Point[Point[Interval[int], Interval[int]], Interval[int]]] ] = None, scale_z: int = 100, filename: str = "routing_tree3d.svg", width: int = 800, height: int = 600, ) -> None: """ Save the routing tree3d visualization as an SVG file. Args: tree3d: GlobalRoutingTree instance filename: Output filename width: SVG canvas width height: SVG canvas height """ svg_content = visualize_routing_tree3d_svg(tree3d, keepouts, scale_z, width, height) with open(filename, "w") as f: f.write(svg_content) print(f"Routing tree3d saved to {filename}")
if __name__ == "__main__": doctest.testmod()