blob: 3fcd7c0745fc0188729c80c7ceecbcc128247edc [file] [log] [blame] [edit]
#!/usr/bin/env python3
"""
Utilities for handling VTR packed netlist (.net) data
VPR packed netlist format specification:
https://docs.verilogtorouting.org/en/latest/vpr/file_formats/#packed-netlist-format-net
"""
import re
import lxml.etree as ET
from block_path import PathNode
# =============================================================================
class Connection:
"""
A class representing a connection of a port pin. For output ports it
refers to child/sibling block, port and pin while for input ports it refers
to the parent/sibling block, port and pin.
"""
# A regex for parsing connection specification
REGEX = re.compile(
r"(?P<driver>\S+)\.(?P<port>\S+)\[(?P<pin>[0-9]+)\]->"
r"(?P<interconnect>\S+)"
)
def __init__(self, driver, port, pin, interconnect):
"""
Basic constructor
"""
self.driver = driver
self.port = port
self.pin = pin
self.interconnect = interconnect
@staticmethod
def from_string(spec):
"""
Parses a specification string. Returns a Connection object
>>> Connection.from_string("parent.A[3]->interconnect")
parent.A[3]->interconnect
>>> Connection.from_string("parent.A->interconnect")
Traceback (most recent call last):
...
AssertionError: parent.A->interconnect
>>> Connection.from_string("parent.A[3]")
Traceback (most recent call last):
...
AssertionError: parent.A[3]
>>> Connection.from_string("Something")
Traceback (most recent call last):
...
AssertionError: Something
"""
assert isinstance(spec, str)
# Try match
match = Connection.REGEX.fullmatch(spec)
assert match is not None, spec
# Extract data
return Connection(
driver=match.group("driver"),
port=match.group("port"),
pin=int(match.group("pin")),
interconnect=match.group("interconnect")
)
def to_string(self):
"""
Builds a specification string that can be stored in packed netlist
"""
return "{}.{}[{}]->{}".format(
self.driver, self.port, self.pin, self.interconnect
)
def __str__(self):
return self.to_string()
def __repr__(self):
return self.to_string()
# =============================================================================
class Port:
"""
A class representing a block port. Stores port information such as type
(direction), bus width and its connections.
"""
def __init__(self, name, type, width=1, connections=None):
"""
Basic constructor
"""
self.name = name
self.type = type
self.width = width
# Sanity check provided port connections
if connections is not None:
assert isinstance(connections, dict)
# Check each entry
for pin, conn in connections.items():
assert isinstance(pin, int), pin
assert pin < self.width, (pin, width)
assert isinstance(conn, Connection) or \
isinstance(conn, str), pin
self.connections = connections
else:
self.connections = {}
# Rotation map
self.rotation_map = None
@staticmethod
def from_etree(elem, type):
"""
Builds a port class from the packed netlist (XML) representation.
"""
assert elem.tag == "port", elem.tag
name = elem.attrib["name"]
conn = elem.text.strip().split()
width = len(conn)
# Remove open connections
conn = {i: conn[i] for i in range(width) if conn[i] != "open"}
# Build connection objects. Do that only for ports that specify a
# connection to another port. Otherwise treat the name as a net name.
for key in conn.keys():
if "->" in conn[key]:
conn[key] = Connection.from_string(conn[key])
return Port(name, type, width, conn)
def to_etree(self):
"""
Converts the port representation to the packed netlist (XML)
representation.
"""
# Format connections
text = []
for i in range(self.width):
if i in self.connections:
text.append(str(self.connections[i]))
else:
text.append("open")
elem = ET.Element("port", attrib={"name": self.name})
elem.text = " ".join(text)
return elem
def __str__(self):
"""
Returns a user-readable description string
"""
return "{}[{}:0] ({})".format(
self.name, self.width - 1, self.type[0].upper()
)
def __repr__(self):
return str(self)
# =============================================================================
class Block:
"""
A hierarchical block of a packed netlist.
"""
def __init__(self, name, instance, mode=None, parent=None):
# Basic attributes
self.name = name
self.instance = instance
self.mode = mode
# Identify the block type. FIXME: Use a regex here
self.type = instance.split("[", maxsplit=1)[0]
# Ports indexed by names
self.ports = {}
# Parent block
self.parent = parent
# Child blocks indexed by instance
self.blocks = {}
# Leaf block attributes and parameters
self.attributes = {}
self.parameters = {}
@staticmethod
def from_etree(elem):
"""
Builds a block class from the packed netlist (XML) representation.
"""
assert elem.tag == "block", elem.tag
# Create the block with basic attributes
block = Block(
name=elem.attrib["name"],
instance=elem.attrib["instance"],
mode=elem.get("mode", "default")
)
# Parse ports
rotation_maps = {}
for tag in ["inputs", "outputs", "clocks"]:
port_type = tag[:-1]
xml_ports = elem.find(tag)
if xml_ports is not None:
for xml_port in xml_ports:
# Got a port rotation map
if xml_port.tag == "port_rotation_map":
port_name = xml_port.attrib["name"]
rotation = xml_port.text
# Parse the map
rotation_map = {}
for i, j in enumerate(rotation.strip().split()):
if j != "open":
rotation_map[i] = int(j)
# Store it to be later associated with a port
rotation_maps[port_name] = rotation_map
# Got a port
else:
port = Port.from_etree(xml_port, port_type)
block.ports[port.name] = port
# Associate rotation maps with ports
for port_name, rotation_map in rotation_maps.items():
assert port_name in block.ports, port_name
block.ports[port_name].rotation_map = rotation_map
# Recursively parse sub-blocks
for xml_block in elem.findall("block"):
sub_block = Block.from_etree(xml_block)
sub_block.parent = block
block.blocks[sub_block.instance] = sub_block
# Parse attributes and parameters
for tag, data in zip(["attributes", "parameters"],
[block.attributes, block.parameters]):
# Find the list
xml_list = elem.find(tag)
if xml_list is not None:
# Only a leaf block can have attributes / parameters
assert block.is_leaf, "Non-leaf block '{}' with {}".format(
block.instance, tag
)
# Parse
sub_tag = tag[:-1]
for xml_item in xml_list.findall(sub_tag):
data[xml_item.attrib["name"]] = xml_item.text
return block
def to_etree(self):
"""
Converts the block representation to the packed netlist (XML)
representation.
"""
# Base block element
attrib = {
"name": self.name,
"instance": self.instance,
}
if not self.is_leaf:
attrib["mode"] = self.mode if self.mode is not None else "default"
elem = ET.Element("block", attrib)
# If this is an "open" block then skip the remaining tags
if self.name == "open":
return elem
# Attributes / parameters
if self.is_leaf:
for tag, data in zip(["attributes", "parameters"],
[self.attributes, self.parameters]):
xml_list = ET.Element(tag)
sub_tag = tag[:-1]
for key, value in data.items():
xml_item = ET.Element(sub_tag, {"name": key})
xml_item.text = value
xml_list.append(xml_item)
elem.append(xml_list)
# Ports
for tag in ["inputs", "outputs", "clocks"]:
xml_ports = ET.Element(tag)
port_type = tag[:-1]
keys = self.ports.keys()
for key in keys:
port = self.ports[key]
if port.type == port_type:
# Encode port
xml_port = port.to_etree()
xml_ports.append(xml_port)
# Rotation map
if port.rotation_map:
# Encode
rotation = []
for i in range(port.width):
rotation.append(
str(port.rotation_map.get(i, "open"))
)
# Make an element
xml_rotation_map = ET.Element(
"port_rotation_map", {"name": port.name}
)
xml_rotation_map.text = " ".join(rotation)
xml_ports.append(xml_rotation_map)
elem.append(xml_ports)
# Recurse
keys = self.blocks.keys()
for key in keys:
xml_block = self.blocks[key].to_etree()
elem.append(xml_block)
return elem
@property
def is_leaf(self):
"""
Returns True when the block is a leaf block
"""
return len(self.blocks) == 0
@property
def is_open(self):
"""
Returns True when the block is open
"""
return self.name == "open"
@property
def is_route_throu(self):
"""
Returns True when the block is a route-throu native LUT
"""
# VPR stores route-through LUTs as "open" blocks with mode set to
# "wire".
return self.is_leaf and self.name == "open" and self.mode == "wire"
def get_path(self, with_indices=True, with_modes=True, default_modes=True):
"""
Returns the full path to the block. When with_indices is True then
index suffixes '[<index>]' are appended to block types. When
with_modes is True then mode suffixes '[<mode>]' are appended. The
default_modes flag controls appending suffixes for default modes.
"""
path = []
block = self
# Walk towards the tree root
while block is not None:
# Type or type with index (instance)
if with_indices:
node = block.instance
else:
node = block.type
# Mode suffix
if not block.is_leaf and with_modes:
if block.mode != "default" or default_modes:
node += "[{}]".format(block.mode)
# Prepend
path = [node] + path
# Go up
block = block.parent
return ".".join(path)
def rename_cluster(self, name):
"""
Renames this block and all its children except leaf blocks
"""
def walk(block):
if not block.is_leaf and not block.is_open:
block.name = name
for child in block.blocks.values():
walk(child)
walk(self)
def rename_nets(self, net_map):
"""
Renames all nets and leaf blocks that drive them according to the
given map.
"""
def walk(block):
# Rename nets in port connections. Check whether the block itself
# should be renamed as well (output pads need to be).
rename_block = block.name.startswith("out:")
for port in block.ports.values():
for pin, conn in port.connections.items():
if isinstance(conn, str):
port.connections[pin] = net_map.get(conn, conn)
if port.type == "output":
rename_block = True
# Rename the leaf block if necessary
if block.is_leaf and not block.is_open and rename_block:
if block.name in net_map:
block.name = net_map[block.name]
elif block.name.startswith("out:"):
key = block.name[4:]
if key in net_map:
block.name = "out:" + net_map[key]
# Recurse
for child in block.blocks.values():
walk(child)
walk(self)
def get_neighboring_block(self, instance):
"""
Returns a neighboring block given in the vicinity of the current block
given an instance name. The block can be:
- This block,
- A child,
- The parent,
- A sibling (the parent's child).
"""
# Strip index. FIXME: Use a regex here.
block_type = instance.split("[", maxsplit=1)[0]
# Check self
if self.instance == instance:
return self
# Check children
if instance in self.blocks:
return self.blocks[instance]
# Check parent and siblings
if self.parent is not None:
# Parent
if self.parent.type == block_type:
return self.parent
# Siblings
if instance in self.parent.blocks:
return self.parent.blocks[instance]
return None
def find_net_for_port(self, port, pin):
"""
Finds net for the given port name and pin index of the block.
"""
# Get the port
assert port in self.ports, (self.name, self.instance, (port, pin))
port = self.ports[port]
# Unconnected
if pin not in port.connections:
return None
# Get the pin connection
conn = port.connections[pin]
# The connection refers to a net directly
if isinstance(conn, str):
return conn
# Get driving block
block = self.get_neighboring_block(conn.driver)
assert block is not None, (self.instance, conn.driver)
# Recurse
return block.find_net_for_port(conn.port, conn.pin)
def get_nets(self):
"""
Returns all nets that are present in this block and its children
"""
nets = set()
# Recursive walk function
def walk(block):
# Examine block ports
for port in block.ports.values():
for pin in range(port.width):
net = block.find_net_for_port(port.name, pin)
if net:
nets.add(net)
# Get the nets
walk(self)
return nets
def get_block_by_path(self, path):
"""
Returns a child block given its hierarchical path. The path must not
include the current block.
The path may or may not contain modes. When a mode is given it will
be used for matching. The path must contain indices.
"""
def walk(block, parts):
# Check if instance matches
instance = "{}[{}]".format(parts[0].name, parts[0].index)
if block.instance != instance:
return None
# Check if operating mode matches
if parts[0].mode is not None:
if block.mode != parts[0].mode:
return None
# Next
parts = parts[1:]
# No more path parts, this is the block
if not parts:
return block
# Find child block by its instance and recurse
instance = "{}[{}]".format(parts[0].name, parts[0].index)
if instance in block.blocks:
return walk(block.blocks[instance], parts)
return None
# Prepend self to the path
path = "{}[{}]".format(self.instance, self.mode) + "." + path
# Split and parse the path
path = path.split(".")
path = [PathNode.from_string(p) for p in path]
# Find the child
return walk(self, path)
def count_leafs(self):
"""
Counts all non-open leaf blocks
"""
def walk(block, count=0):
# This is a non-ope leaf, count it
if block.is_leaf and not block.is_open:
count += 1
# This is a non-leaf block. Recurse
if not block.is_leaf:
for child in block.blocks.values():
count += walk(child)
return count
# Recursive walk and count
return walk(self)
def __str__(self):
"""
Returns a user-readable description string
"""
ref = self.instance
if self.mode is not None:
ref += "[{}]".format(self.mode)
ref += " ({})".format(self.name)
return ref
def __repr__(self):
return str(self)
# =============================================================================
class PackedNetlist:
"""
A VPR Packed netlist representation.
The packed netlist is organized as one huge block representing the whole
FPGA with all placeable blocks (CLBs) as its children. Here we store the
top-level block implicitly so all blocks mentioned in a PackedNetlist
instance refer to individual placeable CLBs.
"""
def __init__(self):
"""
Basic constructor
"""
# Architecture and atom netlist ids
self.arch_id = None
self.netlist_id = None
# Top-level block name and instance
self.name = None
self.instance = None
# Top-level ports (net names)
self.ports = {
"inputs": [],
"outputs": [],
"clocks": [],
}
# CLBs
self.blocks = {}
@staticmethod
def from_etree(root):
"""
Reads the packed netlist from the given element tree
"""
assert root.tag == "block", root.tag
netlist = PackedNetlist()
netlist.name = root.attrib["name"]
netlist.instance = root.attrib["instance"]
netlist.arch_id = root.get("architecture_id", None)
netlist.netlist_id = root.get("atom_netlist_id", None)
# Parse top-level ports
for tag in ["inputs", "outputs", "clocks"]:
xml_ports = root.find(tag)
if xml_ports is not None and xml_ports.text:
netlist.ports[tag] = xml_ports.text.strip().split()
# Parse CLBs
for xml_block in root.findall("block"):
block = Block.from_etree(xml_block)
netlist.blocks[block.instance] = block
return netlist
def to_etree(self):
"""
Builds an element tree (XML) that represents the packed netlist
"""
# Top-level root block
attr = {
"name": self.name,
"instance": self.instance,
}
if self.arch_id is not None:
attr["architecture_id"] = self.arch_id
if self.netlist_id is not None:
attr["atom_netlist_id"] = self.netlist_id
root = ET.Element("block", attr)
# Top-level ports
for tag in ["inputs", "outputs", "clocks"]:
xml_ports = ET.Element(tag)
xml_ports.text = " ".join(self.ports[tag])
root.append(xml_ports)
# CLB blocks
keys = self.blocks.keys()
for key in keys:
xml_block = self.blocks[key].to_etree()
root.append(xml_block)
return root