"""
This script implements simple floorplanning visualization using SVG images.
The visualizer uses the information stored in each Verilog object (as a PROP)
to perform shaping and placement of each block.
but there is no explicit routing performed,
all the edges are connected from point to point.
Detail of properties of different objects
**On Definitions**
``SHAPE`` = Shape of the module [`Rect` (Default), `cross`, `custom`]
- Parameters for ``Rect``
- ``WIDTH`` and ``HEIGHT`` = The rectangular dimension of the module
- Parameters for ``cross``
- ``A``, ``B`` , ``C`` , ``D`` , ``E``, ``F`` = Dimensions of the rectilinear block (as show in figure below)
- Parameters for ``custom``
- It is represented by a sequence of numbers `<Start_direction(V/H)> <FirstPoint (int int)> <Sequence of vertical and horizontal distance>`, and the last point connects to the start point automatically
- For example following example creates rectangle: `V 0 0 10 20 20`
.. rst-class:: ascii
::
d
┌──────────┐
│ │
c│ │
b │ │ e
┌──────┘ └──────┐
│ │
a│ │
│ │
└──────┐ ┌──────┘
│ │
f│ │
│ │
└──────────┘
cross Shape
**On Instances**
``LOC_X`` and ``LOC_Y`` = Location of the component with respect to its parent
**Ports Placement**:
``SIDE``:
Shape size where module port is placed [left/right/bottom/top]
``SIDE2``:
Optional and valid only when shape in cross [left/right/bottom/top]
``OFFSET``:
Offset from the origin of that side
The first point on the respective side in a clockwise direction is considered as the origin
.. rst-class:: ascii
::
top/top
┌──────────┐
top/left │ | top/right
│ │
left/top │ │ right/top
┌──────┘ └──────┐
│ │
left/left │ 0 │ right/right
│ │
└──────┐ ┌──────┘
left/bottom │ │ right/bottom
│ │
bottom/left │ | bottom/right
└──────────┘
bottom/bottom
Representing SIDE/SIDE2 parameters
**TODO** Add Some sort of coordinate transformation which `scaleX` and `scaleY`.
All the inputs are in multiple SC_HEIGHT and SC_WIDTH, a default value
of these variables is set to 1
"""
import logging
import os
import spydrnet as sdn
from spydrnet.ir.outerpin import OuterPin
from svgwrite import Drawing
import numpy as np
from svgwrite.container import Group, Symbol
from svgwrite.shapes import Polyline
logger = logging.getLogger('spydrnet_logs')
base_dir = os.path.dirname(os.path.abspath(__file__))
PROJ_BASE_DIR = os.path.abspath(os.path.join(base_dir, ".."))
PROP = "VERILOG.InlineConstraints"
PIN_H = 4
PIN_W = 4
TOP_PIN = 1
BOTTOM_PIN = 2
LEFT_PIN = 3
RIGHT_PIN = 4
STYLE_SHEET = """
.origin{fill:black}
text{font-family: Verdana; font-size: 5px;}
.module_boundary{stroke:grey; stroke-width:1;opacity: 0.8}
.left_pin{
fill:blue;
text-anchor: start;
transform: translate(5px, 00px) scale(1,-1);}
.right_pin{
fill:blue;
text-anchor: end;
transform: translate(-5px, 00px) scale(1,-1);}
.bottom_pin{
fill:blue;
transform-box: fill-box;
transform-origin: start;
text-anchor: start;
transform: translate(0px, 10px) rotate(90deg) scale(1,-1);}
.top_pin{
fill:blue;
transform-box: fill-box;
transform-origin: bottom left;
text-anchor: start;
transform: translate(0px, -3px) rotate(-90deg) scale(1,-1);}
.in_pin{fill: red;}
.out_pin{fill: blue;}
"""
[docs]class FloorPlanViz:
'''
Implmenetation of SVG Visualiser `floorplan_visualizer <reference/visualization/floorplan_visualizer.rst>`_
'''
[docs] def __init__(self, definition, viewbox=(0, 0, 1000, 1000)):
"""
Initialise the class with definition to render.
Optionally, provide the Height and Width if its not set on the definition
itself
"""
self.module = definition
self.def_list = {} # Stores symbol refrences
self.view_w = 0 # This variable tracks the maximum width of the SVG
self.view_h = 0 # This variable tracks the maximum height of the SVG
self.skip_pins = True
self.custom_style = None
# Create SVG drawing
self.dwg = Drawing()
self.dwg.viewbox(*viewbox)
# Create groups in SVG image
self.dwgbg = self.dwg.add(Group(id="bg"))
self.core = self.dwg.add(Group(id="mainframe"))
t_prop = {"transform": "scale(1,-1)"}
self.dwgShapes = self.core.add(Group(id="mainShapes", **t_prop))
self.dwgText = self.core.add(Group(id="mainText", **t_prop))
self.dwgEdges = self.core.add(Group(id="edges", **t_prop))
@property
def custom_style_sheet(self):
'''
Return custom styles added in this visualiazer
'''
return self.custom_style
@custom_style_sheet.setter
def custom_style_sheet(self, value):
'''
Adds custom styles in this visualizer
'''
self.custom_style = value
[docs] def compose(self, skip_connections=False,
skip_pins=False,
filter_cables=(lambda x: True)) -> Drawing:
'''
Entry point to generate final SVG file
args:
skip_connections(bool) : Skip rednering connections beetween modules
skip_pins(bool) :Skip rendering modules pins
filter_cables(Callable): A callable function which filters the connections to redner
'''
# Create symbol for top-module and add in svg
self.skip_pins = skip_pins
self.add_symbol(self.module)
self.add_top_block(self.module)
# Iterate over all the instaces and place
for child in self.module.children:
self.add_symbol(child.reference)
self.add_block(child)
if skip_connections:
return
# create connections
for cable in self.module.get_cables():
if not filter_cables(cable):
continue
if cable.size:
points = []
# Extract all connection points
for p in cable.wires[0].pins:
x, y = 0, 0
if isinstance(p, OuterPin):
x = int(p.instance.data[PROP].get("LOC_X", 0))
y = int(p.instance.data[PROP].get("LOC_Y", 0))
m = p.instance.reference
x += int(m.data[PROP].get(f"{p.port.name}_X", 0))
y += int(m.data[PROP].get(f"{p.port.name}_Y", 0))
else:
x = int(self.module.data[PROP].get(
f"{p.port.name}_X", 0))
y = int(self.module.data[PROP].get(
f"{p.port.name}_Y", 0))
points.append((x, y))
# if connections found connect them in sequence
if(points):
self.dwgEdges.add(
Polyline(points, fill="none",
class_="edge",
stroke="black",
onmousemove=f"showTooltip(evt, '{cable.name}');",
onmouseout="hideTooltip();",
stroke_width="1"))
return self.dwg
[docs] def add_top_block(self, top_module):
"""
Adds top level block in the design
"""
name = top_module.name
defDict = self.def_list[name]
self.dwgShapes.add(self.dwg.use(defDict["instance"],
class_=f"topModule",
insert=(0, 0)))
self.dwgText.add(self.dwg.text(defDict["name"],
insert=(defDict["width"]*0.5,
-1*defDict["height"]*0.1),
fill="black",
transform="scale(1,-1)",
alignment_baseline="middle",
text_anchor="middle"))
self._update_viewbox(defDict["width"], defDict["height"])
[docs] def add_symbol(self, module):
"""
Inserts symbols in the SVG file
"""
if "ASSIG" in module.name:
return
if self.def_list.get(module.name, None):
return self.def_list[module.name]
shape = module.data[PROP].get("SHAPE", "rect")
if shape.lower() == "cross":
new_def = self._add_rect_linear_symbol(module)
elif shape.lower() == "custom":
new_def = self._add_custom_symbol(module)
else:
new_def = self._add_rect_symbol(module)
self.dwg.defs.add(new_def)
return new_def
[docs] def add_block(self, instance):
"""
Iterates over each instance and adds them in SVG file
"""
name = instance.reference.name
if "ASSIG" in name:
return
defDict = self.def_list[name]
loc_x = int(instance.data[PROP].get("LOC_X", 0))
loc_y = int(instance.data[PROP].get("LOC_Y", 0))
inst_label = instance.data[PROP].get("LABEL", "")
self.dwgShapes.add(self.dwg.use(defDict["instance"],
class_=f"{instance.name}",
insert=(loc_x, loc_y)))
module_name = self.dwg.tspan(text=f"[{instance.reference.name}]",
insert=self._get_label_location(instance),
dy=["1.2em", ])
module_text = self.dwg.text(f"{instance.name}",
insert=self._get_label_location(instance),
transform="scale(1,-1)",
fill="black",
alignment_baseline="middle",
text_anchor="middle")
module_text.add(module_name)
module_label = instance.reference.data[PROP].get('LABEL', None)
if module_label:
module_text.add(self.dwg.tspan(
insert=self._get_label_location(instance),
text=f"{module_label} {inst_label}",
dy=["2.4em", ]))
self.dwgText.add(module_text)
def _get_label_location(self, instance) -> tuple:
'''
Return the label location given the verilog instance
Always in the center of the shape
'''
defDict = self.def_list[instance.reference.name]
loc_x = int(instance.data[PROP].get("LOC_X", 0))
loc_y = int(instance.data[PROP].get("LOC_Y", 0))
loc_x += defDict["width"]*0.5
loc_y += defDict["height"]*0.5
loc_y *= -1
return (loc_x, loc_y)
# ===================================================
# Methods for shapes and pin addition
# ===================================================
def _add_rect_symbol(self, module: sdn.Definition) -> None:
width = int(module.data[PROP].get("WIDTH", 10))
height = int(module.data[PROP].get("HEIGHT", 10))
COLOR = module.data[PROP].get("COLOR", "#f4f0e6")
new_def = self.dwg.symbol(id=module.name)
self.def_list[module.name] = {
"name": module.name,
"instance": new_def,
"shape": "rect",
"points": None,
"width": width,
"height": height,
}
new_def.add(self.dwg.rect(insert=(1, 1),
size=(width-1, height-1),
fill=COLOR,
class_=f"module_boundary {module.name}"))
if not self.skip_pins:
self._add_rect_symbol_pins(module, new_def)
return new_def
def _add_rect_linear_symbol_pins(self, module: sdn.Definition, new_def: Symbol) -> None:
'''
Adds ``rect_linear`` modules in the SVG symbol list
'''
a, b, c, d, e, f = map(int, module.data[PROP].get("POINTS", [10]*6))
width = b+d+e
height = a+c+f
for port in module.ports:
p = port.name
SIDE = port.data[PROP].get(f"SIDE", "center")
SIDE2 = port.data[PROP].get(f"SIDE2", "center")
OFFSET = int(port.data[PROP].get(f"OFFSET", 0))
LOC_X, LOC_Y, PIN_DIR = {
"left": {
"top": (OFFSET, a+f, TOP_PIN),
"bottom": (OFFSET, f, BOTTOM_PIN),
"center": (0, f+OFFSET, LEFT_PIN),
},
"right": {
"top": (b+d+OFFSET, a+f, TOP_PIN),
"bottom": (b+d+OFFSET, f, BOTTOM_PIN),
"center": (width, f+OFFSET, RIGHT_PIN),
},
"bottom": {
"left": (b, OFFSET, LEFT_PIN),
"right": (b+d, OFFSET, RIGHT_PIN),
"center": (b+OFFSET, 0, BOTTOM_PIN),
},
"top": {
"left": (b, a+f+OFFSET, LEFT_PIN),
"right": (b+d, a+f+OFFSET, RIGHT_PIN),
"center": (b+OFFSET, height, TOP_PIN),
},
"center": {
"center": (width*0.5, height*0.5, TOP_PIN)
}}[SIDE][SIDE2]
pin_w, pin_h, mult = (PIN_W, PIN_H, -1) if PIN_DIR in [LEFT_PIN, RIGHT_PIN] \
else (PIN_H, PIN_W, 1)
new_def.add(self.dwg.rect(insert=(LOC_X-pin_w*0.5, LOC_Y-pin_h*0.5),
size=(pin_w, pin_h),
class_=f"module_pin {str(port.direction).split('.')[-1].lower()}_pin",
onmousemove=f"showTooltip(evt, '{port.name}');",
onmouseout="hideTooltip();",
stroke_width=0))
new_def.add(self.dwg.text(port.name,
insert=(LOC_X-pin_w*0.5,
mult * (LOC_Y-pin_h*0.5)),
class_=f"pin {SIDE}_pin {SIDE2}_pin",))
module.data[PROP][f"{p}_X"] = LOC_X
module.data[PROP][f"{p}_Y"] = LOC_Y
def _add_rect_linear_symbol(self, module: sdn.Definition) -> None:
a, b, c, d, e, f = map(int, module.data[PROP].get("POINTS", [10]*6))
COLOR = module.data[PROP].get("COLOR", "#f4f0e6")
new_def = self.dwg.symbol(id=module.name)
self.def_list[module.name] = {
"name": module.name,
"instance": new_def,
"shape": "cross",
"a": a, "b": b, "c": c,
"d": d, "e": e, "f": f,
"width": b+d+e,
"height": a+c+f,
}
path = f"M {b} 0 v {f} h {-1*b} v {a} h {b} "
path += f" v {c} h {d} v {-1*c} h {e} v {-1*a} "
path += f" h {-1*e} v {-1*f} Z"
path += f" Z"
new_def.add(self.dwg.path(d=path, fill=COLOR,
class_=f"module_boundary {module.name}"))
if not self.skip_pins:
self._add_rect_linear_symbol_pins(module, new_def)
return new_def
def _add_custom_symbol(self, module: sdn.Definition) -> None:
'''
Format = [<start_dir> 0 0 L1 L2 L3 L4 Z]
'''
path = module.data[PROP].get("POINTS", "v 0 0 10 10 -10 -10").split()
start_dir = path[0]
origin = path[1:3]
path = list(map(int, map(float, path[3:])))
COLOR = module.data[PROP].get("COLOR", "#f4f0e6")
minY = min(np.cumsum(path[::2]))
minX = min(np.cumsum(path[1::2]))
HEIGHT = max(np.cumsum(path[::2]))-minY
WIDTH = max(np.cumsum(path[1::2]))-minX
svg_path = "m {} {} ".format(*origin)
for eachpt in zip(path[::2], path[1::2]):
svg_path += "v {} h {} ".format(*eachpt)
svg_path += " z"
if start_dir.lower() == "h":
WIDTH, HEIGHT = HEIGHT, WIDTH
minX, minY = minY, minX
svg_path = svg_path.replace("v", "vv")
svg_path = svg_path.replace("h", "v")
svg_path = svg_path.replace("vv", "h")
new_def = self.dwg.symbol(id=module.name,
viewBox=f"{minX} {minY} {WIDTH} {HEIGHT} ")
# SVGwrite does not support addding width and height attribute to
# symbol element, tis is workaround
new_def._parameter.debug = False
new_def.attribs["width"] = WIDTH
new_def.attribs["height"] = HEIGHT
new_def.add(self.dwg.path(d=svg_path, fill=COLOR,
class_=f"module_boundary {module.name}"))
self.def_list[module.name] = {
"name": module.name,
"instance": new_def,
"shape": "custom",
"llx": origin[0],
"lly": origin[1],
"path": path,
"width": WIDTH,
"height": HEIGHT,
}
# TODO: Perform pin placement for the path shape
# if not self.skip_pins:
# self._add_rect_linear_symbol_pins(module, new_def)
return new_def
def _add_rect_symbol_pins(self, module: sdn.Definition, new_def: Symbol) -> None:
'''
Adds pins on the rectangular shpapes
'''
width = int(module.data[PROP].get("WIDTH", 10))
height = int(module.data[PROP].get("HEIGHT", 10))
for port in module.ports:
p = port.name
SIDE = port.data[PROP].get(f"SIDE", [])
OFFSET = int(port.data[PROP].get(f"OFFSET", 0))
if 'left' in SIDE:
LOC_X, LOC_Y = 2, OFFSET
pin_w, pin_h = PIN_W, PIN_H
mult = -1
elif 'right' in SIDE:
LOC_X, LOC_Y = width-1, height-OFFSET
pin_w, pin_h = PIN_W, PIN_H
mult = -1
elif 'bottom' in SIDE:
LOC_X, LOC_Y = width-OFFSET+1, 2
pin_w, pin_h = PIN_H, PIN_W
mult = 1
elif 'top' in SIDE:
LOC_X, LOC_Y = width-OFFSET+1, height-1-PIN_W
pin_w, pin_h = PIN_H, PIN_W
mult = 1
else:
LOC_X, LOC_Y = width/2, height/2
pin_w, pin_h = PIN_W, PIN_H
mult = -1
new_def.add(self.dwg.rect(insert=(LOC_X-pin_w*0.5, LOC_Y-pin_h*0.5),
size=(pin_w, pin_h),
class_=f"module_pin {str(port.direction).split('.')[-1].lower()}_pin",
onmousemove=f"showTooltip(evt, '{port.name}');",
onmouseout="hideTooltip();",
stroke_width=0))
new_def.add(self.dwg.text(port.name,
insert=(LOC_X-pin_w*0.5, mult *
(LOC_Y-pin_h*0.5)),
class_=f"pin {SIDE}_pin",))
# transform=f"translate({OFF_X}, {OFF_Y}) rotate({ROT}) " + "scale(1,-1)",
module.data[PROP][f"{p}_X"] = LOC_X
module.data[PROP][f"{p}_Y"] = LOC_Y
# ===================================================
# SVG Rendering Related
# ===================================================
def _update_viewbox(self, x, y):
'''
Updates the view box x and y value provided before saving image
'''
self.view_w = self.view_w if self.view_w > x else x
self.view_h = self.view_h if self.view_h > y else y
[docs] def add_stylehseet(self):
'''
Adds custom stylesheet to the SVG image
'''
self.dwg.defs.add(self.dwg.style(STYLE_SHEET))
if self.custom_style:
self.dwg.defs.add(self.dwg.style(self.custom_style))
def _add_background(self, bgColor="#FFF"):
self.dwgbg.add(self.dwg.rect(insert=(-25, -25),
size=(self.view_w+50,
self.view_h+50),
id=f"background",
fill=bgColor,
stroke_width=0))
self.dwg.viewbox(-50, -1*(self.view_h+100),
self.view_w+100, self.view_h+100)
self.dwg.add(self.dwg.rect(
insert=(0, 0), size=(10, 10), class_="origin"))
[docs] def get_svg(self):
'''
Returns SVG string of the current floorplan
'''
self.add_stylehseet()
self._add_background()
return self.dwg
[docs] def get_html(self):
'''
Adds the SVG image to HTML page which supports zoom and pan control using d3.js
'''
self.get_svg()
static_root = os.path.join(PROJ_BASE_DIR, "support_files")
with open(os.path.join(static_root, "html_templates",
"svg_render.html"), "rb") as fp:
content = fp.read()
script = """function load_svg() {
d3.select("#svgViewer").html(`%s`)}""" % self.dwg.tostring()
content = content.replace(
'onload="init_websocket();"'.encode(),
'onload="load_svg();"'.encode(),
)
content = content.replace(
'// <ADDITIONAL JS>'.encode(),
script.encode(),
)
return content