qnngroup / qnngds

MIT License
5 stars 3 forks source link

set up smarter routing #109

Open reed-foster opened 3 weeks ago

reed-foster commented 3 weeks ago

potential starting point

def autoroute(exp_ports, pad_ports, workspace_size, exp_bbox, width, spacing, pad_offset, layer):
    """
    automatically routes an experiment to a set of pads

    Two step process. First step partially routes any experiment ports which are orthogonal to their
    designated pad array port so that they are facing each other.  Then, sorts pairs of ports based
    on the minimum horizontal (or vertical distance) between ports facing each other. Finally, 
    routes the sorted pairs.
    exp_ports       - list of ports in experiment geometry
    pad_ports       - list of ports to connect to pad array
    workspace_size  - side length of workspace area
    exp_bbox        - bounding box of experiment
    width           - width of traces (in microns)
    spacing         - min spacing between traces (in microns)
    pad_offset      - extra spacing from pads
    layer           - gds layer
    """

    D = Device('autorouted_traces')

    if len(exp_ports) != len(pad_ports):
        raise ValueError("invalid port lists for autorouter, lengths must match")
    num_ports = len(exp_ports)

    # find the pad with the minimum distance to experiment port 0
    # we'll connect these two and connect pairs of ports sequentially around the pad array
    # so there is no overlap
    min_dist, min_pad = max(workspace_size), -1
    for i in range(num_ports):
        norm = np.linalg.norm(exp_ports[0].center - pad_ports[i].center)
        if norm < min_dist:
            min_dist, min_pad = norm, i

    # group the pairs or ports based on whether or not they are orthogonal and which direction the
    # experiment port is facing
    pairs = [(exp_ports[i], pad_ports[(min_pad + i) % num_ports]) for i in range(num_ports)]
    # split pairs into four groups based on face of experiment (N, S, E, W)
    grouped_pairs = [[], [], [], []]
    orthogonal_pairs = [[], [], [], []]
    paths = [[], [], [], []]
    for port_pair in pairs:
        ep_n = port_pair[0].normal[1] - port_pair[0].center
        pp_n = port_pair[1].normal[1] - port_pair[1].center
        if abs(np.dot(ep_n, (1,0))) < 1e-9:
            q = 0 if np.dot(ep_n, (0,1)) > 0 else 1
        else:
            q = 2 if np.dot(ep_n, (1,0)) > 0 else 3
        if abs(np.dot(pp_n, ep_n)) < 1e-9:
            orthogonal_pairs[q].append(port_pair)
        else:
            grouped_pairs[q].append(port_pair)
            paths[q].append(None)

    # first create partial paths for orthogonal pairs and create new port at the end of partial path
    for q, quadrant in enumerate(orthogonal_pairs):
        # keep track of height/separation from experiment bbox on both halves of experiment face
        # halves are based on x/y coordinate of pad_p
        # since orthogonal ports are sorted based on how close they are to the edge of the bbox
        # processing them in sorted order and incrementing height appropriately will prevent collisions
        height = [0, 0]
        for port_pair in sorted(quadrant, key = lambda p: abs(p[0].x - p[1].x) if q < 2 else abs(p[0].y - p[1].y)):
            exp_p = port_pair[0]
            pad_p = port_pair[1]
            start = np.array([exp_p.x, exp_p.y])
            if q < 2:
                # select direction based on x coordinate of pad_p
                direction = 0 if pad_p.x < 0 else 1
                height[direction] += spacing
                start += (0, height[direction] if q == 0 else -height[direction])
                end = (exp_bbox[0][0] if pad_p.x < 0 else exp_bbox[1][0], start[1])
                new_q = 3 if pad_p.x < 0 else 2
            else:
                # select direction based on y coordinate of pad_p
                direction = 0 if pad_p.y < 0 else 1
                height[direction] += spacing
                start += (height[direction] if q == 2 else -height[direction], 0)
                end = (start[0], exp_bbox[0][1] if pad_p.y < 0 else exp_bbox[1][1])
                new_q = 1 if pad_p.y < 0 else 0
            path = Path((exp_p.center, start, end))
            new_port = Port(name=exp_p.name, midpoint=end, width=exp_p.width,
                        orientation=(pad_p.orientation + 180) % 360)
            grouped_pairs[new_q].append((new_port, pad_p))
            paths[new_q].append(path)

    # split each quadrant of pairs into sections which can be routed independently
    sectioned_pairs = [[], [], [], []]
    sectioned_paths = [[], [], [], []]
    for q, quadrant in enumerate(grouped_pairs):
        last_direction = None
        for p, port_pair in sorted(enumerate(quadrant), key = lambda a: a[1][0].x if q < 2 else a[1][0].y):
            # sorted based on exp_port x coord (-x to +x) for top and bottom sides
            # based on exp_port y coord (-y to +y) for left and right sides
            direction = np.sign(port_pair[1].x - port_pair[0].x) if q < 2 else np.sign(port_pair[1].y - port_pair[0].y)
            new_section = False
            if direction != last_direction or last_direction is None:
                new_section = True
            else:
                pad_port = port_pair[1]
                exp_port = port_pair[0]
                prev_pad_port = sectioned_pairs[q][-1][-1][1]
                prev_exp_port = sectioned_pairs[q][-1][-1][0]
                if direction == 1:
                    # moving rightwards/upwards
                    if q < 2 and exp_port.x > prev_pad_port.x + prev_pad_port.width/3:
                        new_section = True
                    elif q >= 2 and exp_port.y > prev_pad_port.y + prev_pad_port.width/3:
                        new_section = True
                    if q < 2 and pad_port.x + prev_pad_port.width/3 < prev_exp_port.x:
                        new_section = True
                    elif q >= 2 and pad_port.y + prev_pad_port.width/3 < prev_exp_port.y:
                        new_section = True
                elif direction == -1:
                    # moving leftwards/downwards
                    if q < 2 and exp_port.x < prev_pad_port.x - prev_pad_port.width/3:
                        new_section = True
                    elif q >= 2 and exp_port.y < prev_pad_port.y - prev_pad_port.width/3:
                        new_section = True
                    if q < 2 and pad_port.x - prev_pad_port.width/3 > prev_exp_port.x:
                        new_section = True
                    elif q >= 2 and pad_port.y - prev_pad_port.width/3 > prev_exp_port.y:
                        new_section = True
                else:
                    new_section = True
            if new_section:
                sectioned_pairs[q].append([])
                sectioned_paths[q].append([])
            sectioned_pairs[q][-1].append(port_pair)
            sectioned_paths[q][-1].append(paths[q][p])
            last_direction = direction

    # now all ports face each other and the automatic routing will be straightforward
    for q, quadrant in enumerate(sectioned_pairs):
        for s, section in enumerate(quadrant):
            pad_dist = pad_offset
            direction = np.sign(section[0][1].x - section[0][0].x) if q < 2 else np.sign(section[0][1].y - section[0][0].y)
            for p, port_pair in sorted(enumerate(section), key = lambda a: direction*(a[1][0].x if q < 2 else a[1][0].y)):
                exp_p = port_pair[0]
                pad_p = port_pair[1]
                if q < 2:
                    # ports are vertically aligned
                    if abs(exp_p.x - pad_p.x) < pad_p.width/3:
                        # ports are close enough to route together with a straight
                        new_path = Path((exp_p.center, (exp_p.x, pad_p.y)))
                        new_port = Port(name=pad_p.name, midpoint=(exp_p.x, pad_p.y),
                                        width=pad_p.width, orientation=(exp_p.orientation + 180) % 360)
                    else:
                        direction = np.sign(port_pair[1].x - port_pair[0].x)
                        start = np.array((exp_p.x, pad_p.y))
                        start += (0, -pad_dist if q == 0 else pad_dist)
                        if direction < 0:
                            # routing leftwards
                            end_x = max(min(pad_p.x, exp_p.x - 5*width), pad_p.x - pad_p.width/3)
                        else:
                            end_x = min(max(pad_p.x, exp_p.x + 5*width), pad_p.x + pad_p.width/3)
                        mid = np.array((end_x, start[1]))
                        end = (end_x, pad_p.y)
                        pad_dist += spacing
                        new_path = Path((exp_p.center, start, mid, end))
                        new_port = Port(name=pad_p.name, midpoint=end,
                                        width=pad_p.width, orientation=(exp_p.orientation + 180) % 360)

                else:
                    # ports are horizontally aligned
                    if abs(exp_p.y - pad_p.y) < pad_p.width/3:
                        # ports are close enough to route together with a straight
                        new_path = Path((exp_p.center, (pad_p.x, exp_p.y)))
                        new_port = Port(name=pad_p.name, midpoint=(pad_p.x, exp_p.y),
                                        width=pad_p.width, orientation=(exp_p.orientation + 180) % 360)
                    else:
                        direction = np.sign(port_pair[1].y - port_pair[0].y)
                        start = np.array((pad_p.x, exp_p.y))
                        start += (-pad_dist if q == 2 else pad_dist, 0)
                        if direction < 0:
                            # routing leftwards
                            end_y = max(min(pad_p.y, exp_p.y - 5*width), pad_p.y - pad_p.width/3)
                        else:
                            end_y = min(max(pad_p.y, exp_p.y + 5*width), pad_p.y + pad_p.width/3)
                        mid = np.array((start[0], end_y))
                        end = (pad_p.x, end_y)
                        pad_dist += spacing
                        new_path = Path((exp_p.center, start, mid, end))
                        new_port = Port(name=pad_p.name, midpoint=end,
                                        width=pad_p.width, orientation=(exp_p.orientation + 180) % 360)
                if sectioned_paths[q][s][p] is not None:
                    sectioned_paths[q][s][p].append(new_path)
                else:
                    sectioned_paths[q][s][p] = new_path
                sectioned_pairs[q][s][p] = (exp_p, new_port)
    # perform routing along path and add final ports to connect to pad array
    for q, quadrant in enumerate(sectioned_pairs):
        for s, section in enumerate(quadrant):
            for p, port_pair in enumerate(section):
                try:
                    route = D << pr.route_smooth(port1=port_pair[0], port2=port_pair[1], radius=1.5*width, width=width,
                                                 path_type='manual', manual_path=sectioned_paths[q][s][p], layer=layer)
                except ValueError as e:
                    traceback.print_exc()
                    print('An error occurred with phidl.routing.route_smooth(), try increasing the size of the workspace')
                    print(sectioned_paths[q][s][p].points)
                    raise ValueError(e)
                # add taper
                pad_p = pad_ports[port_pair[1].name]
                final_w = port_pair[1].width + 2*(abs(route.ports[2].x - pad_p.x) + abs(route.ports[2].y - pad_p.y))
                ht = Device("chopped_hyper_taper")
                taper = ht << qg.hyper_taper(length=2*width, wide_section=final_w + width, narrow_section=width, layer=layer)
                cut = ht << pg.straight(size=(pad_p.width + width, 2*width))
                conn = ht << pg.connector(width=pad_p.width)
                taper.connect(taper.ports[1], route.ports[2])
                conn.connect(conn.ports[1], pad_p)
                cut.connect(cut.ports[1], conn.ports[1])
                D << pg.boolean(A=taper, B=cut, operation='and', precision=1e-6, layer=layer)
    D = pg.union(D)
    # create ports in final device so pg.outline doesn't block them
    for n, port in enumerate(exp_ports):
        D.add_port(name=n, midpoint=port.midpoint, width=port.width,
                   orientation=(port.orientation + 180) % 360)
    for n, port in enumerate(pad_ports):
        o = port.orientation
        dx = -2*width if o == 0 else 2*width if o == 180 else 0
        dy = -2*width if o == 90 else 2*width if o == 270 else 0
        D.add_port(name=num_ports + n, midpoint=(port.x + dx, port.y + dy), width=port.width + width,
                   orientation=(port.orientation + 180) % 360)
    return D