blob: 44724f5a8790ed6dd7faf1f3b7ae7259356a5145 [file] [log] [blame] [edit]
#!/usr/bin/env python3
"""
This script allows to read the tile grid from various sources and render it to
either SVG or PDF file. It can also draw connections between tiles when
provided with the 'tileconn.json' file used in the prjxray database.
Use ONE of following argument sets for data source specification:
1. --tilgrid <tilegrid.json> [--tileconn <tileconn.json>]
2. --arch-xml <arch.xml>
3. --graph-xml <rr_graph.xml>
4. --conn-db <channels.db> [--tb-table <tile table name>]
"""
import sys
import argparse
import os
import re
from collections import namedtuple
import progressbar
import json
import sqlite3
import lxml.etree as ET
import lxml.objectify as objectify
import svgwrite
# =============================================================================
class GridVisualizer(object):
BLOCK_RECT = 100
BLOCK_GAP = 10
BLOCK_SIZE = BLOCK_RECT + BLOCK_GAP
Loc = namedtuple("Loc", "x y")
Conn = namedtuple("Conn", "loc0 loc1")
GridExtent = namedtuple("GridExtent", "xmin ymin xmax ymax")
def __init__(self):
self.tilegrid = None
self.tileconn = None
self.grid_roi = None
self.conn_roi = None
self.tile_colormap = None
self.connections = {}
def load_tilegrid_from_json(self, json_file):
# Load JSON files
with open(json_file, "r") as fp:
self.tilegrid = json.load(fp)
self._determine_grid_extent()
self._build_loc_map()
def load_tileconn_from_json(self, json_file):
# Load JSON files
with open(json_file, "r") as fp:
self.tileconn = json.load(fp)
self._form_connections()
def load_tilegrid_from_arch_xml(self, xml_file):
# Load and parse the XML
parser = ET.XMLParser(remove_comments=True)
xml_tree = objectify.parse(xml_file, parser=parser)
xml_root = xml_tree.getroot()
# Get the layout section
layout = xml_root.find("layout")
assert (layout is not None)
# Get the fixed_layout section
fixed_layout = layout.find("fixed_layout")
assert (fixed_layout is not None)
# Extract the grid extent
dx = int(fixed_layout.get("width"))
dy = int(fixed_layout.get("height"))
self.grid_extent = self.GridExtent(0, 0, dx, dy)
# Convert
self.tilegrid = {}
for tile in list(fixed_layout):
assert (tile.tag == "single")
# Basic tile parameters
grid_x = int(tile.get("x"))
grid_y = int(tile.get("y"))
tile_type = tile.get("type")
# Tile name (if present)
tile_name = None
metadata = tile.find("metadata")
if metadata is not None:
for meta in metadata.findall("meta"):
if meta.get("name") == "fasm_prefix":
tile_name = meta.text
# Fake tile name
if tile_name is None:
tile_name = "UNKNOWN_X%dY%d" % (grid_x, grid_y)
# Already exists
if tile_name in self.tilegrid:
tile_name += "(2)"
self.tilegrid[tile_name] = {
"grid_x": grid_x,
"grid_y": grid_y,
"type": tile_type
}
self._build_loc_map()
def load_tilegrid_from_graph_xml(self, xml_file):
# Load and parse the XML
parser = ET.XMLParser(remove_comments=True)
xml_tree = objectify.parse(xml_file, parser=parser)
xml_root = xml_tree.getroot()
# Load block types
xml_block_types = xml_root.find("block_types")
assert (xml_block_types is not None)
block_types = {}
for xml_block_type in xml_block_types:
block_type_id = int(xml_block_type.get("id"))
block_name = xml_block_type.get("name")
block_types[block_type_id] = block_name
# Load grid
self.tilegrid = {}
all_x = set()
all_y = set()
xml_grid = xml_root.find("grid")
assert (xml_grid is not None)
for xml_grid_loc in xml_grid:
grid_x = int(xml_grid_loc.get("x"))
grid_y = int(xml_grid_loc.get("y"))
block_type_id = int(xml_grid_loc.get("block_type_id"))
all_x.add(grid_x)
all_y.add(grid_y)
# Fake tile name
tile_name = "BLOCK_X%dY%d" % (grid_x, grid_y)
self.tilegrid[tile_name] = {
"grid_x": grid_x,
"grid_y": grid_y,
"type": block_types[block_type_id]
}
# Determine grid extent
self.grid_extent = self.GridExtent(
min(all_x), min(all_y), max(all_x), max(all_y)
)
self._build_loc_map()
def load_tilegrid_from_conn_db(self, db_file, db_table):
# Connect to the database and load data
with sqlite3.Connection("file:%s?mode=ro" % db_file, uri=True) as conn:
c = conn.cursor()
# Load the grid
db_tiles = c.execute(
"SELECT pkey, name, tile_type_pkey, grid_x, grid_y FROM %s" %
(db_table)
).fetchall() # They say that it is insecure..
# Load tile types
db_tile_types = c.execute("SELECT pkey, name FROM tile_type"
).fetchall()
# Maps pkey to type string
tile_type_map = {}
for item in db_tile_types:
tile_type_map[item[0]] = item[1]
# Translate information
self.tilegrid = {}
all_x = set()
all_y = set()
for tile in db_tiles:
tile_type_pkey = tile[2]
if tile_type_pkey not in tile_type_map.keys():
print("Unknown tile type pkey %d !" % tile_type_pkey)
continue
tile_name = tile[1]
tile_type = tile_type_map[tile_type_pkey]
tile_grid_x = tile[3]
tile_grid_y = tile[4]
if tile_name in self.tilegrid:
print("Duplicate tile name '%s' !" % tile_name)
continue
all_x.add(tile_grid_x)
all_y.add(tile_grid_y)
self.tilegrid[tile_name] = {
"grid_x": tile_grid_x,
"grid_y": tile_grid_y,
"type": tile_type
}
# Determine grid extent
self.grid_extent = self.GridExtent(
min(all_x), min(all_y), max(all_x), max(all_y)
)
self._build_loc_map()
def load_tile_colormap(self, colormap):
# If it fails just skip it
try:
with open(colormap, "r") as fp:
self.tile_colormap = json.load(fp)
except FileNotFoundError:
pass
def set_grid_roi(self, roi):
self.grid_roi = roi
def set_conn_roi(self, roi):
self.conn_roi = roi
def _determine_grid_extent(self):
# Determine the grid extent
xs = set()
ys = set()
for tile in self.tilegrid.values():
xs.add(tile["grid_x"])
ys.add(tile["grid_y"])
self.grid_extent = self.GridExtent(min(xs), min(ys), max(xs), max(ys))
if self.grid_roi is not None:
self.grid_extent = self.GridExtent(
max(self.grid_extent.xmin, self.grid_roi[0]),
max(self.grid_extent.ymin, self.grid_roi[1]),
min(self.grid_extent.xmax, self.grid_roi[2]),
min(self.grid_extent.ymax, self.grid_roi[3])
)
def _build_loc_map(self):
self.loc_map = {}
for tile_name, tile in self.tilegrid.items():
loc = self.Loc(tile["grid_x"], tile["grid_y"])
if loc in self.loc_map.keys():
print("Duplicate tile at [%d, %d] !" % (loc.x, loc.y))
self.loc_map[loc] = tile_name
def _form_connections(self):
# Loop over tiles of interest
print("Forming connections...")
for tile_name, tile in progressbar.progressbar(self.tilegrid.items()):
this_loc = self.Loc(tile["grid_x"], tile["grid_y"])
this_type = tile["type"]
# Find matching connection rules
for rule in self.tileconn:
grid_deltas = rule["grid_deltas"]
tile_types = rule["tile_types"]
wire_count = len(rule["wire_pairs"])
for k in [+1]:
# Get a couterpart tile according to the rule grid delta
other_loc = self.Loc(
this_loc.x + k * grid_deltas[0],
this_loc.y + k * grid_deltas[1]
)
try:
other_name = self.loc_map[other_loc]
other_type = self.tilegrid[other_name]["type"]
except KeyError:
continue
# Check match
if this_type == tile_types[0] and \
other_type == tile_types[1]:
# Add the connection
conn = self.Conn(this_loc, other_loc)
if conn not in self.connections.keys():
self.connections[conn] = wire_count
else:
self.connections[conn] += wire_count
def _draw_connection(self, x0, y0, x1, y1, curve=False):
if curve:
dx = x1 - x0
dy = y1 - y0
cx = (x1 + x0) * 0.5 + dy * 0.33
cy = (y1 + y0) * 0.5 - dx * 0.33
path = "M %.3f %.3f " % (x0, y0)
path += "C %.3f %.3f %.3f %.3f %.3f %.3f" % \
(cx, cy, cx, cy, x1, y1)
self.svg.add(self.svg.path(d=path, fill="none", stroke="#000000"))
else:
self.svg.add(self.svg.line((x0, y0), (x1, y1), stroke="#000000"))
def _grid_to_drawing(self, x, y):
xc = (x - self.grid_extent.xmin + 1) * self.BLOCK_SIZE
yc = (y - self.grid_extent.ymin + 1) * self.BLOCK_SIZE
return xc, yc
def _create_drawing(self):
# Drawing size
self.svg_dx = (self.grid_extent.xmax - self.grid_extent.xmin + 2) \
* self.BLOCK_SIZE
self.svg_dy = (self.grid_extent.ymax - self.grid_extent.ymin + 2) \
* self.BLOCK_SIZE
# Create the drawing
self.svg = svgwrite.Drawing(
size=(self.svg_dx, self.svg_dy), profile="full", debug=False
)
def _get_tile_color(self, tile_name, tile):
tile_type = tile["type"]
# Match
if self.tile_colormap is not None:
for rule in self.tile_colormap:
# Match by tile name
if "name" in rule and re.match(rule["name"], tile_name):
return rule["color"]
# Match by tile type
if "type" in rule and re.match(rule["type"], tile_type):
return rule["color"]
# A default color
return "#C0C0C0"
def _draw_grid(self):
svg_tiles = []
svg_text = []
# Draw tiles
print("Drawing grid...")
for tile_name, tile in progressbar.progressbar(self.tilegrid.items()):
grid_x = tile["grid_x"]
grid_y = tile["grid_y"]
tile_type = tile["type"]
if self.grid_roi:
if grid_x < self.grid_roi[0] or grid_x > self.grid_roi[2]:
continue
if grid_y < self.grid_roi[1] or grid_y > self.grid_roi[3]:
continue
xc, yc = self._grid_to_drawing(grid_x, grid_y)
color = self._get_tile_color(tile_name, tile)
if color is None:
continue
font_size = self.BLOCK_RECT / 10
# Rectangle
svg_tiles.append(
self.svg.rect(
(
xc - self.BLOCK_RECT / 2,
(self.svg_dy - 1 - yc) - self.BLOCK_RECT / 2
), (self.BLOCK_RECT, self.BLOCK_RECT),
stroke="#C0C0C0",
fill=color
)
)
if grid_x & 1:
text_ofs = -font_size
else:
text_ofs = font_size
# Tile name
svg_text.append(
self.svg.text(
tile_name, (
xc - self.BLOCK_RECT / 2 + 2,
(self.svg_dy - 1 - yc) - font_size / 2 + text_ofs
),
font_size=font_size
)
)
# Tile type
svg_text.append(
self.svg.text(
tile_type, (
xc - self.BLOCK_RECT / 2 + 2,
(self.svg_dy - 1 - yc) + font_size / 2 + text_ofs
),
font_size=font_size
)
)
# Index
svg_text.append(
self.svg.text(
"X%dY%d" % (grid_x, grid_y), (
xc - self.BLOCK_RECT / 2 + 2,
(self.svg_dy - 1 - yc) + self.BLOCK_RECT / 2 - 2
),
font_size=font_size
)
)
# Add tiles to SVG
for item in svg_tiles:
self.svg.add(item)
# Add text to SVG
for item in svg_text:
self.svg.add(item)
def _draw_connections(self):
# Draw connections
print("Drawing connections...")
for conn, count in progressbar.progressbar(self.connections.items()):
if self.conn_roi:
if conn.loc0.x < self.grid_roi[0] or \
conn.loc0.x > self.grid_roi[2]:
if conn.loc1.x < self.grid_roi[0] or \
conn.loc1.x > self.grid_roi[2]:
continue
if conn.loc0.y < self.grid_roi[1] or \
conn.loc0.y > self.grid_roi[3]:
if conn.loc1.y < self.grid_roi[1] or \
conn.loc1.y > self.grid_roi[3]:
continue
dx = conn.loc1.x - conn.loc0.x
dy = conn.loc1.y - conn.loc0.y
xc0, yc0 = self._grid_to_drawing(conn.loc0.x, conn.loc0.y)
xc1, yc1 = self._grid_to_drawing(conn.loc1.x, conn.loc1.y)
max_count = int(self.BLOCK_RECT * 0.75 * 0.5)
line_count = min(count, max_count)
# Mostly horizontal
if abs(dx) > abs(dy):
for i in range(line_count):
k = 0.5 if line_count == 1 else i / (line_count - 1)
k = (k - 0.5) * 0.75
if dx > 0:
x0 = xc0 + self.BLOCK_RECT / 2
x1 = xc1 - self.BLOCK_RECT / 2
else:
x0 = xc0 - self.BLOCK_RECT / 2
x1 = xc1 + self.BLOCK_RECT / 2
y0 = yc0 + k * self.BLOCK_RECT
y1 = yc1 + k * self.BLOCK_RECT
self._draw_connection(
x0, (self.svg_dy - 1 - y0), x1, (self.svg_dy - 1 - y1),
True
)
# Mostly vertical
elif abs(dy) > abs(dx):
for i in range(line_count):
k = 0.5 if line_count == 1 else i / (line_count - 1)
k = (k - 0.5) * 0.75
if dy > 0:
y0 = yc0 + self.BLOCK_RECT / 2
y1 = yc1 - self.BLOCK_RECT / 2
else:
y0 = yc0 - self.BLOCK_RECT / 2
y1 = yc1 + self.BLOCK_RECT / 2
x0 = xc0 + k * self.BLOCK_RECT
x1 = xc1 + k * self.BLOCK_RECT
self._draw_connection(
x0, (self.svg_dy - 1 - y0), x1, (self.svg_dy - 1 - y1),
True
)
# Diagonal
else:
# FIXME: Do it in a more elegant way...
max_count = int(max_count * 0.40)
line_count = min(count, max_count)
for i in range(line_count):
k = 0.5 if line_count == 1 else i / (line_count - 1)
k = (k - 0.5) * 0.25
if (dx > 0) ^ (dy > 0):
x0 = xc0 + k * self.BLOCK_RECT
x1 = xc1 + k * self.BLOCK_RECT
y0 = yc0 + k * self.BLOCK_RECT
y1 = yc1 + k * self.BLOCK_RECT
else:
x0 = xc0 - k * self.BLOCK_RECT
x1 = xc1 - k * self.BLOCK_RECT
y0 = yc0 + k * self.BLOCK_RECT
y1 = yc1 + k * self.BLOCK_RECT
x0 += dx * self.BLOCK_RECT / 4
y0 += dy * self.BLOCK_RECT / 4
x1 -= dx * self.BLOCK_RECT / 4
y1 -= dy * self.BLOCK_RECT / 4
self._draw_connection(
x0, (self.svg_dy - 1 - y0), x1, (self.svg_dy - 1 - y1),
False
)
def run(self):
if self.grid_roi is not None:
if self.conn_roi is None:
self.conn_roi = self.grid_roi
else:
self.conn_roi[0] = max(self.conn_roi[0], self.grid_roi[0])
self.conn_roi[1] = max(self.conn_roi[1], self.grid_roi[1])
self.conn_roi[2] = min(self.conn_roi[2], self.grid_roi[2])
self.conn_roi[3] = min(self.conn_roi[3], self.grid_roi[3])
self._create_drawing()
self._draw_grid()
self._draw_connections()
def save(self, file_name):
self.svg.saveas(file_name)
# =============================================================================
def main():
# Parse arguments
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument(
"--tilegrid",
type=str,
default=None,
help="Project X-Ray 'tilegrid.json' file"
)
parser.add_argument(
"--tileconn",
type=str,
default=None,
help="Project X-Ray 'tileconn.json' file"
)
parser.add_argument(
"--arch-xml",
type=str,
default=None,
help="Architecture definition XML file"
)
parser.add_argument(
"--graph-xml", type=str, default=None, help="Routing graph XML file"
)
parser.add_argument(
'--conn-db',
type=str,
default=None,
help='Connection SQL database (eg. "channels.db")'
)
parser.add_argument(
'--db-table',
type=str,
default="tile",
help='Table name in the SQL database to read (def. "tile")'
)
parser.add_argument(
"--colormap",
type=str,
default=None,
help="JSON file with tile coloring rules"
)
parser.add_argument(
"--grid-roi",
type=int,
nargs=4,
default=None,
help="Grid ROI to draw (x0 y0 x1 y1)"
)
parser.add_argument(
"--conn-roi",
type=int,
nargs=4,
default=None,
help="Connection ROI to draw (x0 y0 x1 y1)"
)
parser.add_argument(
"-o", type=str, default="layout.svg", help="Output SVG file name"
)
if len(sys.argv) <= 1:
parser.print_help()
exit(1)
args = parser.parse_args()
script_path = os.path.dirname(os.path.realpath(__file__))
if args.colormap is None:
args.colormap = os.path.join(script_path, "tile_colormap.json")
# Create the visualizer
visualizer = GridVisualizer()
# Set ROI
visualizer.set_grid_roi(args.grid_roi)
visualizer.set_conn_roi(args.conn_roi)
# Load arch XML file
if args.arch_xml is not None:
visualizer.load_tilegrid_from_arch_xml(args.arch_xml)
# Load routing graph XML
elif args.graph_xml is not None:
visualizer.load_tilegrid_from_graph_xml(args.graph_xml)
# Load JSON files
elif args.tilegrid is not None:
visualizer.load_tilegrid_from_json(args.tilegrid)
if args.tileconn is not None:
visualizer.load_tileconn_from_json(args.tileconn)
# Load SQL database
elif args.conn_db is not None:
visualizer.load_tilegrid_from_conn_db(args.conn_db, args.db_table)
# No data input
else:
raise RuntimeError("No input data specified")
# Load tile colormap
if args.colormap:
visualizer.load_tile_colormap(args.colormap)
# Do the visualization
visualizer.run()
# Save drawing
if args.o.endswith(".svg"):
print("Saving SVG...")
visualizer.save(args.o)
elif args.o.endswith(".pdf"):
print("Saving PDF...")
from cairosvg import svg2pdf
svg2pdf(visualizer.svg.tostring(), write_to=args.o)
else:
print("Unknown output file type '{}'".format(args.o))
exit(-1)
print("Done.")
# =============================================================================
if __name__ == "__main__":
main()