| """ Simple and correct version of Channel formation. |
| |
| channel.py is actually broken for some track configuration, and generates |
| an excessive number of dummy tracks to fill empty space (channel.Channel = >2M |
| versus channel2.Channel ~70k). |
| |
| """ |
| |
| |
| class Channel(object): |
| """ Packs tracks into ptc tracks |
| |
| >>> tracks = [ |
| ... (1, 3, 0), |
| ... (1, 1, 1), |
| ... (4, 5, 2), |
| ... (4, 4, 3), |
| ... (0, 10, 4), |
| ... ] |
| >>> channel_model = Channel(tracks) |
| >>> channel_model.pack_tracks() |
| >>> for ptc, tree in enumerate(channel_model.trees): |
| ... print('ptc={}'.format(ptc)) |
| ... for itr in tree: |
| ... x, y, idx = tracks[itr[2]] |
| ... assert idx == itr[2] |
| ... print(' tracks[{}] = ({}, {})'.format(itr[2], x, y)) |
| ptc=0 |
| tracks[4] = (0, 10) |
| ptc=1 |
| tracks[0] = (1, 3) |
| tracks[2] = (4, 5) |
| ptc=2 |
| tracks[1] = (1, 1) |
| tracks[3] = (4, 4) |
| >>> for ptc, min_v, max_v in channel_model.fill_empty(0, 10): |
| ... print('ptc={} ({}, {})'.format(ptc, min_v, max_v)) |
| ptc=1 (0, 0) |
| ptc=1 (6, 10) |
| ptc=2 (0, 0) |
| ptc=2 (2, 3) |
| ptc=2 (5, 10) |
| """ |
| |
| def __init__(self, tracks): |
| """ |
| |
| Attributes |
| ---------- |
| tracks : list of tuples of (min, max, idx) |
| """ |
| self.trees = [] |
| self.tracks = sorted(tracks, key=lambda x: x[1] - x[0]) |
| |
| def _start_track(self, track): |
| self.trees.append([track]) |
| |
| def _add_track_to_tree(self, track, idx=-1): |
| self.trees[idx].append(track) |
| |
| def _verify_trees(self): |
| for tree in self.trees: |
| for a, b in zip(tree, tree[1:]): |
| assert a[1] <= b[0] |
| |
| def pack_tracks(self): |
| """pack all tracks |
| |
| Algorithm: |
| |
| 1. Sort tracks by length, shortest tracks first. Popping from back |
| of python lists is O(1). |
| 2. Create stack for each starting values, inserting in length order. |
| 3. Starting with the lowest starting value, greedly pack tracks |
| Algorithm weaknesses: |
| - Linear scan for lowest starting value |
| - Linear scan for packing |
| |
| Both weaknesses are O(Number grid dim * Number of tracks) in |
| pathological case, however grid dimensions tend to be fairly small, |
| (e.g. 50T is 150), so scans are practically fast. |
| |
| If the grid dimension size grows large, revisit how to find the |
| lowest starting value and next bucket pack. Relevant operation is |
| given coordinate, find next largest non-empty bucket. |
| 3a. Pop largest track from smallest starting value, creating a new |
| channel |
| 3b. Pop largest track starting from end of previous track until no |
| tracks can follow. |
| 3c. Repeat 3 until everything is packed. |
| |
| """ |
| |
| by_low = {} |
| |
| def pop(low): |
| track = by_low[low].pop() |
| |
| if len(by_low[low]) == 0: |
| del by_low[low] |
| |
| return track |
| |
| for low, high, key in self.tracks: |
| if low not in by_low: |
| by_low[low] = [] |
| |
| by_low[low].append((high, key)) |
| |
| if len(by_low) > 0: |
| high = max(by_low) |
| |
| while len(by_low) > 0: |
| track_low = min(by_low) |
| track_high, key = pop(track_low) |
| |
| self._start_track((track_low, track_high, key)) |
| |
| while track_high is not None: |
| start = track_high + 1 |
| track_high = None |
| for track_low in range(start, high + 1): |
| if track_low in by_low: |
| track_high, key = pop(track_low) |
| self._add_track_to_tree((track_low, track_high, key)) |
| break |
| |
| self._verify_trees() |
| |
| def fill_empty(self, min_value, max_value): |
| """Generator that yields tracks for any gaps in the channels. |
| """ |
| for idx, tree in enumerate(self.trees): |
| tracks = sorted(tree, key=lambda x: x[0]) |
| |
| if min_value <= tracks[0][0] - 1: |
| yield (idx, min_value, tracks[0][0] - 1) |
| |
| for cur_track, next_track in zip(tracks, tracks[1:]): |
| if cur_track[1] + 1 <= next_track[0] - 1: |
| yield (idx, cur_track[1] + 1, next_track[0] - 1) |
| |
| if tracks[-1][1] + 1 <= max_value: |
| yield (idx, tracks[-1][1] + 1, max_value) |