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
potential starting point