| #!/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') |