|  | #!/usr/bin/env python3 | 
|  | """ | 
|  | This file for packing tracks into channels. | 
|  |  | 
|  | It does *not* manage channel XML nodes. | 
|  |  | 
|  | Note channels go between switchboxes. Switchboxes cannot be at the last grid | 
|  | coordinate. | 
|  |  | 
|  | Therefore, you need a grid size of at least 3 rows or cols to allow any | 
|  | channels to exist. With a 3 width configuration you would get a channel of | 
|  | length 0, connecting the switchbox at 0 to the switchbox at 1. | 
|  |  | 
|  | With above in mind, objects here entirely omit the last row/col and placing a | 
|  | channel in the first is illegal. | 
|  |  | 
|  | Specifically: | 
|  | * For CHANX: X=0 is invalid, X=grid.width-1 is invalid | 
|  | * For CHANY: Y=0 is invalid, Y=grid.height-1 is invalid | 
|  | """ | 
|  |  | 
|  | import pprint | 
|  | import enum | 
|  | import io | 
|  | from collections import namedtuple | 
|  | import lxml.etree as ET | 
|  |  | 
|  | from . import Position | 
|  | from . import Size | 
|  |  | 
|  | from . import node_pos, single_element | 
|  | from ..asserts import assert_eq | 
|  | from ..asserts import assert_len_eq | 
|  | from ..asserts import assert_type | 
|  | from ..asserts import assert_type_or_none | 
|  |  | 
|  | # FIXME: doctests and static_property are not playing nice together. | 
|  | # from . import static_property | 
|  | static_property = property | 
|  |  | 
|  |  | 
|  | class ChannelNotStraight(TypeError): | 
|  | pass | 
|  |  | 
|  |  | 
|  | _Track = namedtuple( | 
|  | "Track", ("start", "end", "direction", "segment_id", "idx") | 
|  | ) | 
|  |  | 
|  |  | 
|  | class Track(_Track): | 
|  | """ | 
|  | Represents a single CHANX or CHANY (track) within a channel. | 
|  |  | 
|  | IE The tracks of a x_list or y_list entry in <channels> element. | 
|  |  | 
|  | start: start Pos | 
|  | end: end Pos | 
|  | idx: XML index integer | 
|  | """ | 
|  |  | 
|  | class Type(enum.Enum): | 
|  | """ | 
|  | subset of NodeType in graph2 | 
|  | """ | 
|  | # Horizontal routing | 
|  | X = 'CHANX' | 
|  | # Vertical routing | 
|  | Y = 'CHANY' | 
|  |  | 
|  | def __repr__(self): | 
|  | return 'Track.Type.' + self.name | 
|  |  | 
|  | class Direction(enum.Enum): | 
|  | INC = 'INC_DIR' | 
|  | DEC = 'DEC_DIR' | 
|  | BI = 'BI_DIR' | 
|  |  | 
|  | def __repr__(self): | 
|  | return 'Track.Direction.' + self.name | 
|  |  | 
|  | def __new__( | 
|  | cls, | 
|  | start, | 
|  | end, | 
|  | direction=Direction.INC, | 
|  | segment_id=0, | 
|  | idx=None, | 
|  | name=None, | 
|  | type_hint=None, | 
|  | ): | 
|  | """Make most but not all attributes immutable""" | 
|  |  | 
|  | if not isinstance(start, Position): | 
|  | start = Position(*start) | 
|  | assert_type(start, Position) | 
|  | if not isinstance(end, Position): | 
|  | end = Position(*end) | 
|  | assert_type(end, Position) | 
|  |  | 
|  | if start.x != end.x and start.y != end.y: | 
|  | raise ChannelNotStraight( | 
|  | "Track not straight! {}->{}".format(start, end) | 
|  | ) | 
|  |  | 
|  | assert_type(direction, cls.Direction) | 
|  | assert_type(segment_id, int) | 
|  | assert_type_or_none(idx, int) | 
|  | assert_type_or_none(name, str) | 
|  | assert_type_or_none(type_hint, cls.Type) | 
|  |  | 
|  | obj = _Track.__new__(cls, start, end, direction, segment_id, idx) | 
|  | obj.name = name | 
|  | obj.type_hint = type_hint | 
|  | return obj | 
|  |  | 
|  | @static_property | 
|  | def type(self): | 
|  | """Type of the channel. | 
|  |  | 
|  | Returns: Track.Type | 
|  |  | 
|  | >>> Track((1, 0), (10, 0)).type | 
|  | Track.Type.X | 
|  | >>> Track((0, 1), (0, 10)).type | 
|  | Track.Type.Y | 
|  | >>> Track((1, 1), (1, 1)).type | 
|  | Traceback (most recent call last): | 
|  | ... | 
|  | ValueError: Ambiguous type | 
|  | >>> Track((1, 1), (1, 1), type_hint=Track.Type.X).type | 
|  | Track.Type.X | 
|  | >>> Track((1, 1), (1, 1), type_hint=Track.Type.Y).type | 
|  | Track.Type.Y | 
|  |  | 
|  | """ | 
|  | if self.type_hint: | 
|  | return self.type_hint | 
|  | guess = self.type_guess | 
|  | if guess is None: | 
|  | raise ValueError("Ambiguous type") | 
|  | return guess | 
|  |  | 
|  | @static_property | 
|  | def type_guess(self): | 
|  | """Type of the channel. | 
|  |  | 
|  | Returns: Track.Type | 
|  |  | 
|  | >>> Track((1, 0), (10, 0)).type_guess | 
|  | Track.Type.X | 
|  | >>> Track((0, 1), (0, 10)).type_guess | 
|  | Track.Type.Y | 
|  | >>> str(Track((1, 1), (1, 1)).type_guess) | 
|  | 'None' | 
|  | """ | 
|  | if self.start.x == self.end.x and self.start.y == self.end.y: | 
|  | return None | 
|  | elif self.start.x == self.end.x: | 
|  | return Track.Type.Y | 
|  | elif self.start.y == self.end.y: | 
|  | return Track.Type.X | 
|  | else: | 
|  | assert False, self | 
|  |  | 
|  | def positions(self): | 
|  | """Generate all positions this track occupies""" | 
|  | startx, endx = sorted([self.start.x, self.end.x]) | 
|  | starty, endy = sorted([self.start.y, self.end.y]) | 
|  |  | 
|  | for x in range(startx, endx + 1): | 
|  | for y in range(starty, endy + 1): | 
|  | yield Position(x, y) | 
|  |  | 
|  | @static_property | 
|  | def start0(self): | 
|  | """The non-constant start coordinate. | 
|  |  | 
|  | >>> Track((1, 0), (10, 0)).start0 | 
|  | 1 | 
|  | >>> Track((0, 1), (0, 10)).start0 | 
|  | 1 | 
|  | >>> Track((1, 1), (1, 1)).start0 | 
|  | Traceback (most recent call last): | 
|  | ... | 
|  | ValueError: Ambiguous type | 
|  | >>> Track((1, 1), (1, 1), type_hint=Track.Type.X).start0 | 
|  | 1 | 
|  | >>> Track((10, 0), (1, 0)).start0 | 
|  | 10 | 
|  | >>> Track((0, 10), (0, 1)).start0 | 
|  | 10 | 
|  | """ | 
|  | if self.type == Track.Type.X: | 
|  | return self.start.x | 
|  | elif self.type == Track.Type.Y: | 
|  | return self.start.y | 
|  | else: | 
|  | assert False | 
|  |  | 
|  | @static_property | 
|  | def end0(self): | 
|  | """The non-constant end coordinate. | 
|  |  | 
|  | >>> Track((1, 0), (10, 0)).end0 | 
|  | 10 | 
|  | >>> Track((0, 1), (0, 10)).end0 | 
|  | 10 | 
|  | >>> Track((1, 1), (1, 1)).end0 | 
|  | Traceback (most recent call last): | 
|  | ... | 
|  | ValueError: Ambiguous type | 
|  | >>> Track((1, 1), (1, 1), type_hint=Track.Type.X).end0 | 
|  | 1 | 
|  | >>> Track((10, 0), (1, 0)).end0 | 
|  | 1 | 
|  | >>> Track((0, 10), (0, 1)).end0 | 
|  | 1 | 
|  | """ | 
|  | if self.type == Track.Type.X: | 
|  | return self.end.x | 
|  | elif self.type == Track.Type.Y: | 
|  | return self.end.y | 
|  | else: | 
|  | assert False, self.type | 
|  |  | 
|  | @static_property | 
|  | def common(self): | 
|  | """The common coordinate value. | 
|  |  | 
|  | >>> Track((0, 0), (10, 0)).common | 
|  | 0 | 
|  | >>> Track((0, 0), (0, 10)).common | 
|  | 0 | 
|  | >>> Track((1, 1), (1, 1)).common | 
|  | Traceback (most recent call last): | 
|  | ... | 
|  | ValueError: Ambiguous type | 
|  | >>> Track((1, 1), (1, 1), type_hint=Track.Type.X).common | 
|  | 1 | 
|  | >>> Track((10, 0), (0, 0)).common | 
|  | 0 | 
|  | >>> Track((0, 10), (0, 0)).common | 
|  | 0 | 
|  | >>> Track((4, 10), (4, 0)).common | 
|  | 4 | 
|  | """ | 
|  | if self.type == Track.Type.X: | 
|  | assert_eq(self.start.y, self.end.y) | 
|  | return self.start.y | 
|  | elif self.type == Track.Type.Y: | 
|  | assert_eq(self.start.x, self.end.x) | 
|  | return self.start.x | 
|  | else: | 
|  | assert False | 
|  |  | 
|  | @static_property | 
|  | def length(self): | 
|  | """Length of the track. | 
|  |  | 
|  | >>> Track((0, 0), (10, 0)).length | 
|  | 10 | 
|  | >>> Track((0, 0), (0, 10)).length | 
|  | 10 | 
|  | >>> Track((1, 1), (1, 1)).length | 
|  | 0 | 
|  | >>> Track((10, 0), (0, 0)).length | 
|  | 10 | 
|  | >>> Track((0, 10), (0, 0)).length | 
|  | 10 | 
|  | """ | 
|  | try: | 
|  | return abs(self.end0 - self.start0) | 
|  | except ValueError: | 
|  | return 0 | 
|  |  | 
|  | def new_idx(self, idx): | 
|  | """Create a new channel with the same start/end but new index value. | 
|  |  | 
|  | >>> s = (1, 4) | 
|  | >>> e = (1, 8) | 
|  | >>> c1 = Track(s, e, idx=0) | 
|  | >>> c2 = c1.new_idx(2) | 
|  | >>> assert_eq(c1.start, c2.start) | 
|  | >>> assert_eq(c1.end, c2.end) | 
|  | >>> c1.idx | 
|  | 0 | 
|  | >>> c2.idx | 
|  | 2 | 
|  | """ | 
|  | return self.__class__( | 
|  | self.start, | 
|  | self.end, | 
|  | self.direction, | 
|  | self.segment_id, | 
|  | idx, | 
|  | name=self.name, | 
|  | type_hint=self.type_hint, | 
|  | ) | 
|  |  | 
|  | def __repr__(self): | 
|  | """ | 
|  |  | 
|  | >>> repr(Track((0, 0), (10, 0))) | 
|  | 'T((0,0), (10,0))' | 
|  | >>> repr(Track((0, 0), (0, 10))) | 
|  | 'T((0,0), (0,10))' | 
|  | >>> repr(Track((1, 2), (3, 2), idx=5)) | 
|  | 'T((1,2), (3,2), 5)' | 
|  | >>> repr(Track((1, 2), (3, 2), name="ABC")) | 
|  | 'T(ABC)' | 
|  | >>> repr(Track((1, 2), (3, 2), idx=5, name="ABC")) | 
|  | 'T(ABC,5)' | 
|  | """ | 
|  | if self.name: | 
|  | idx_str = "" | 
|  | if self.idx is not None: | 
|  | idx_str = ",{}".format(self.idx) | 
|  | return "T({}{})".format(self.name, idx_str) | 
|  |  | 
|  | idx_str = "" | 
|  | if self.idx is not None: | 
|  | idx_str = ", {}".format(self.idx) | 
|  | return "T(({},{}), ({},{}){})".format( | 
|  | self.start.x, self.start.y, self.end.x, self.end.y, idx_str | 
|  | ) | 
|  |  | 
|  | def __str__(self): | 
|  | """ | 
|  |  | 
|  | >>> str(Track((0, 0), (10, 0))) | 
|  | 'CHANX 0,0->10,0' | 
|  | >>> str(Track((0, 0), (0, 10))) | 
|  | 'CHANY 0,0->0,10' | 
|  | >>> str(Track((1, 2), (3, 2), idx=5)) | 
|  | 'CHANX 1,2->3,2 @5' | 
|  | >>> str(Track((1, 2), (3, 2), name="ABC")) | 
|  | 'ABC' | 
|  | >>> str(Track((1, 2), (3, 2), idx=5, name="ABC")) | 
|  | 'ABC@5' | 
|  | """ | 
|  | idx_str = "" | 
|  | if self.idx is not None: | 
|  | idx_str = " @{}".format(self.idx) | 
|  | if self.name: | 
|  | return "{}{}".format(self.name, idx_str[1:]) | 
|  | return "{} {},{}->{},{}{}".format( | 
|  | self.type.value, self.start.x, self.start.y, self.end.x, | 
|  | self.end.y, idx_str | 
|  | ) | 
|  |  | 
|  |  | 
|  | # Short alias. | 
|  | T = Track | 
|  |  | 
|  |  | 
|  | class ChannelGrid(dict): | 
|  | """ | 
|  | Functionality: | 
|  | * Manages single type of track (either `CHANX` or `CHANY`). | 
|  | * Manages channel width along grid. | 
|  | * Allocates tracks within channels. | 
|  |  | 
|  | The `ChannelGrid` is indexed by `Position` and returns a sequence width all | 
|  | the `Track`s at that position. | 
|  | """ | 
|  |  | 
|  | def __init__(self, size, chan_type): | 
|  | """ | 
|  | size: Size representing tile grid width/height | 
|  | chan_type: of Channels.Type | 
|  | """ | 
|  | self.chan_type = chan_type | 
|  | self.size = Size(*size) | 
|  |  | 
|  | self.clear() | 
|  |  | 
|  | @property | 
|  | def width(self): | 
|  | """Grid width | 
|  |  | 
|  | >>> g = ChannelGrid((6, 7), Track.Type.Y) | 
|  | >>> g.width | 
|  | 6 | 
|  | """ | 
|  | return self.size.width | 
|  |  | 
|  | @property | 
|  | def height(self): | 
|  | """Grid height | 
|  |  | 
|  | >>> g = ChannelGrid((6, 7), Track.Type.Y) | 
|  | >>> g.height | 
|  | 7 | 
|  | """ | 
|  | return self.size.height | 
|  |  | 
|  | def column(self, x): | 
|  | """Get a y coordinate indexed list giving tracks at that x + y position""" | 
|  | column = [] | 
|  | for y in range(0, self.height): | 
|  | column.append(self[Position(x, y)]) | 
|  | return column | 
|  |  | 
|  | def row(self, y): | 
|  | """Get an x coordinate indexed list giving tracks at that x + y position""" | 
|  | row = [] | 
|  | for x in range(0, self.width): | 
|  | row.append(self[Position(x, y)]) | 
|  | return row | 
|  |  | 
|  | """ | 
|  | dim_*: CHANX/CHANY abstraction functions | 
|  | These can be used to write code that is not aware of specifics related to CHANX vs CHANY | 
|  | """ | 
|  |  | 
|  | def dim_rc(self): | 
|  | """Get dimension a, the number of row/col positions""" | 
|  | return { | 
|  | Track.Type.X: self.height, | 
|  | Track.Type.Y: self.width, | 
|  | }[self.chan_type] | 
|  |  | 
|  | def dim_chanl(self): | 
|  | """Get dimension b, the number of valid track positions within a specific channel""" | 
|  | return { | 
|  | Track.Type.X: self.width, | 
|  | Track.Type.Y: self.height, | 
|  | }[self.chan_type] | 
|  |  | 
|  | def foreach_position(self): | 
|  | """Generate all valid placement positions (exclude border)""" | 
|  | xmin, ymin = { | 
|  | Track.Type.X: (1, 0), | 
|  | Track.Type.Y: (0, 1), | 
|  | }[self.chan_type] | 
|  |  | 
|  | for row in range(ymin, self.height): | 
|  | for col in range(xmin, self.width): | 
|  | yield Position(col, row) | 
|  |  | 
|  | def foreach_track(self): | 
|  | """Generate all current legal channel positions (exclude border)""" | 
|  | for pos in self.foreach_position(): | 
|  | for ti, t in enumerate(self[pos]): | 
|  | yield (pos, ti, t) | 
|  |  | 
|  | def slicen(self): | 
|  | """Get grid width or height corresponding to chanx/chany type""" | 
|  | return { | 
|  | Track.Type.X: self.height, | 
|  | Track.Type.Y: self.width, | 
|  | }[self.chan_type] | 
|  |  | 
|  | def slice(self, i): | 
|  | """Get row or col corresponding to chanx/chany type""" | 
|  | return { | 
|  | Track.Type.X: self.row, | 
|  | Track.Type.Y: self.column, | 
|  | }[self.chan_type]( | 
|  | i | 
|  | ) | 
|  |  | 
|  | def track_slice(self, t): | 
|  | """Get the row or column the track runs along""" | 
|  | return { | 
|  | Track.Type.X: self.row, | 
|  | Track.Type.Y: self.column, | 
|  | }[t.type]( | 
|  | t.common | 
|  | ) | 
|  |  | 
|  | def tracks(self): | 
|  | """Get all channels in a set""" | 
|  | ret = set() | 
|  | for _pos, _ti, t in self.foreach_track(): | 
|  | ret.add(t) | 
|  | return ret | 
|  |  | 
|  | def validate_pos(self, pos, msg=''): | 
|  | """ | 
|  | A channel must go between switchboxes (where channels can cross) | 
|  | Channels are upper right of tile | 
|  | Therefore, the first position in a channel cannot have a track because | 
|  | there is no proceeding switchbox""" | 
|  | if msg: | 
|  | msg = msg + ': ' | 
|  | # Gross error out of grid | 
|  | if pos.x < 0 or pos.y < 0 or pos.x >= self.width or pos.y >= self.height: | 
|  | raise ValueError( | 
|  | "%sGrid %s, point %s out of grid size coordinate" % | 
|  | (msg, self.size, pos) | 
|  | ) | 
|  |  | 
|  | if self.chan_type == Track.Type.X and pos.x == 0: | 
|  | raise ValueError("%sInvalid CHANX x=0 point %s" % (msg, pos)) | 
|  | if self.chan_type == Track.Type.Y and pos.y == 0: | 
|  | raise ValueError("%sInvalid CHANY y=0 point %s" % (msg, pos)) | 
|  |  | 
|  | def create_track(self, t, idx=None): | 
|  | """ | 
|  | Channel allocator | 
|  | Finds an optimal place to put the channel, increasing the channel width if necessary | 
|  | If idx is given, it must go there | 
|  | Throw exception if location is already occupied | 
|  |  | 
|  | >>> g = ChannelGrid((11, 11), Track.Type.X) | 
|  | >>> # Adding the first channel | 
|  | >>> g.create_track(Track((1, 6), (4, 6), name="A")) | 
|  | T(A,0) | 
|  | >>> g[(1,6)] | 
|  | [T(A,0)] | 
|  | >>> g[(2,6)] | 
|  | [T(A,0)] | 
|  | >>> g[(4,6)] | 
|  | [T(A,0)] | 
|  | >>> g[(5,6)] | 
|  | [None] | 
|  | >>> # Adding second non-overlapping second channel | 
|  | >>> g.create_track(Track((5, 6), (7, 6), name="B")) | 
|  | T(B,0) | 
|  | >>> g[(4,6)] | 
|  | [T(A,0)] | 
|  | >>> g[(5,6)] | 
|  | [T(B,0)] | 
|  | >>> g[(7,6)] | 
|  | [T(B,0)] | 
|  | >>> g[(8,6)] | 
|  | [None] | 
|  | >>> # Adding third channel which overlaps with second channel | 
|  | >>> g.create_track(Track((5, 6), (7, 6), name="T")) | 
|  | T(T,1) | 
|  | >>> g[(4,6)] | 
|  | [T(A,0), None] | 
|  | >>> g[(5,6)] | 
|  | [T(B,0), T(T,1)] | 
|  | >>> g[(7,6)] | 
|  | [T(B,0), T(T,1)] | 
|  | >>> # Adding a channel which overlaps, but is a row over | 
|  | >>> g.create_track(Track((5, 7), (7, 7), name="D")) | 
|  | T(D,0) | 
|  | >>> g[(5,6)] | 
|  | [T(B,0), T(T,1)] | 
|  | >>> g[(5,7)] | 
|  | [T(D,0)] | 
|  | >>> # Adding fourth channel which overlaps both the first | 
|  | >>> # and second+third channel | 
|  | >>> g.create_track(Track((3, 6), (6, 6), name="E")) | 
|  | T(E,2) | 
|  | >>> g[(2,6)] | 
|  | [T(A,0), None, None] | 
|  | >>> g[(3,6)] | 
|  | [T(A,0), None, T(E,2)] | 
|  | >>> g[(6,6)] | 
|  | [T(B,0), T(T,1), T(E,2)] | 
|  | >>> g[(7,6)] | 
|  | [T(B,0), T(T,1), None] | 
|  | >>> # This channel fits in the hole left by the last one. | 
|  | >>> g.create_track(Track((1, 6), (3, 6), name="F")) | 
|  | T(F,1) | 
|  | >>> g[(1,6)] | 
|  | [T(A,0), T(F,1), None] | 
|  | >>> g[(2,6)] | 
|  | [T(A,0), T(F,1), None] | 
|  | >>> g[(3,6)] | 
|  | [T(A,0), T(F,1), T(E,2)] | 
|  | >>> g[(4,6)] | 
|  | [T(A,0), None, T(E,2)] | 
|  | >>> # Add another channel which causes a hole | 
|  | >>> g.create_track(Track((1, 6), (7, 6), name="G")) | 
|  | T(G,3) | 
|  | >>> g[(1,6)] | 
|  | [T(A,0), T(F,1), None, T(G,3)] | 
|  | >>> g[(2,6)] | 
|  | [T(A,0), T(F,1), None, T(G,3)] | 
|  | >>> g[(3,6)] | 
|  | [T(A,0), T(F,1), T(E,2), T(G,3)] | 
|  | >>> g[(4,6)] | 
|  | [T(A,0), None, T(E,2), T(G,3)] | 
|  | >>> g[(5,6)] | 
|  | [T(B,0), T(T,1), T(E,2), T(G,3)] | 
|  | >>> g[(6,6)] | 
|  | [T(B,0), T(T,1), T(E,2), T(G,3)] | 
|  | >>> g[(7,6)] | 
|  | [T(B,0), T(T,1), None, T(G,3)] | 
|  | >>> g[(8,6)] | 
|  | [None, None, None, None] | 
|  | """ | 
|  | assert t.idx is None | 
|  | force_idx = idx | 
|  |  | 
|  | self.validate_pos(t.start, 'start') | 
|  | self.validate_pos(t.end, 'end') | 
|  |  | 
|  | if t.type_guess != self.chan_type: | 
|  | if t.length != 0: | 
|  | raise TypeError( | 
|  | "Can only add channels of type {} which {} ({}) is not.". | 
|  | format(self.chan_type, t, t.type) | 
|  | ) | 
|  | else: | 
|  | t.type_hint = self.chan_type | 
|  |  | 
|  | c = self.track_slice(t) | 
|  | assert_len_eq(c) | 
|  |  | 
|  | # Find start and end | 
|  | s, e = min(t.start0, t.end0), max(t.start0, t.end0) | 
|  | assert e >= s, (e, '>=', s) | 
|  | assert s < len(c), (s, '<', len(c), c) | 
|  | assert e < len(c), (e + 1, '<', len(c), c) | 
|  |  | 
|  | # Find a idx that this channel fits. | 
|  | # Normally start at first channel (0) unless forcing to a specific channel | 
|  | max_idx = force_idx if force_idx is not None else 0 | 
|  | while True: | 
|  | # Check each position if the track can fit | 
|  | # Expanding channel width as required index grows | 
|  | for p in c[s:e + 1]: | 
|  | while len(p) < max_idx + 1: | 
|  | p.append(None) | 
|  | # Can't place here? | 
|  | if p[max_idx] is not None: | 
|  | # Grow track width | 
|  | if force_idx is not None: | 
|  | raise IndexError( | 
|  | "Can't fit channel at index %d" % force_idx | 
|  | ) | 
|  | max_idx += 1 | 
|  | break | 
|  | # Was able to place into all locations | 
|  | else: | 
|  | break | 
|  |  | 
|  | # Make sure everything has the same length. | 
|  | for p in c: | 
|  | while len(p) < max_idx + 1: | 
|  | p.append(None) | 
|  | assert_len_eq(c) | 
|  |  | 
|  | t = t.new_idx(max_idx) | 
|  | assert t.idx == max_idx | 
|  | for p in c[s:e + 1]: | 
|  | p[t.idx] = t | 
|  | return t | 
|  |  | 
|  | def pretty_print(self): | 
|  | """ | 
|  | If type == Track.Type.X | 
|  |  | 
|  | A--AC-C | 
|  | B-----B | 
|  |  | 
|  | D--DE-E | 
|  | F-----F | 
|  |  | 
|  | If type == Track.Type.Y | 
|  |  | 
|  | AB  DF | 
|  | ||  || | 
|  | ||  || | 
|  | A|  D| | 
|  | C|  E| | 
|  | ||  || | 
|  | CB  EF | 
|  |  | 
|  | """ | 
|  |  | 
|  | def get_str(t): | 
|  | if not t: | 
|  | s = "" | 
|  | elif t.name: | 
|  | s = t.name | 
|  | else: | 
|  | s = str(t) | 
|  | return s | 
|  |  | 
|  | # Work out how many characters the largest label takes up. | 
|  | s_maxlen = 1 | 
|  | for row in range(0, self.height): | 
|  | for col in range(0, self.width): | 
|  | for t in self[(col, row)]: | 
|  | s_maxlen = max(s_maxlen, len(get_str(t))) | 
|  |  | 
|  | assert s_maxlen > 0, s_maxlen | 
|  | s_maxlen += 3 | 
|  | if self.chan_type == Track.Type.Y: | 
|  | beg_fmt = "{:^%i}" % s_maxlen | 
|  | end_fmt = beg_fmt | 
|  | mid_fmt = beg_fmt.format("||") | 
|  | elif self.chan_type == Track.Type.X: | 
|  | beg_fmt = "{:>%i}>" % (s_maxlen - 1) | 
|  | end_fmt = "->{:<%i}" % (s_maxlen - 2) | 
|  | mid_fmt = "-" * s_maxlen | 
|  | else: | 
|  | assert False | 
|  | non_fmt = " " * s_maxlen | 
|  | """ | 
|  | rows[row][col][c] | 
|  | row: global row location | 
|  | col: column of output | 
|  | c: character showing occupation along a track | 
|  |  | 
|  | Channel width may vary across tiles, but all columns within that region | 
|  | should have the same length | 
|  | """ | 
|  | rows = [] | 
|  | for y in range(0, self.height): | 
|  | cols = [] | 
|  | for x in range(0, self.width): | 
|  | # Header | 
|  | hdri = {Track.Type.X: x, Track.Type.Y: y}[self.chan_type] | 
|  | channels = [("|{: ^%i}" % (s_maxlen - 1)).format(hdri)] | 
|  |  | 
|  | for t in self[(x, y)]: | 
|  | if not t: | 
|  | fmt = non_fmt | 
|  | elif t.start == t.end: | 
|  | s = get_str(t) | 
|  | channels.append( | 
|  | "{} ".format( | 
|  | "".join( | 
|  | [ | 
|  | beg_fmt.format(s), | 
|  | mid_fmt.format(s), | 
|  | end_fmt.format(s), | 
|  | ] | 
|  | )[:s_maxlen - 1] | 
|  | ) | 
|  | ) | 
|  | continue | 
|  | elif t.start == (x, y): | 
|  | fmt = beg_fmt | 
|  | elif t.end == (x, y): | 
|  | fmt = end_fmt | 
|  | else: | 
|  | fmt = mid_fmt | 
|  |  | 
|  | channels.append(fmt.format(get_str(t))) | 
|  | cols.append(channels) | 
|  | rows.append(cols) | 
|  |  | 
|  | # Dump the track state as a string | 
|  | f = io.StringIO() | 
|  |  | 
|  | def p(*args, **kw): | 
|  | print(*args, file=f, **kw) | 
|  |  | 
|  | if self.chan_type == Track.Type.X: | 
|  | for r in range(0, len(rows)): | 
|  | assert_len_eq(rows[r]) | 
|  | # tracks + 1 for header | 
|  | track_rows = len(rows[r][0]) | 
|  | for tracki in range(0, track_rows): | 
|  | for c in range(0, len(rows[r])): | 
|  | p(rows[r][c][tracki], end="") | 
|  | # Close header | 
|  | if tracki == 0: | 
|  | p("|", end="") | 
|  | p() | 
|  | p("\n") | 
|  | elif self.chan_type == Track.Type.Y: | 
|  | for r in range(0, len(rows)): | 
|  | # tracks + 1 for header | 
|  | for c in range(0, len(rows[r])): | 
|  | track_cols = len(rows[r][c]) | 
|  | p("|*", end="") | 
|  | for tracki in range(0, track_cols): | 
|  | p(rows[r][c][tracki], end="") | 
|  | # Close header | 
|  | if tracki == 0: | 
|  | p("|", end="") | 
|  | p("  ", end="") | 
|  | p("") | 
|  | # p("|*|") | 
|  | else: | 
|  | assert False | 
|  |  | 
|  | return f.getvalue() | 
|  |  | 
|  | def clear(self): | 
|  | """Remove tracks from all currently occupied positions, making channel width 0""" | 
|  | for x in range(0, self.width): | 
|  | for y in range(0, self.height): | 
|  | self[Position(x, y)] = [] | 
|  |  | 
|  | def check(self): | 
|  | """Self integrity check""" | 
|  | # Verify uniform track length | 
|  | if self.chan_type == Track.Type.X: | 
|  | for y in range(self.height): | 
|  | assert_len_eq(self.row(y)) | 
|  | elif self.chan_type == Track.Type.Y: | 
|  | for x in range(self.width): | 
|  | assert_len_eq(self.column(x)) | 
|  | else: | 
|  | assert False | 
|  |  | 
|  | def density(self): | 
|  | """Return (number occupied positions, total number positions)""" | 
|  | occupied = 0 | 
|  | net = 0 | 
|  | for _pos, _ti, t in self.foreach_track(): | 
|  | net += 1 | 
|  | if t is not None: | 
|  | occupied += 1 | 
|  | return occupied, net | 
|  |  | 
|  | def fill_empty(self, segment_id, name=None): | 
|  | tracks = [] | 
|  | for pos, ti, t in self.foreach_track(): | 
|  | if t is None: | 
|  | tracks.append( | 
|  | self.create_track( | 
|  | Track( | 
|  | pos, | 
|  | pos, | 
|  | segment_id=segment_id, | 
|  | name=name, | 
|  | type_hint=self.chan_type, | 
|  | direction=Track.Direction.BI | 
|  | ), | 
|  | idx=ti | 
|  | ) | 
|  | ) | 
|  | return tracks | 
|  |  | 
|  | def channel_widths(self): | 
|  | """Return (min channel width, max channel width, row/col widths)""" | 
|  | cwmin = float('+inf') | 
|  | cwmax = float('-inf') | 
|  | xy_list = [] | 
|  | for i in range(self.slicen()): | 
|  | # track width should be consistent along a slice | 
|  | # just take the first element | 
|  | loc = self.slice(i)[0] | 
|  | cwmin = min(cwmin, len(loc)) | 
|  | cwmax = max(cwmax, len(loc)) | 
|  | xy_list.append(len(loc)) | 
|  | return (cwmin, cwmax, xy_list) | 
|  |  | 
|  | def assert_width(self, width): | 
|  | """Assert all channels have specified --route_chan_width""" | 
|  | for pos in self.foreach_position(): | 
|  | tracks = self[pos] | 
|  | assert len( | 
|  | tracks | 
|  | ) == width, 'Bad width Position(x=%d, y=%d): expect %d, got %d' % ( | 
|  | pos.x, pos.y, width, len(tracks) | 
|  | ) | 
|  |  | 
|  | def assert_full(self): | 
|  | """Assert all allocated channels are fully occupied""" | 
|  | self.check() | 
|  | # occupied, net = self.density() | 
|  | # print("Occupied %d / %d" % (occupied, net)) | 
|  | for pos, ti, t in self.foreach_track(): | 
|  | assert t is not None, 'Unoccupied Position(x=%d, y=%d) track=%d' % ( | 
|  | pos.x, pos.y, ti | 
|  | ) | 
|  |  | 
|  |  | 
|  | class Channels: | 
|  | """Holds all channels for the whole grid (X + Y)""" | 
|  |  | 
|  | def __init__(self, size): | 
|  | self.size = size | 
|  | self.x = ChannelGrid(size, Track.Type.X) | 
|  | self.y = ChannelGrid(size, Track.Type.Y) | 
|  |  | 
|  | def create_diag_track(self, start, end, segment_id, idx=None): | 
|  | # Actually these can be tuple as well | 
|  | # assert_type(start, Pos) | 
|  | # assert_type(end, Pos) | 
|  |  | 
|  | # Create track(s) | 
|  | try: | 
|  | return (self.create_xy_track(start, end, segment_id, idx=idx), ) | 
|  | except ChannelNotStraight: | 
|  | assert idx is None, idx | 
|  | corner = (start.x, end.y) | 
|  | ta = self.create_xy_track(start, corner, segment_id)[0] | 
|  | tb = self.create_xy_track(corner, end, segment_id)[0] | 
|  | return (ta, tb) | 
|  |  | 
|  | def create_xy_track( | 
|  | self, | 
|  | start, | 
|  | end, | 
|  | segment_id, | 
|  | idx=None, | 
|  | name=None, | 
|  | typeh=None, | 
|  | direction=None | 
|  | ): | 
|  | """ | 
|  | idx: None to automatically allocate | 
|  | """ | 
|  | # Actually these can be tuple as well | 
|  | # assert_type(start, Pos) | 
|  | # assert_type(end, Pos) | 
|  |  | 
|  | # Create track(s) | 
|  | # Will throw exception if not straight | 
|  | t = Track( | 
|  | start, | 
|  | end, | 
|  | segment_id=segment_id, | 
|  | name=name, | 
|  | type_hint=typeh, | 
|  | direction=direction | 
|  | ) | 
|  |  | 
|  | # Add the track to associated channel list | 
|  | # Get the track now with the index assigned | 
|  | t = { | 
|  | Track.Type.X: self.x.create_track, | 
|  | Track.Type.Y: self.y.create_track | 
|  | }[t.type]( | 
|  | t, idx=idx | 
|  | ) | 
|  | # print('create %s %s to %s idx %s' % (t.type, start, end, idx)) | 
|  |  | 
|  | assert t.idx is not None | 
|  | if typeh: | 
|  | assert t.type == typeh, (t.type.value, typeh) | 
|  | return t | 
|  |  | 
|  | def pad_channels(self, segment_id): | 
|  | tracks = [] | 
|  | tracks.extend(self.x.fill_empty(segment_id)) | 
|  | tracks.extend(self.y.fill_empty(segment_id)) | 
|  | return tracks | 
|  |  | 
|  | def pretty_print(self): | 
|  | s = '' | 
|  | s += 'X\n' | 
|  | s += self.x.pretty_print() | 
|  | s += 'Y\n' | 
|  | s += self.y.pretty_print() | 
|  | return s | 
|  |  | 
|  | def clear(self): | 
|  | """Remove all channels""" | 
|  | self.x.clear() | 
|  | self.y.clear() | 
|  |  | 
|  | def from_xml_nodes(self, nodes_xml): | 
|  | """Add channels from <nodes> CHANX/CHANY""" | 
|  | for node_xml in nodes_xml: | 
|  | ntype = node_xml.get('type') | 
|  | if ntype not in ('CHANX', 'CHANY'): | 
|  | continue | 
|  | ntype_e = Track.Type(ntype) | 
|  |  | 
|  | direction = Track.Direction(node_xml.get('direction')) | 
|  |  | 
|  | loc = single_element(node_xml, 'loc') | 
|  | idx = int(loc.get('ptc')) | 
|  | pos_low, pos_high = node_pos(node_xml) | 
|  | # print('Importing %s @ %s:%s :: %d' % (ntype, pos_low, pos_high, idx)) | 
|  |  | 
|  | segment_xml = single_element(node_xml, 'segment') | 
|  | segment_id = int(segment_xml.get('segment_id')) | 
|  |  | 
|  | # idx will get assigned when adding to track | 
|  | try: | 
|  | self.create_xy_track( | 
|  | pos_low, | 
|  | pos_high, | 
|  | segment_id, | 
|  | idx=idx, | 
|  | # XML has no name concept. Should it? | 
|  | name=None, | 
|  | typeh=ntype_e, | 
|  | direction=direction | 
|  | ) | 
|  | except Exception: | 
|  | print("Bad XML: %s" % (ET.tostring(node_xml))) | 
|  | raise | 
|  |  | 
|  | def to_xml_channels(self, channels_xml): | 
|  | channels_xml.clear() | 
|  |  | 
|  | # channel entry | 
|  | cw_xmin, cw_xmax, x_lists = self.x.channel_widths() | 
|  | cw_ymin, cw_ymax, y_lists = self.y.channel_widths() | 
|  | cw_max = max(cw_xmax, cw_ymax) | 
|  | ET.SubElement( | 
|  | channels_xml, 'channel', { | 
|  | 'chan_width_max': str(cw_max), | 
|  | 'x_min': str(cw_xmin), | 
|  | 'x_max': str(cw_xmax), | 
|  | 'y_min': str(cw_ymin), | 
|  | 'y_max': str(cw_ymax), | 
|  | } | 
|  | ) | 
|  | # x_list / y_list tries | 
|  | for i, info in enumerate(x_lists): | 
|  | ET.SubElement( | 
|  | channels_xml, 'x_list', { | 
|  | 'index': str(i), | 
|  | 'info': str(info) | 
|  | } | 
|  | ) | 
|  | for i, info in enumerate(y_lists): | 
|  | ET.SubElement( | 
|  | channels_xml, 'y_list', { | 
|  | 'index': str(i), | 
|  | 'info': str(info) | 
|  | } | 
|  | ) | 
|  |  | 
|  | def to_xml(self, xml_graph): | 
|  | self.to_xml_channels(single_element(xml_graph, 'channels')) | 
|  |  | 
|  |  | 
|  | def TX(start, end, idx=None, name=None, direction=None, segment_id=None): | 
|  | if direction is None: | 
|  | direction = Track.Direction.INC | 
|  | if segment_id is None: | 
|  | segment_id = 0 | 
|  | return T( | 
|  | start, | 
|  | end, | 
|  | direction=direction, | 
|  | segment_id=segment_id, | 
|  | idx=idx, | 
|  | name=name, | 
|  | type_hint=Track.Type.X, | 
|  | ) | 
|  |  | 
|  |  | 
|  | def TY(start, end, idx=None, name=None, direction=None, segment_id=None): | 
|  | if direction is None: | 
|  | direction = Track.Direction.INC | 
|  | if segment_id is None: | 
|  | segment_id = 0 | 
|  | return T( | 
|  | start, | 
|  | end, | 
|  | direction=direction, | 
|  | segment_id=segment_id, | 
|  | idx=idx, | 
|  | name=name, | 
|  | type_hint=Track.Type.Y, | 
|  | ) | 
|  |  | 
|  |  | 
|  | def docprint(x): | 
|  | pprint.pprint(x.splitlines()) | 
|  |  | 
|  |  | 
|  | def create_test_channel_grid(): | 
|  | g = ChannelGrid((6, 3), Track.Type.X) | 
|  | g.create_track(TX((1, 0), (5, 0), name="AA")) | 
|  | g.create_track(TX((1, 0), (3, 0), name="BB")) | 
|  | g.create_track(TX((2, 0), (5, 0), name="CC")) | 
|  | g.create_track(TX((1, 0), (1, 0), name="DD")) | 
|  |  | 
|  | g.create_track(TX((1, 1), (3, 1), name="aa")) | 
|  | g.create_track(TX((4, 1), (5, 1), name="bb")) | 
|  | g.create_track(TX((1, 1), (5, 1), name="cc")) | 
|  |  | 
|  | g.check() | 
|  | return g | 
|  |  | 
|  |  | 
|  | def test_x_auto(): | 
|  | """ | 
|  | >>> docprint(test_x_auto()) | 
|  | ['| 0  | 1  | 2  | 3  | 4  | 5  |', | 
|  | '       AA>---------------->AA ', | 
|  | '       BB>------>BB           ', | 
|  | '       DD   CC>----------->CC ', | 
|  | '', | 
|  | '', | 
|  | '| 0  | 1  | 2  | 3  | 4  | 5  |', | 
|  | '       aa>------>aa   bb>->bb ', | 
|  | '       cc>---------------->cc ', | 
|  | '', | 
|  | '', | 
|  | '| 0  | 1  | 2  | 3  | 4  | 5  |', | 
|  | '', | 
|  | ''] | 
|  | """ | 
|  | g = create_test_channel_grid() | 
|  | return g.pretty_print() | 
|  |  | 
|  |  | 
|  | def test_pad(): | 
|  | """ | 
|  | >>> docprint(test_pad()) | 
|  | ['| 0  | 1  | 2  | 3  | 4  | 5  |', | 
|  | '       AA>---------------->AA ', | 
|  | '       BB>------>BB   XX   XX ', | 
|  | '       DD   CC>----------->CC ', | 
|  | '', | 
|  | '', | 
|  | '| 0  | 1  | 2  | 3  | 4  | 5  |', | 
|  | '       aa>------>aa   bb>->bb ', | 
|  | '       cc>---------------->cc ', | 
|  | '', | 
|  | '', | 
|  | '| 0  | 1  | 2  | 3  | 4  | 5  |', | 
|  | '', | 
|  | ''] | 
|  | """ | 
|  | g = create_test_channel_grid() | 
|  | g.fill_empty(0, name='XX') | 
|  | g.check() | 
|  | return g.pretty_print() | 
|  |  | 
|  |  | 
|  | def test_x_manual(): | 
|  | """ | 
|  | >>> pprint.pprint(test_x_manual().splitlines()) | 
|  | ['| 0  | 1  | 2  | 3  | 4  | 5  |', | 
|  | '       AA>---------------->AA ', | 
|  | '       BB>------>BB           ', | 
|  | '       DD   CC>----------->CC ', | 
|  | '', | 
|  | '', | 
|  | '| 0  | 1  | 2  | 3  | 4  | 5  |', | 
|  | '       aa>------>aa   bb>->bb ', | 
|  | '       cc>---------------->cc ', | 
|  | '', | 
|  | '', | 
|  | '| 0  | 1  | 2  | 3  | 4  | 5  |', | 
|  | '', | 
|  | ''] | 
|  | """ | 
|  | g = ChannelGrid((6, 3), Track.Type.X) | 
|  | g.create_track(TX((1, 0), (5, 0), name="AA"), idx=0) | 
|  | g.create_track(TX((1, 0), (3, 0), name="BB"), idx=1) | 
|  | g.create_track(TX((2, 0), (5, 0), name="CC"), idx=2) | 
|  | g.create_track(TX((1, 0), (1, 0), name="DD"), idx=2) | 
|  |  | 
|  | g.create_track(TX((1, 1), (3, 1), name="aa"), idx=0) | 
|  | g.create_track(TX((4, 1), (5, 1), name="bb"), idx=0) | 
|  | g.create_track(TX((1, 1), (5, 1), name="cc"), idx=1) | 
|  |  | 
|  | try: | 
|  | g.create_track(TX((1, 1), (5, 1), name="dd"), idx=1) | 
|  | assert False, "Should have failed to place" | 
|  | except IndexError: | 
|  | pass | 
|  |  | 
|  | g.check() | 
|  | return g.pretty_print() | 
|  |  | 
|  |  | 
|  | def test_y_auto(): | 
|  | """ | 
|  | >>> docprint(test_y_auto()) | 
|  | ['|*| 0  |                 |*| 0  |            |*| 0  |  ', | 
|  | '|*| 1  | AA   BB   DD    |*| 1  | aa   cc    |*| 1  |  ', | 
|  | '|*| 2  | ||   ||   CC    |*| 2  | ||   ||    |*| 2  |  ', | 
|  | '|*| 3  | ||   BB   ||    |*| 3  | aa   ||    |*| 3  |  ', | 
|  | '|*| 4  | ||        ||    |*| 4  | bb   ||    |*| 4  |  ', | 
|  | '|*| 5  | AA        CC    |*| 5  | bb   cc    |*| 5  |  '] | 
|  | """ | 
|  | g = ChannelGrid((3, 6), Track.Type.Y) | 
|  | g.create_track(TY((0, 1), (0, 5), name="AA")) | 
|  | g.create_track(TY((0, 1), (0, 3), name="BB")) | 
|  | g.create_track(TY((0, 2), (0, 5), name="CC")) | 
|  | g.create_track(TY((0, 1), (0, 1), name="DD")) | 
|  |  | 
|  | g.create_track(TY((1, 1), (1, 3), name="aa")) | 
|  | g.create_track(TY((1, 4), (1, 5), name="bb")) | 
|  | g.create_track(TY((1, 1), (1, 5), name="cc")) | 
|  |  | 
|  | g.check() | 
|  | return g.pretty_print() | 
|  |  | 
|  |  | 
|  | if __name__ == "__main__": | 
|  | import doctest | 
|  | print('doctest: begin') | 
|  | failure_count, test_count = doctest.testmod() | 
|  | assert test_count > 0 | 
|  | assert failure_count == 0, "Doctests failed!" | 
|  | print('doctest: end') |