def create_tracks(graph, grid_width, grid_height, rcw, verbose=False): print("Creating tracks, channel width: %d" % rcw) # Not strictly required, but VPR wants this I think? assert rcw % 2 == 0 def alt_pos(begin, end, swap): if swap: return end, begin, graph2.NodeDirection.DEC_DIR else: return begin, end, graph2.NodeDirection.INC_DIR # chanx going entire width for y in range(0, grid_height - 1): for tracki in range(rcw): begin, end, direction = alt_pos( (1, y), (grid_width - 2, y), tracki % 2 == 1 ) graph.add_track( track=tracks.Track( direction='X', x_low=begin[0], x_high=end[0], y_low=begin[1], y_high=end[1], ), segment_id=graph.segments[0].id, capacity=1, direction=direction, name="CHANX{:04d}@{:04d}".format(y, tracki), ) # chany going entire height for x in range(0, grid_width - 1): for tracki in range(rcw): begin, end, direction = alt_pos( (x, 1), (x, grid_height - 2), tracki % 2 == 1 ) graph.add_track( track=tracks.Track( direction='Y', x_low=begin[0], x_high=end[0], y_low=begin[1], y_high=end[1], ), segment_id=graph.segments[0].id, capacity=1, direction=direction, name="CHANY{:04d}@{:04d}".format(x, tracki), )
def add_track_chain(graph, direction, u, v0, v1, segment_id, switch_id): """ Adds a chain of tracks that span the grid in the given direction. Returns the first and last node of the chain along with a map of coordinates to nodes. """ node_by_v = {} prev_node = None # Make range generator if v0 > v1: coords = range(v0, v1 - 1, -1) else: coords = range(v0, v1 + 1) # Add track chain for v in coords: # Add track (node) if direction == "X": track = tracks.Track( direction=direction, x_low=v, x_high=v, y_low=u, y_high=u, ) elif direction == "Y": track = tracks.Track( direction=direction, x_low=u, x_high=u, y_low=v, y_high=v, ) else: assert False, direction curr_node = add_track(graph, track, segment_id) # Add edge from the previous one if prev_node is not None: add_edge(graph, prev_node.id, curr_node.id, switch_id) # No previous one, this is the first one else: start_node = curr_node node_by_v[v] = curr_node prev_node = curr_node return start_node, curr_node, node_by_v
def create_track_for_hop_connection(graph, connection): """ Creates a HOP wire track for the given connection """ # Determine whether the wire goes horizontally or vertically. if connection.src.loc.y == connection.dst.loc.y: direction = "X" elif connection.src.loc.x == connection.dst.loc.x: direction = "Y" else: assert False, connection assert connection.src.loc != connection.dst.loc, connection # Determine the connection length length = max(abs(connection.src.loc.x - connection.dst.loc.x), abs(connection.src.loc.y - connection.dst.loc.y)) segment_name = "hop{}".format(length) # Add the track to the graph track = tracks.Track( direction=direction, x_low=min(connection.src.loc.x, connection.dst.loc.x), x_high=max(connection.src.loc.x, connection.dst.loc.x), y_low=min(connection.src.loc.y, connection.dst.loc.y), y_high=max(connection.src.loc.y, connection.dst.loc.y), ) node = add_track(graph, track, graph.get_segment_id_from_name(segment_name)) return node
def import_tracks(conn, alive_tracks, node_mapping, graph, segment_id): c2 = conn.cursor() for (graph_node_pkey, track_pkey, graph_node_type, x_low, x_high, y_low, y_high, ptc) in c2.execute(""" SELECT pkey, track_pkey, graph_node_type, x_low, x_high, y_low, y_high, ptc FROM graph_node WHERE track_pkey IS NOT NULL;"""): if track_pkey not in alive_tracks: continue node_type = graph2.NodeType(graph_node_type) if node_type == graph2.NodeType.CHANX: direction = 'X' x_low = max(x_low, 1) elif node_type == graph2.NodeType.CHANY: direction = 'Y' y_low = max(y_low, 1) else: assert False, node_type track = tracks.Track( direction=direction, x_low=x_low, x_high=x_high, y_low=y_low, y_high=y_high, ) assert graph_node_pkey not in node_mapping node_mapping[graph_node_pkey] = graph.add_track(track=track, segment_id=segment_id, ptc=ptc)
def add_node(graph, loc, direction, segment_id): """ Adds a track of length 1 to the graph. Returns the node object """ return add_track( graph, tracks.Track( direction=direction, x_low=loc.x, x_high=loc.x, y_low=loc.y, y_high=loc.y, ), segment_id)
def get_track_model(conn, track_pkey): assert track_pkey is not None track_list = [] track_nodes = [] c2 = conn.cursor() graph_node_pkey = {} for idx, (pkey, graph_node_type, x_low, x_high, y_low, y_high) in enumerate( c2.execute( """ SELECT pkey, graph_node_type, x_low, x_high, y_low, y_high FROM graph_node WHERE track_pkey = ?""", (track_pkey, ))): node_type = graph2.NodeType(graph_node_type) if node_type == graph2.NodeType.CHANX: direction = 'X' elif node_type == graph2.NodeType.CHANY: direction = 'Y' graph_node_pkey[pkey] = idx track_nodes.append(pkey) track_list.append( tracks.Track(direction=direction, x_low=x_low, x_high=x_high, y_low=y_low, y_high=y_high)) track_connections = set() for src_graph_node_pkey, dest_graph_node_pkey in c2.execute( """ SELECT src_graph_node_pkey, dest_graph_node_pkey FROM graph_edge WHERE track_pkey = ?""", (track_pkey, )): src_idx = graph_node_pkey[src_graph_node_pkey] dest_idx = graph_node_pkey[dest_graph_node_pkey] track_connections.add(tuple(sorted((src_idx, dest_idx)))) tracks_model = tracks.Tracks(track_list, list(track_connections)) return tracks_model, track_nodes
def import_dummy_tracks(conn, graph, segment_id): cur = conn.cursor() num_dummy = 0 for (graph_node_pkey, track_pkey, graph_node_type, x_low, x_high, y_low, y_high, ptc) in cur.execute( """ SELECT pkey, track_pkey, graph_node_type, x_low, x_high, y_low, y_high, ptc FROM graph_node WHERE (graph_node_type = ? or graph_node_type = ?) and capacity = 0;""", (graph2.NodeType.CHANX.value, graph2.NodeType.CHANY.value)): node_type = graph2.NodeType(graph_node_type) if node_type == graph2.NodeType.CHANX: direction = 'X' x_low = x_low elif node_type == graph2.NodeType.CHANY: direction = 'Y' y_low = y_low else: assert False, node_type track = tracks.Track( direction=direction, x_low=x_low, x_high=x_high, y_low=y_low, y_high=y_high, ) graph.add_track(track=track, segment_id=segment_id, capacity=0, ptc=ptc) num_dummy += 1 return num_dummy
def import_tracks(conn, alive_tracks, node_mapping, graph, default_segment_id): cur = conn.cursor() cur2 = conn.cursor() for (graph_node_pkey, track_pkey, graph_node_type, x_low, x_high, y_low, y_high, ptc, capacitance, resistance) in cur.execute(""" SELECT pkey, track_pkey, graph_node_type, x_low, x_high, y_low, y_high, ptc, capacitance, resistance FROM graph_node WHERE track_pkey IS NOT NULL;"""): if track_pkey not in alive_tracks: continue cur2.execute( """ SELECT name FROM segment WHERE pkey = ( SELECT segment_pkey FROM track WHERE pkey = ? )""", (track_pkey, )) result = cur2.fetchone() if result is not None: segment_name = result[0] segment_id = graph.get_segment_id_from_name(segment_name) else: segment_id = default_segment_id node_type = graph2.NodeType(graph_node_type) if node_type == graph2.NodeType.CHANX: direction = 'X' x_low = max(x_low, 1) elif node_type == graph2.NodeType.CHANY: direction = 'Y' y_low = max(y_low, 1) else: assert False, node_type canonical_loc = None cur2.execute( """ SELECT grid_x, grid_y FROM phy_tile WHERE pkey = ( SELECT canon_phy_tile_pkey FROM track WHERE pkey = ? )""", (track_pkey, )) result = cur2.fetchone() if result: canonical_loc = graph2.CanonicalLoc(x=result[0], y=result[1]) track = tracks.Track( direction=direction, x_low=x_low, x_high=x_high, y_low=y_low, y_high=y_high, ) assert graph_node_pkey not in node_mapping node_mapping[graph_node_pkey] = graph.add_track( track=track, segment_id=segment_id, ptc=ptc, timing=graph2.NodeTiming( r=resistance, c=capacitance, ), canonical_loc=canonical_loc)
def main(): parser = argparse.ArgumentParser() parser.add_argument( '--db_root', required=True, help='Project X-Ray Database') parser.add_argument( '--read_rr_graph', required=True, help='Input rr_graph file') parser.add_argument( '--write_rr_graph', required=True, help='Output rr_graph file') parser.add_argument( '--channels', required=True, help='Channel definitions from prjxray_form_channels') parser.add_argument( '--synth_tiles', help='If using an ROI, synthetic tile defintion from prjxray-arch-import') args = parser.parse_args() db = prjxray.db.Database(args.db_root) grid = db.grid() if args.synth_tiles: use_roi = True with open(args.synth_tiles) as f: synth_tiles = json.load(f) roi = Roi( db=db, x1=synth_tiles['info']['GRID_X_MIN'], y1=synth_tiles['info']['GRID_Y_MIN'], x2=synth_tiles['info']['GRID_X_MAX'], y2=synth_tiles['info']['GRID_Y_MAX'], ) print('{} generating routing graph for ROI.'.format(now())) else: use_roi = False # Convert input rr graph into graph2.Graph object. input_rr_graph = read_xml_file(args.read_rr_graph) xml_graph = xml_graph2.Graph( input_rr_graph, progressbar=progressbar.progressbar) graph = xml_graph.graph tool_version = input_rr_graph.getroot().attrib['tool_version'] tool_comment = input_rr_graph.getroot().attrib['tool_comment'] delayless_switch = graph.get_delayless_switch_id() print('{} reading channels definitions.'.format(now())) with open(args.channels) as f: channels = json.load(f) segment_id = graph.get_segment_id_from_name('dummy') track_wire_map = {} print('{} add nodes for all channels.'.format(now())) used_channels = 0 for idx, channel in progressbar.progressbar(enumerate(channels['channels'])): # Don't use dead channels if using an ROI. # Consider a channel alive if at least 1 wire in the node is part of a # live tile. if use_roi: alive = False for tile, wire in channel['wires']: loc = grid.loc_of_tilename(tile) if roi.tile_in_roi(loc) or tile in synth_tiles['tiles']: alive = True break if not alive: continue used_channels += 1 nodes = [] track_list = [] for idx2, track_dict in enumerate(channel['tracks']): if track_dict['direction'] == 'X': track_dict['x_low'] = max(track_dict['x_low'], 1) elif track_dict['direction'] == 'Y': track_dict['y_low'] = max(track_dict['y_low'], 1) track = tracks.Track(**track_dict) track_list.append(track) nodes.append(graph.add_track(track=track, segment_id=segment_id, name='track_{}_{}'.format(idx, idx2))) for a_idx, b_idx in channel['track_connections']: graph.add_edge(nodes[a_idx], nodes[b_idx], delayless_switch, 'track_{}_to_{}'.format(a_idx, b_idx)) graph.add_edge(nodes[b_idx], nodes[a_idx], delayless_switch, 'track_{}_to_{}'.format(b_idx, a_idx)) tracks_model = tracks.Tracks(track_list, channel['track_connections']) for tile, wire in channel['wires']: track_wire_map[(tile, wire)] = (tracks_model, nodes) print('original {} final {}'.format(len(channels['channels']), used_channels)) routing_switch = graph.get_switch_id('routing') pip_map = {} edges_with_mux = {} for idx, edge_with_mux in progressbar.progressbar(enumerate(channels['edges_with_mux'])): if edge_with_mux['pip'] not in edges_with_mux: edges_with_mux[edge_with_mux['pip']] = {} assert len(edge_with_mux['source_node']) == 1 edges_with_mux[edge_with_mux['pip']][tuple(edge_with_mux['source_node'][0])] = edge_with_mux['destination_node'] # Set of (src, sink, switch_id) tuples that pip edges have been sent to # VPR. VPR cannot handle duplicate paths with the same switch id. pip_set = set() print('{} Adding edges'.format(now())) for loc in progressbar.progressbar(grid.tile_locations()): gridinfo = grid.gridinfo_at_loc(loc) tile_name = grid.tilename_at_loc(loc) if use_roi: if tile_name in synth_tiles['tiles']: assert len(synth_tiles['tiles'][tile_name]['pins']) == 1 for pin in synth_tiles['tiles'][tile_name]['pins']: tracks_model, track_nodes = track_wire_map[(tile_name, pin['wire'])] option = list(tracks_model.get_tracks_for_wire_at_coord(loc)) assert len(option) > 0 if pin['port_type'] == 'input': tile_type = 'BLK_SY-OUTPAD' wire = 'outpad' elif pin['port_type'] == 'output': tile_type = 'BLK_SY-INPAD' wire = 'inpad' else: assert False, pin track_node = track_nodes[option[0][0]] pin_name = graph.create_pin_name_from_tile_type_and_pin( tile_type, wire) pin_node = graph.get_nodes_for_pin(loc, pin_name) if pin['port_type'] == 'input': graph.add_edge( src_node=track_node, sink_node=pin_node[0][0], switch_id=routing_switch, name='synth_{}_{}'.format(tile_name, pin['wire']), ) elif pin['port_type'] == 'output': graph.add_edge( src_node=pin_node[0][0], sink_node=track_node, switch_id=routing_switch, name='synth_{}_{}'.format(tile_name, pin['wire']), ) else: assert False, pin else: # Not a synth node, check if in ROI. if not roi.tile_in_roi(loc): continue tile_type = db.get_tile_type(gridinfo.tile_type) for pip in tile_type.get_pips(): if pip.is_pseudo: continue if not pip.is_directional: # TODO: Handle bidirectional pips? continue edge_node = make_connection(graph, track_wire_map, loc, tile_name, gridinfo.tile_type, pip, routing_switch, edges_with_mux, grid, pip_set) if edge_node is not None: pip_map[(tile_name, pip.name)] = edge_node print('{} Writing node mapping.'.format(now())) node_mapping = { 'pips': [], 'tracks': [] } for (tile, pip_name), edge in pip_map.items(): node_mapping['pips'].append({ 'tile': tile, 'pip': pip_name, 'edge': edge }) for (tile, wire), (_, nodes) in track_wire_map.items(): node_mapping['tracks'].append({ 'tile': tile, 'wire': wire, 'nodes': nodes, }) with open('node_mapping.pickle', 'wb') as f: pickle.dump(node_mapping, f) print('{} Create channels and serializing.'.format(now())) pool = multiprocessing.Pool(10) serialized_rr_graph = xml_graph.serialize_to_xml( tool_version=tool_version, tool_comment=tool_comment, pad_segment=segment_id, pool=pool, ) print('{} Writing to disk.'.format(now())) with open(args.write_rr_graph, "wb") as f: f.write(serialized_rr_graph) print('{} Done.'.format(now()))
def add_l_track(graph, x0, y0, x1, y1, segment_id, switch_id): """ Add a "L"-shaped track consisting of two channel nodes and a switch between the given two grid coordinates. The (x0, y0) determines source location and (x1, y1) destination (sink) location. Returns a tuple with indices of the first and last node. """ dx = x1 - x0 dy = y1 - y0 assert dx != 0 or dy != 0, (x0, y0) nodes = [None, None] # Go vertically first if abs(dy) >= abs(dx): xc, yc = x0, y1 if abs(dy): track = tracks.Track( direction="Y", x_low=min(x0, xc), x_high=max(x0, xc), y_low=min(y0, yc), y_high=max(y0, yc), ) nodes[0] = add_track(graph, track, segment_id) if abs(dx): track = tracks.Track( direction="X", x_low=min(xc, x1), x_high=max(xc, x1), y_low=min(yc, y1), y_high=max(yc, y1), ) nodes[1] = add_track(graph, track, segment_id) # Go horizontally first else: xc, yc = x1, y0 if abs(dx): track = tracks.Track( direction="X", x_low=min(x0, xc), x_high=max(x0, xc), y_low=min(y0, yc), y_high=max(y0, yc), ) nodes[0] = add_track(graph, track, segment_id) if abs(dy): track = tracks.Track( direction="Y", x_low=min(xc, x1), x_high=max(xc, x1), y_low=min(yc, y1), y_high=max(yc, y1), ) nodes[1] = add_track(graph, track, segment_id) # In case of a horizontal or vertical only track make both nodes the same assert nodes[0] is not None or nodes[1] is not None if nodes[0] is None: nodes[0] = nodes[1] if nodes[1] is None: nodes[1] = nodes[0] # Add edge connecting the two nodes if needed if nodes[0].id != nodes[1].id: add_edge(graph, nodes[0].id, nodes[1].id, switch_id) return nodes
def main(): parser = argparse.ArgumentParser() parser.add_argument('--db_root', help='Project X-Ray Database', required=True) parser.add_argument('--channels', help='Input JSON defining channel assignments', required=True) parser.add_argument( '--pin_assignments', help= 'Output JSON assigning pins to tile types and direction connections', required=True) args = parser.parse_args() db = prjxray.db.Database(args.db_root) grid = db.grid() edge_assignments = {} wires_in_tile_types = set() for tile_type in db.get_tile_types(): type_obj = db.get_tile_type(tile_type) for wire in type_obj.get_wires(): wires_in_tile_types.add((tile_type, wire)) for site in type_obj.get_sites(): for site_pin in site.site_pins: if site_pin.wire is None: continue key = (tile_type, site_pin.wire) assert key not in edge_assignments, key edge_assignments[key] = [] print('{} Reading channel data'.format(datetime.datetime.now())) with open(args.channels) as f: channels = json.load(f) print('{} Done reading channel data'.format(datetime.datetime.now())) direct_connections = set() # Edges with mux should have one source tile and one destination_tile. # The pin from the source_tile should face the destination_tile. # # It is expected that all edges_with_mux will lies in a line (e.g. X only or # Y only). for edge_with_mux in progressbar.progressbar(channels['edges_with_mux']): source_tile = None source_tile_type = None source_wire = None destination_tile = None destination_tile_type = None destination_wire = None for tile, wire in edge_with_mux['source_node']: tileinfo = grid.gridinfo_at_tilename(tile) tile_type = db.get_tile_type(tileinfo.tile_type) wire_info = tile_type.get_wire_info(wire) if len(wire_info.sites) == 1: assert source_tile is None, (tile, wire, source_tile) source_tile = tile source_tile_type = tileinfo.tile_type source_wire = wire for tile, wire in edge_with_mux['destination_node']: tileinfo = grid.gridinfo_at_tilename(tile) tile_type = db.get_tile_type(tileinfo.tile_type) wire_info = tile_type.get_wire_info(wire) if len(wire_info.sites) == 1: assert destination_tile is None, (tile, wire, destination_tile, wire_info) destination_tile = tile destination_tile_type = tileinfo.tile_type destination_wire = wire assert source_tile is not None assert destination_tile is not None source_loc = grid.loc_of_tilename(source_tile) destination_loc = grid.loc_of_tilename(destination_tile) assert source_loc.grid_x == destination_loc.grid_x or source_loc.grid_y == destination_loc.grid_y, ( source_tile, destination_tile, edge_with_mux['pip']) direct_connections.add( DirectConnection( from_pin='{}.{}'.format(source_tile_type, source_wire), to_pin='{}.{}'.format(destination_tile_type, destination_wire), switch_name='routing', x_offset=destination_loc.grid_x - source_loc.grid_x, y_offset=destination_loc.grid_y - source_loc.grid_y, )) if destination_loc.grid_x == source_loc.grid_x: if destination_loc.grid_y > source_loc.grid_y: source_dir = tracks.Direction.TOP destination_dir = tracks.Direction.BOTTOM else: source_dir = tracks.Direction.BOTTOM destination_dir = tracks.Direction.TOP else: if destination_loc.grid_x > source_loc.grid_x: source_dir = tracks.Direction.RIGHT destination_dir = tracks.Direction.LEFT else: source_dir = tracks.Direction.LEFT destination_dir = tracks.Direction.RIGHT edge_assignments[(source_tile_type, source_wire)].append( (source_dir, )) edge_assignments[(destination_tile_type, destination_wire)].append( (destination_dir, )) wires_not_in_channels = {} for node in progressbar.progressbar(channels['node_not_in_channels']): reason = node['classification'] for tile, wire in node['wires']: tileinfo = grid.gridinfo_at_tilename(tile) key = (tileinfo.tile_type, wire) # Sometimes nodes in particular tile instances are disconnected, # disregard classification changes if this is the case. if reason != 'NULL': if key not in wires_not_in_channels: wires_not_in_channels[key] = reason else: other_reason = wires_not_in_channels[key] assert reason == other_reason, (tile, wire, reason, other_reason) if key in wires_in_tile_types: wires_in_tile_types.remove(key) # List of nodes that are channels. channel_nodes = [] # Map of (tile, wire) to track. This will be used to find channels for pips # that come from EDGES_TO_CHANNEL. channel_wires_to_tracks = {} # Generate track models and verify that wires are either in a channel # or not in a channel. for channel in progressbar.progressbar(channels['channels']): track_list = [] for track in channel['tracks']: track_list.append(tracks.Track(**track)) tracks_model = tracks.Tracks(track_list, channel['track_connections']) channel_nodes.append(tracks_model) for tile, wire in channel['wires']: tileinfo = grid.gridinfo_at_tilename(tile) key = (tileinfo.tile_type, wire) # Make sure all wires in channels always are in channels assert key not in wires_not_in_channels if key in wires_in_tile_types: wires_in_tile_types.remove(key) channel_wires_to_tracks[(tile, wire)] = tracks_model # Make sure all wires appear to have been assigned. assert len(wires_in_tile_types) == 0 # Verify that all tracks are sane. for node in channel_nodes: node.verify_tracks() null_tile_wires = set() # Verify that all nodes that are classified as edges to channels have at # least one site, and at least one live connection to a channel. # # If no live connections from the node are present, this node should've # been marked as NULL during channel formation. for node in progressbar.progressbar(channels['node_not_in_channels']): reason = node['classification'] assert reason != 'EDGE_WITH_SHORT' if reason == 'NULL': for tile, wire in node['wires']: tileinfo = grid.gridinfo_at_tilename(tile) tile_type = db.get_tile_type(tileinfo.tile_type) null_tile_wires.add((tileinfo.tile_type, wire)) if reason == 'EDGES_TO_CHANNEL': num_sites = 0 for tile, wire in node['wires']: tileinfo = grid.gridinfo_at_tilename(tile) loc = grid.loc_of_tilename(tile) tile_type = db.get_tile_type(tileinfo.tile_type) wire_info = tile_type.get_wire_info(wire) num_sites += len(wire_info.sites) for pip in wire_info.pips: other_wire = prjxray.tile.get_other_wire_from_pip( tile_type.get_pip_by_name(pip), wire) key = (tile, other_wire) if key in channel_wires_to_tracks: tracks_model = channel_wires_to_tracks[key] if len(wire_info.sites) > 0: available_pins = set( pin_dir for _, pin_dir in tracks_model. get_tracks_for_wire_at_coord((loc.grid_x, loc.grid_y))) edge_assignments[(tileinfo.tile_type, wire)].append(available_pins) final_edge_assignments = {} for (tile_type, wire), available_pins in progressbar.progressbar( edge_assignments.items()): if len(available_pins) == 0: if (tile_type, wire) not in null_tile_wires: # TODO: Figure out what is going on with these wires. Appear to # tile internal connections sometimes? print((tile_type, wire)) final_edge_assignments[(tile_type, wire)] = [tracks.Direction.RIGHT] continue pins = set(available_pins[0]) for p in available_pins[1:]: pins &= set(p) if len(pins) > 0: final_edge_assignments[(tile_type, wire)] = [list(pins)[0]] else: # More than 2 pins are required, final the minimal number of pins pins = set() for p in available_pins: pins |= set(p) while len(pins) > 2: pins = list(pins) prev_len = len(pins) for idx in range(len(pins)): pins_subset = list(pins) del pins_subset[idx] pins_subset = set(pins_subset) bad_subset = False for p in available_pins: if len(pins_subset & set(p)) == 0: bad_subset = True break if not bad_subset: pins = list(pins_subset) break # Failed to remove any pins, stop. if len(pins) == prev_len: break final_edge_assignments[(tile_type, wire)] = pins for (tile_type, wire), available_pins in edge_assignments.items(): pins = set(final_edge_assignments[(tile_type, wire)]) for required_pins in available_pins: assert len(pins & set(required_pins)) > 0, (tile_type, wire, pins, required_pins) pin_directions = {} for (tile_type, wire), pins in final_edge_assignments.items(): if tile_type not in pin_directions: pin_directions[tile_type] = {} pin_directions[tile_type][wire] = [pin._name_ for pin in pins] with open(args.pin_assignments, 'w') as f: json.dump( { 'pin_directions': pin_directions, 'direct_connections': [d._asdict() for d in direct_connections], }, f, indent=2)