| #!/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() |