| from collections import namedtuple |
| import pprint |
| from enum import Enum |
| |
| |
| class Direction(Enum): |
| NO_SIDE = 0 |
| LEFT = 1 |
| RIGHT = 2 |
| TOP = 3 |
| BOTTOM = 4 |
| RIGHT_LEFT = 5 |
| RIGHT_BOTTOM = 6 |
| RIGHT_BOTTOM_LEFT = 7 |
| TOP_RIGHT = 8 |
| TOP_BOTTOM = 9 |
| TOP_LEFT = 10 |
| TOP_RIGHT_BOTTOM = 11 |
| TOP_RIGHT_LEFT = 12 |
| TOP_BOTTOM_LEFT = 13 |
| TOP_RIGHT_BOTTOM_LEFT = 14 |
| BOTTOM_LEFT = 15 |
| |
| |
| # Track (aka Channel) is a wire that runs from one position to another along X or Y axis |
| Track = namedtuple('Track', 'direction x_low x_high y_low y_high') |
| |
| |
| def print_tracks(tracks): |
| pprint.pprint(tracks) |
| |
| |
| def make_tracks(xs, ys, points, grid_width=None, grid_height=None): |
| """ Give a list of xs columns and ys rows and points, return a list of |
| Track's and connections between the tracks. |
| |
| An assert will fail if each point in the point list is not covered by |
| a column in xs or a row in ys. |
| |
| Connections will be models as indicies into the track list. |
| |
| Return: |
| [Track], [(index into track list, index into track list)] |
| |
| >>> pos = [ |
| ... (1,1), (3,1), |
| ... (1,2), (2,2), (3,2), |
| ... (1,3), (3,3), |
| ... (1,4), (2,4), (3,4), |
| ... (1,5), (3,5), |
| ... ] |
| >>> xs = [1, 3] |
| >>> ys = [2, 4] |
| >>> tracks, connections = make_tracks(xs, ys, pos) |
| >>> print_tracks(tracks) |
| [Track(direction='Y', x_low=1, x_high=1, y_low=1, y_high=5), |
| Track(direction='Y', x_low=3, x_high=3, y_low=1, y_high=5), |
| Track(direction='X', x_low=1, x_high=3, y_low=2, y_high=2), |
| Track(direction='X', x_low=1, x_high=3, y_low=4, y_high=4)] |
| >>> print(sorted(connections)) |
| [(2, 0), (2, 1), (3, 0)] |
| |
| >>> pos = [ |
| ... (68,48), (69,48), |
| ... (68,49), (69,49), |
| ... (69,50), |
| ... (69,51), |
| ... (69,52), |
| ... (69,53), (70,53), (71,53), (72,53)] |
| >>> xs = [68, 69] |
| >>> ys = [53] |
| >>> tracks, connections = make_tracks(xs, ys, pos) |
| >>> print_tracks(tracks) |
| [Track(direction='Y', x_low=68, x_high=68, y_low=48, y_high=53), |
| Track(direction='Y', x_low=69, x_high=69, y_low=48, y_high=53), |
| Track(direction='X', x_low=68, x_high=72, y_low=53, y_high=53)] |
| >>> print(connections) |
| [(2, 0), (2, 1)] |
| |
| |
| """ |
| x_set = set(xs) |
| y_set = set(ys) |
| |
| for x, y in points: |
| if x > 0: |
| in_x_set = x in x_set or x - 1 in x_set, (x, x_set) |
| else: |
| in_x_set = x in x_set |
| |
| if y > 0: |
| in_y_set = y in y_set or y - 1 in y_set, (y, y_set) |
| else: |
| in_y_set = y in y_set |
| |
| assert in_x_set or in_y_set |
| |
| all_xs, all_ys = zip(*points) |
| x_min = min(all_xs) |
| x_max = max(all_xs) |
| y_min = min(all_ys) |
| y_max = max(all_ys) |
| |
| tracks = [] |
| x_tracks = [] |
| y_tracks = [] |
| for x in xs: |
| y_low = max(y_min, 1) |
| y_high = y_max |
| if grid_height is not None: |
| y_high = min(y_max, grid_height - 2) |
| |
| tracks.append( |
| Track( |
| direction='Y', |
| x_low=x, |
| x_high=x, |
| y_low=y_low, |
| y_high=y_high, |
| ) |
| ) |
| y_tracks.append(len(tracks) - 1) |
| |
| for y in ys: |
| x_low = max(x_min, 1) |
| x_high = x_max |
| if grid_width is not None: |
| x_high = min(x_high, grid_width - 2) |
| |
| tracks.append( |
| Track( |
| direction='X', |
| x_low=x_low, |
| x_high=x_high, |
| y_low=y, |
| y_high=y, |
| ) |
| ) |
| x_tracks.append(len(tracks) - 1) |
| |
| if len(tracks) == 1: |
| return tracks, [] |
| |
| # If there is more than 1 track, there must be a track in each dimension |
| assert len(xs) >= 1 and len(ys) >= 1 |
| |
| connections = set() |
| |
| # Always just connect X tracks to the first Y track, and Y tracks to the |
| # first X tracks. |
| # |
| # To dedup connections, the x channel track will appear first in the |
| # connection list. |
| for idx, track in enumerate(tracks): |
| if track.direction == 'X': |
| connections.add((idx, y_tracks[0])) |
| else: |
| assert track.direction == 'Y' |
| connections.add((x_tracks[0], idx)) |
| |
| return tracks, list(connections) |
| |
| |
| class Tracks(object): |
| """Tracks groups tracks and their connections (AKA Node). |
| """ |
| |
| def __init__(self, tracks, track_connections): |
| self.tracks = tracks |
| self.track_connections = track_connections |
| |
| self.track_cache = {} |
| |
| def verify_tracks(self): |
| """ Verify that all tracks are connected to all other tracks. """ |
| track_connections = {} |
| |
| for idx, _ in enumerate(self.tracks): |
| track_connections[idx] = set((idx, )) |
| |
| for conn_a, conn_b in self.track_connections: |
| if track_connections[conn_a] is track_connections[conn_b]: |
| continue |
| |
| assert self.tracks[conn_a].direction != self.tracks[conn_b |
| ].direction |
| |
| track_connections[conn_a] |= track_connections[conn_b] |
| |
| for track_idx in track_connections[conn_a]: |
| track_connections[track_idx] = track_connections[conn_a] |
| |
| assert len( |
| set(id(s) for s in track_connections.values()) |
| ) == 1, track_connections |
| |
| def is_wire_adjacent_to_track(self, idx, coord): |
| """returns direction if wire at coord is adjacent to track at index idx |
| returns NO_SIDE if not adjacent. |
| """ |
| track = self.tracks[idx] |
| wire_x, wire_y = coord |
| |
| if track.direction == 'X': |
| pin_top = track.y_low == wire_y |
| pin_bottom = track.y_low == wire_y - 1 |
| adjacent_channel = (pin_top or pin_bottom) and ( |
| track.x_low <= wire_x and wire_x <= track.x_high |
| ) |
| |
| if adjacent_channel: |
| if pin_top: |
| return Direction.TOP |
| elif pin_bottom: |
| return Direction.BOTTOM |
| else: |
| assert False, (coord, track) |
| else: |
| return Direction.NO_SIDE |
| |
| elif track.direction == 'Y': |
| pin_right = track.x_low == wire_x |
| pin_left = track.x_low == wire_x - 1 |
| adjacent_channel = (pin_right or pin_left) and ( |
| track.y_low <= wire_y and wire_y <= track.y_high |
| ) |
| |
| if adjacent_channel: |
| if pin_right: |
| return Direction.RIGHT |
| elif pin_left: |
| return Direction.LEFT |
| else: |
| assert False, (coord, track) |
| else: |
| return Direction.NO_SIDE |
| else: |
| assert False, track |
| |
| def get_tracks_for_wire_at_coord(self, coord): |
| """Returns map of direction to track indicies for valid connections. |
| |
| There may be multiple connections to tracks at a particular coordinate, |
| this method only returns a possible connection in each direction to |
| this track. |
| |
| Parameters |
| ---------- |
| coord : (int, int) |
| Coordinate to attach to track |
| |
| Returns |
| ------- |
| dict(Direction -> int) |
| Dictionary of pin direction to track index. |
| |
| """ |
| |
| if coord in self.track_cache: |
| return self.track_cache[coord] |
| else: |
| wire_x, wire_y = coord |
| |
| conns = {} |
| for idx, track in enumerate(self.tracks): |
| pin_dir = self.is_wire_adjacent_to_track(idx, coord) |
| if pin_dir != Direction.NO_SIDE: |
| conns[pin_dir] = idx |
| |
| self.track_cache[coord] = conns |
| return conns |
| |
| |
| def main(): |
| import doctest |
| |
| print('Doctest begin') |
| failure_count, test_count = doctest.testmod(optionflags=doctest.ELLIPSIS) |
| assert test_count > 0 |
| assert failure_count == 0, "Doctests failed!" |
| print('Doctest end') |
| |
| |
| if __name__ == "__main__": |
| main() |