blob: d6fc001591f1b6569d0bdfd05c98b24b79717419 [file] [log] [blame] [edit]
""" Grid splitting model with connection database back references.
The Grid object provides methods to manipulate a 2D grid of Tile objects, that
contain zero or more Site objects. Site objects are considered immutable.
To construct the Grid object, the initial grid and an empty_tile_type_pkey must
be provided. The initial grid should be provided as a map of 2 element int
tuples to Tile objects. Tile objects should already contain their initial
sites prior to construction of the Grid object.
"""
from enum import Enum
from collections import namedtuple
class Direction(Enum):
""" Grid directions. """
NORTH = 1
SOUTH = 2
EAST = 3
WEST = 4
NORTH = Direction.NORTH
SOUTH = Direction.SOUTH
EAST = Direction.EAST
WEST = Direction.WEST
OPPOSITE_DIRECTIONS = {
NORTH: SOUTH,
SOUTH: NORTH,
EAST: WEST,
WEST: EAST,
}
# Zipper direction when splitting in a direction
SPLIT_NEXT_DIRECTIONS = {
NORTH: EAST,
SOUTH: EAST,
EAST: SOUTH,
WEST: SOUTH,
}
def opposite_direction(direction):
""" Return opposite direction of given direction.
>>> opposite_direction(NORTH)
<Direction.SOUTH: 2>
>>> opposite_direction(SOUTH)
<Direction.NORTH: 1>
>>> opposite_direction(EAST)
<Direction.WEST: 4>
>>> opposite_direction(WEST)
<Direction.EAST: 3>
"""
return OPPOSITE_DIRECTIONS[direction]
# Right handed coordinate system, N/S in y, E/W in x, E is x-positive,
# S is y-positive.
DIRECTION_OFFSET = {
NORTH: [0, -1],
SOUTH: [0, 1],
EAST: [1, 0],
WEST: [-1, 0],
}
def coordinate_in_direction(coord, direction):
""" Given a coordinate, returns a new coordinate 1 step in direction.
Coordinate system is right handed, N/S in y, E/W in x. E is x-positive.
S is y-positive.
Parameters
----------
coord : Tuple of 2 ints
Starting coordinate
direction : Direction
Direction to add unit vector.
Returns
-------
Tuple of 2 ints
Coordinate 1 unit step in specified direction. Will return none if
x or y coordinate is negative.
Examples
--------
>>> coordinate_in_direction((0, 0), SOUTH)
(0, 1)
>>> coordinate_in_direction((0, 0), EAST)
(1, 0)
>>> coordinate_in_direction((1, 1), SOUTH)
(1, 2)
>>> coordinate_in_direction((1, 1), NORTH)
(1, 0)
>>> coordinate_in_direction((1, 1), EAST)
(2, 1)
>>> coordinate_in_direction((1, 1), WEST)
(0, 1)
# Returns None for negative coordinates.
>>> coordinate_in_direction((0, 0), NORTH)
>>> coordinate_in_direction((1, 0), NORTH)
>>> coordinate_in_direction((0, 0), WEST)
>>> coordinate_in_direction((0, 1), WEST)
"""
x, y = coord
dx, dy = DIRECTION_OFFSET[direction]
x += dx
y += dy
if x < 0 or y < 0:
return None
else:
return x, y
class Site(namedtuple('Site', ('name', 'phy_tile_pkey', 'tile_type_pkey',
'site_type_pkey', 'site_pkey', 'x', 'y'))):
""" Object to hold back reference information for a site. """
pass
class Tile(object):
""" Tile instance within the grid.
Attributes
----------
root_phy_tile_pkeys : list of ints
The list of root_phy_tile_pkey's.
By default a tile typically has one root phy_tile_pkey, which is the
phy_tile this initial represents.
If two Tile objects are merged, the root_phy_tile_pkeys are also merged.
If a Tile object is split, only one of the split tiles will take all
of the root_phy_tile_pkeys, and the other tiles will have no
root_phy_tile_pkeys.
Invariant: Each phy_tile_pkey will appear in 1 and only 1 Tile
object's root_phy_tile_pkeys list.
Because of the invariant, the root_phy_tile_pkeys list can be used as
a default assignment of children of the relevant phy_tile_pkey items
(e.g. wires, pips, sites, etc).
phy_tile_pkeys : list of ints
The list of phy_tile_pkey's. This is the list of all phy_tile_pkey's
that are involved in this tile via either a tile merge or split.
By default a tile typically has one phy_tile_pkey, which is the
phy_tile this initial represents.
If two Tile objects are split, all output tiles will get a copy of the
original phy_tile_pkeys list. This attribute can be used to determine
what phy_tile_pkeys were used to make this tile.
sites : list of Site objects
This is the list of Site's contained within this tile. This should
be initial set to the Site objects contained within the original
phy_tile.
Invariant: Each Site object will be contained within exactly one Tile
object.
split_sites : boolean
True if this tile was split.
Invariant: Each split tile will contain exactly one Site object.
Invariant: Two tiles that were split cannot be merged, otherwise the
resulting Tile will have two Sites, potentially from different
phy_tile_pkey, which cannot be presented using FASm prefixes.
neighboors : Map of Direction to Tile object
Linked list pointers to neighboors tiles.
Invariant: Underlying linked link should be rectangular after an
operation on the grid. An single operation on the Tile will typically
invalidate the overall grid, it is up to the Grid object to enforce
the rectangular constraint.
Invariant: Underlying linked link must not be circular.
"""
def __init__(
self, root_phy_tile_pkeys, phy_tile_pkeys, tile_type_pkey, sites
):
self.root_phy_tile_pkeys = root_phy_tile_pkeys
self.phy_tile_pkeys = phy_tile_pkeys
self.tile_type_pkey = tile_type_pkey
self.sites = sites
self.split_sites = False
self.neighboors = {}
def link_neighboor_in_direction(self, other_tile, direction_to_other_tile):
""" Connect this tile to another tile in a specific direction.
It is legal to call this method on an existing connection, but it is
not legal to call this method to replace an existing connection.
Parameters
----------
other_tile : Tile object
Other Tile object to connect in specified direction.
direction_to_other_tile : Direction
Direction to connect other tile.
"""
if direction_to_other_tile in self.neighboors:
assert id(
self.neighboors[direction_to_other_tile]
) == id(other_tile), (self.neighboors, direction_to_other_tile)
self.neighboors[direction_to_other_tile] = other_tile
direction_to_this_tile = opposite_direction(direction_to_other_tile)
if direction_to_this_tile in other_tile.neighboors:
assert id(other_tile.neighboors[direction_to_this_tile]
) == id(self)
other_tile.neighboors[direction_to_this_tile] = self
def insert_in_direction(self, other_tile, direction_to_other_tile):
""" Insert a tile in a specified direction.
Parameters
----------
other_tile : Tile object
Other Tile object to insert in specified direction.
direction_to_other_tile : Direction
Direction to insert other tile.
"""
old_neighboor = self.neighboors.get(direction_to_other_tile, None)
direction_to_this_tile = opposite_direction(direction_to_other_tile)
self.neighboors[direction_to_other_tile] = other_tile
other_tile.neighboors[direction_to_this_tile] = self
if old_neighboor is not None:
other_tile.neighboors[direction_to_other_tile] = old_neighboor
old_neighboor.neighboors[direction_to_this_tile] = other_tile
def walk_in_direction(self, direction):
""" Walk in specified direction from this Tile node.
Parameters
----------
direction : Direction
Direction to walk in.
Yields
------
tile : Tile
Tile in specified direction. First Tile object will always be the
tile whose walk_in_direction was invoked. When the end of the grid
is encounted, no more tiles will be yielded.
"""
node = self
while True:
yield node
if direction in node.neighboors:
node = node.neighboors[direction]
else:
break
def check_grid_loc(grid_loc_map):
""" Verifies input grid makes sense.
Internal grid consistency is defined as:
- Has an origin location @ (0, 0)
- Is rectangular
- Has no gaps.
Parameters
----------
grid_loc_map : Dict of 2 int tuple to Tile objects
Grid being checked.
Raises
------
AssertionError
If provided grid does not conform to assumptions about grid.
"""
xs, ys = zip(*grid_loc_map.keys())
max_x = max(xs)
max_y = max(ys)
for x in range(max_x + 1):
for y in range(max_y + 1):
assert (x, y) in grid_loc_map, (x, y)
def build_mesh(current, visited, loc, grid_loc_map):
""" Stitch grid_loc_map into a double-linked list 2D mesh.
Modifies Tile object neighboors attributes to form a doubly linked list
2D mesh.
It is strongly recommended that grid_loc_map be passed to check_grid_loc
prior to calling build_mesh to verify grid invariants.
Parameters
----------
current : Tile object
visited : set of python object id's
Should be empty on root invocation.
loc : Location of current Tile object argument
grid_loc_map : Dict of 2 int tuple to Tile objects
Grid being converted to linked list form.
"""
for direction in (SOUTH, EAST):
new_loc = coordinate_in_direction(loc, direction)
if new_loc in grid_loc_map:
current.link_neighboor_in_direction(
grid_loc_map[new_loc], direction
)
if id(grid_loc_map[new_loc]) not in visited:
visited.add(id(grid_loc_map[new_loc]))
build_mesh(
grid_loc_map[new_loc], visited, new_loc, grid_loc_map
)
class Grid(object):
""" Object for manipulating a 2D grid of Tile objects.
Parameters
----------
grid_loc_map : Dict of 2 int tuple to Tile objects
Initial grid of Tile objects.
empty_tile_type_pkey : int
tile_type_pkey to use when creating new empty tiles during tile splits.
"""
def __init__(self, grid_loc_map, empty_tile_type_pkey):
# Make sure initial grid is sane
check_grid_loc(grid_loc_map)
# Keep root object of grid.
self.origin = grid_loc_map[(0, 0)]
# Convert grid to doubly-linked list.
build_mesh(self.origin, set(), (0, 0), grid_loc_map)
# Keep list of all Tile objects for convience.
self.items = grid_loc_map.values()
self.empty_tile_type_pkey = empty_tile_type_pkey
def column(self, x):
""" Return Tile object at top of column.
Parameters
----------
x : int
0 based column to retrive.
Returns
-------
top_of_column : Tile
Tile object at top of column
"""
top_of_column = self.origin
for _ in range(x):
top_of_column = top_of_column.neighboors[EAST]
return top_of_column
def row(self, y):
""" Return Tile object at right of row.
Parameters
----------
y : int
0 based row to retrive.
Returns
-------
right_of_row : Tile
Tile object at right of row
"""
right_of_row = self.origin
for _ in range(y):
right_of_row = right_of_row.neighboors[SOUTH]
def split_tile(self, tile, tile_type_pkeys, split_direction, split_map):
""" Split tile in specified direction.
This method requires that the tiles required to perform the split (e.g.
len(tile_type_pkeys)-1 tiles in split_direction from tile) have
tile_type_pkey == empty_tile_type_pkey, e.g. they are empty tiles.
If empty tiles must be inserted into the grid to accomidate the split,
this must be done prior to calling this method.
Parameters
----------
tile : Tile object
Tile being split
tile_type_pkeys : List of int
List of new tile_type_pkeys to be used after the tile split.
The tile being split will become tile_type_pkeys[0], the next tile
in split_direction will become tile_type_pkeys[1], etc.
split_direction : Direction
Which direction from tile should the split occur.
split_map : Dict of (int, int) to int
Mapping of site location (x, y) to tile_type_pkey indicies.
This enables control over which sites go to which tiles based on
their coordinate.
min(split_map.values()) >= 0
max(split_map.values()) < len(tile_type_pkeys)
"""
sites = tile.sites
tile.tile_type_pkey = self.empty_tile_type_pkey
phy_tile_pkeys = set(tile.phy_tile_pkeys)
new_tiles = []
for idx, tile in enumerate(tile.walk_in_direction(split_direction)):
assert tile.tile_type_pkey == self.empty_tile_type_pkey, (
tile.tile_type_pkey
)
tile.phy_tile_pkeys = []
new_tiles.append(tile)
if idx + 1 >= len(tile_type_pkeys):
break
for tile, new_tile_type_pkey in zip(new_tiles, tile_type_pkeys):
assert tile.tile_type_pkey == self.empty_tile_type_pkey
tile.tile_type_pkey = new_tile_type_pkey
tile.phy_tile_pkeys = list(
set(tile.phy_tile_pkeys) | phy_tile_pkeys
)
tile.sites = []
tile.split_sites = True
for site in sites:
site_idx = split_map[site.x, site.y]
assert site_idx < len(tile_type_pkeys), (
site, site_idx, tile_type_pkeys
)
new_tiles[site_idx].sites.append(site)
def insert_empty(self, top, insert_in_direction):
""" Insert empty row/colum.
Insert a row/column of empty tiles from the tiles in the row/column specified
by top_of_row/column tile. The new empty tiles will have tile_type_pkey
set to empty_tile_type_pkey, and have phy_tile_pkeys of the tile they
were inserted from.
Parameters
----------
top_of_row/column : Tile object
Tile at top of row/column adjcent to where new row/column should be
inserted.
insert_in_direction : Direction
Direction to insert empty tiles, from perspective of the row/column
specified by top_of_row/column.
"""
# Verify that insert direction is not the same as zipper direction.
next_dir = SPLIT_NEXT_DIRECTIONS[insert_in_direction]
# Verify that top is in fact the top of the zipper
assert OPPOSITE_DIRECTIONS[next_dir] not in top.neighboors
empty_tiles = []
for tile in top.walk_in_direction(next_dir):
empty_tile = Tile(
root_phy_tile_pkeys=[],
phy_tile_pkeys=list(tile.phy_tile_pkeys),
tile_type_pkey=self.empty_tile_type_pkey,
sites=[]
)
empty_tiles.append(empty_tile)
tile.insert_in_direction(empty_tile, insert_in_direction)
for a, b in zip(empty_tiles, empty_tiles[1:]):
a.link_neighboor_in_direction(b, next_dir)
self.check_grid()
def split_in_dir(
self,
top,
tile_type_pkey,
tile_type_pkeys,
split_direction,
split_map,
):
""" Split row/column of tiles.
Splits specified tile types into new row/column by first inserting any
required empty row/column in the split direction, and then performing
the split.
Parameters
----------
top_of_row/column : Tile object
Tile at top of row/column where split should be performed.
tile_type_pkey : Tile type to split.
tile_type_pkeys : Refer to split_tile documentation.
split_direction : Direction
Direction to insert perform split. New row/column will be inserted
in that direction to accomidate the tile split.
split_map : Dict of (int, int) to int
Mapping of site location (x, y) to tile_type_pkey indicies.
This enables control over which sites go to which tiles based on
their coordinate.
"""
next_dir = SPLIT_NEXT_DIRECTIONS[split_direction]
# Find how many empty tiles are required to support the split
num_to_insert = 0
for tile in top.walk_in_direction(next_dir):
if tile.tile_type_pkey != tile_type_pkey:
continue
for idx, tile_in_split in enumerate(
tile.walk_in_direction(split_direction)):
if idx == 0:
continue
else:
if tile_in_split.tile_type_pkey != self.empty_tile_type_pkey:
num_to_insert = max(num_to_insert, idx)
if idx + 1 >= len(tile_type_pkeys):
break
for _ in range(num_to_insert):
self.insert_empty(top, split_direction)
for tile in top.walk_in_direction(next_dir):
if tile.tile_type_pkey != tile_type_pkey:
continue
self.split_tile(tile, tile_type_pkeys, split_direction, split_map)
def split_tile_type(
self, tile_type_pkey, tile_type_pkeys, split_direction, split_map
):
""" Split a specified tile type within grid.
Splits specified tile types by finding each column that contains the
relevant tile type, and spliting each column.
Parameters
----------
tile_type_pkey : Tile type to split.
tile_type_pkeys : Refer to split_tile documentation.
split_direction : Direction
Direction to insert perform split.
split_map : Dict of (int, int) to int
Mapping of site location (x, y) to tile_type_pkey indicies.
This enables control over which sites go to which tiles based on
their coordinate.
"""
tiles_seen = set()
tiles = []
tops_to_split = []
next_dir = SPLIT_NEXT_DIRECTIONS[split_direction]
for tile in self.items:
if id(tile) in tiles_seen:
continue
if tile.tile_type_pkey == tile_type_pkey:
# Found a row/column that needs to be split, walk to the bottom of
# the row/column, then back to the top.
for tile in tile.walk_in_direction(next_dir):
pass
for tile in tile.walk_in_direction(
OPPOSITE_DIRECTIONS[next_dir]):
if id(tile) not in tiles_seen:
tiles_seen.add(id(tile))
if tile.tile_type_pkey == tile_type_pkey:
tiles.append(tile)
tops_to_split.append(tile)
for top in tops_to_split:
self.split_in_dir(
top, tile_type_pkey, tile_type_pkeys, split_direction,
split_map
)
def merge_in_dir(self, tile, merge_direction):
""" Merge tile in specified direction.
Merging a tile causes the connects of that tile to be merged into
the tile in the merge direction. The tile that was merged will become
empty.
The original tile root_phy_tile_pkeys and phy_tile_pkeys will appear
first.
Parameters
----------
tile : Tile
Tile to merge
merge_direction : Direction
Direction to merge tiles.
"""
assert merge_direction in tile.neighboors, (tile, merge_direction)
merge_into = tile.neighboors[merge_direction]
merge_into.root_phy_tile_pkeys.extend(tile.root_phy_tile_pkeys)
merge_into.phy_tile_pkeys.extend(tile.phy_tile_pkeys)
merge_into.sites.extend(tile.sites)
tile.sites = list()
tile.tile_type_pkey = self.empty_tile_type_pkey
tile.root_phy_tile_pkeys = list()
tile.phy_tile_pkeys = list()
def merge_tile_type(self, tile_type_pkey, merge_direction):
""" Merge tile types in specified direction.
Parameters
----------
tile_type_pkey : Tile type to split.
merge_direction : Direction
Direction to merge tiles.
"""
for tile in self.items:
if tile.tile_type_pkey == tile_type_pkey:
self.merge_in_dir(tile, merge_direction)
def output_grid(self):
""" Convert grid back to coordinate lookup form.
Returns
-------
grid_loc_map : Dict of 2 int tuple to Tile objects
Output grid of Tile objects.
"""
grid_loc_map = {}
for x, tile in enumerate(self.origin.walk_in_direction(EAST)):
for y, tile in enumerate(tile.walk_in_direction(SOUTH)):
grid_loc_map[(x, y)] = tile
check_grid_loc(grid_loc_map)
return grid_loc_map
def check_grid(self):
""" Verifies that grid linked list model still represents valid grid.
"""
self.output_grid()