diff --git a/compiler/base/hierarchy_layout.py b/compiler/base/hierarchy_layout.py index 48735585..8b744a7f 100644 --- a/compiler/base/hierarchy_layout.py +++ b/compiler/base/hierarchy_layout.py @@ -1368,6 +1368,142 @@ class layout(): return via + def compute_min_area_rect_dims(self, layer, width, height): + """ + Return ``(width, height)`` after the same min-area expansion as + ``add_min_area_rect_center`` (no geometry added). If ``minarea`` for + ``layer`` is zero, returns ``(width, height)`` unchanged. + """ + min_area = drc("minarea_{}".format(layer)) + if min_area == 0: + return width, height + + min_width = drc("minwidth_{}".format(layer)) + + if preferred_directions[layer] == "V": + new_height = ceil(max(min_area / width, min_width)) + new_width = width + else: + new_width = ceil(max(min_area / height, min_width)) + new_height = height + debug.check(min_area <= round_to_grid(new_height * new_width), "Min area violated.") + return new_width, new_height + + def via_stack_metal_extent_after_min_area(self, + from_layer, + to_layer, + directions, + metal_layer, + horizontal_extent, + size=(1, 1)): + """ + Horizontal span (if ``horizontal_extent``) or vertical span of + ``metal_layer`` patches that ``add_via_stack_center`` would produce on + that layer: the hop's contact ``first_layer`` size, plus + ``compute_min_area_rect_dims`` when ``add_via_stack_center`` would call + ``add_min_area_rect_center`` for that hop (intermediate routing metal). + + ``directions`` and ``size`` match ``add_via_stack_center`` / ``add_via_center``. + Returns ``0.0`` if ``metal_layer`` is never the starting layer of a hop + on the path from ``from_layer`` to ``to_layer``. + """ + if from_layer == to_layer: + return 0.0 + + intermediate_layers = self.get_metal_layers(from_layer, to_layer) + best = 0.0 + cur_layer = from_layer + while cur_layer != to_layer: + from_id = tech_layer_indices[cur_layer] + to_id = tech_layer_indices[to_layer] + + if from_id < to_id: + search_id = 0 + next_id = 2 + else: + search_id = 2 + next_id = 0 + + curr_stack = next(filter(lambda stack: stack[search_id] == cur_layer, tech_layer_stacks), None) + if curr_stack is None: + debug.error("via_stack_metal_extent_after_min_area: no stack for {} toward {}".format(cur_layer, to_layer), -1) + + via_mod = factory.create(module_type="contact", + layer_stack=curr_stack, + dimensions=size, + directions=directions, + implant_type=None, + well_type=None) + + if cur_layer == metal_layer: + fw = via_mod.first_layer_width + fh = via_mod.first_layer_height + if cur_layer in intermediate_layers: + nw, nh = self.compute_min_area_rect_dims(cur_layer, fw, fh) + else: + nw, nh = fw, fh + cand = nw if horizontal_extent else nh + best = max(best, cand) + + cur_layer = curr_stack[next_id] + + return best + + def via_stack_metal_layer_extent_parallel_to_rail(self, + from_layer, + to_layer, + directions, + metal_layer, + parallel_along_y, + size=(1, 1)): + """ + Span along the rail axis of ``metal_layer`` metal only (the hop where + that layer is the contact first layer), including ``compute_min_area_rect_dims`` + when ``add_via_stack_center`` would add a min-area patch on that layer. + Does **not** use the full contact cell bbox (which includes via cut layers). + """ + if from_layer == to_layer: + return 0.0 + + intermediate_layers = self.get_metal_layers(from_layer, to_layer) + best = 0.0 + cur_layer = from_layer + while cur_layer != to_layer: + from_id = tech_layer_indices[cur_layer] + to_id = tech_layer_indices[to_layer] + + if from_id < to_id: + search_id = 0 + next_id = 2 + else: + search_id = 2 + next_id = 0 + + curr_stack = next(filter(lambda stack: stack[search_id] == cur_layer, tech_layer_stacks), None) + if curr_stack is None: + debug.error("via_stack_metal_layer_extent_parallel_to_rail: no stack for {} toward {}".format(cur_layer, to_layer), -1) + + via_mod = factory.create(module_type="contact", + layer_stack=curr_stack, + dimensions=size, + directions=directions, + implant_type=None, + well_type=None) + + if cur_layer == metal_layer: + fw = via_mod.first_layer_width + fh = via_mod.first_layer_height + if cur_layer in intermediate_layers: + nw, nh = self.compute_min_area_rect_dims(cur_layer, fw, fh) + else: + nw, nh = fw, fh + span = nh if parallel_along_y else nw + best = max(best, span) + + cur_layer = curr_stack[next_id] + + return best + def add_min_area_rect_center(self, layer, offset, @@ -1381,15 +1517,7 @@ class layout(): if min_area == 0: return - min_width = drc("minwidth_{}".format(layer)) - - if preferred_directions[layer] == "V": - new_height = ceil(max(min_area / width, min_width)) - new_width = width - else: - new_width = ceil(max(min_area / height, min_width)) - new_height = height - debug.check(min_area <= round_to_grid(new_height*new_width), "Min area violated.") + new_width, new_height = self.compute_min_area_rect_dims(layer, width, height) self.add_rect_center(layer=layer, offset=offset, diff --git a/compiler/modules/capped_replica_bitcell_array.py b/compiler/modules/capped_replica_bitcell_array.py index e58b51c3..2b7a073d 100644 --- a/compiler/modules/capped_replica_bitcell_array.py +++ b/compiler/modules/capped_replica_bitcell_array.py @@ -243,9 +243,12 @@ class capped_replica_bitcell_array(bitcell_base_array): self.route_power_ring(self.supply_stack[2], self.supply_stack[0]) + self._strap_routing_endpoints = [] self.route_supplies() self.route_unused_wordlines() + self.debug_print_strap_routing_endpoints("right") + self._bridge_close_strap_taps() self.reset_coordinates() self.add_layout_pins() @@ -270,6 +273,11 @@ class capped_replica_bitcell_array(bitcell_base_array): getattr(self, "{}_pitch".format(h_layer))) self.supply_rail_pitch = max(drc_pitch, tech_pitch) self.add_power_ring(v_layer=v_layer, h_layer=h_layer, top=power_ring_top, bottom=power_ring_bottom, left=power_ring_left, right=power_ring_right) + # Match metal widths used by route_vertical_side_pin / route_horizontal_side_pin for strap bridges. + _vring = contact(layer_stack=self.supply_stack, directions=("H", "H")) + self._strap_width_vertical_rail = _vring.second_layer_width + _hring = contact(layer_stack=self.supply_stack, directions=("V", "V")) + self._strap_width_horizontal_rail = _hring.first_layer_height def get_main_array_top(self): return self.replica_bitcell_array_inst.by() + self.replica_bitcell_array.get_main_array_top() @@ -360,96 +368,55 @@ class capped_replica_bitcell_array(bitcell_base_array): bottom = connect_ring_bottom left = connect_ring_left right = connect_ring_right - + if 'vdd' in top: inst = self.dummy_row_insts[1] if 'vdd' in inst.mod.pins: - array_pins = inst.get_pins('vdd') - print("found pin", inst, inst.mod, array_pins) - for array_pin in array_pins: - supply_pin = self.top_vdd_pin - self.add_path(array_pin.layer, [array_pin.center(), vector(array_pin.center()[0], supply_pin.center()[1])]) - self.add_via_stack_center(from_layer = array_pin.layer, - to_layer = supply_pin.layer, - offset = vector(array_pin.center()[0], supply_pin.center()[1]), - directions=("V", "V")) + for array_pin in inst.get_pins('vdd'): + self.connect_side_pin(array_pin, "top", self.top_vdd_pin.cy(), + strap_pin=self.top_vdd_pin) if 'gnd' in top: inst = self.dummy_row_insts[1] if 'gnd' in inst.mod.pins: - array_pins = inst.get_pins('gnd') - for array_pin in array_pins: - supply_pin = self.top_gnd_pin - self.add_path(array_pin.layer, [array_pin.center(), vector(array_pin.center()[0], supply_pin.center()[1])]) - self.add_via_stack_center(from_layer = array_pin.layer, - to_layer = supply_pin.layer, - offset = vector(array_pin.center()[0], supply_pin.center()[1]), - directions=("V", "V")) + for array_pin in inst.get_pins('gnd'): + self.connect_side_pin(array_pin, "top", self.top_gnd_pin.cy(), + strap_pin=self.top_gnd_pin) if 'vdd' in bottom: inst = self.dummy_row_insts[0] if 'vdd' in inst.mod.pins: - array_pins = inst.get_pins('vdd') - for array_pin in array_pins: - supply_pin = self.bottom_vdd_pin - self.add_path(array_pin.layer, [array_pin.center(), vector(array_pin.center()[0], supply_pin.center()[1])]) - self.add_via_stack_center(from_layer = array_pin.layer, - to_layer = supply_pin.layer, - offset = vector(array_pin.center()[0], supply_pin.center()[1]), - directions=("V", "V")) + for array_pin in inst.get_pins('vdd'): + self.connect_side_pin(array_pin, "bottom", self.bottom_vdd_pin.cy(), + strap_pin=self.bottom_vdd_pin) if 'gnd' in bottom: inst = self.dummy_row_insts[0] if 'gnd' in inst.mod.pins: - array_pins = inst.get_pins('gnd') - for array_pin in array_pins: - supply_pin = self.bottom_gnd_pin - self.add_path(array_pin.layer, [array_pin.center(), vector(array_pin.center()[0], supply_pin.center()[1])]) - self.add_via_stack_center(from_layer = array_pin.layer, - to_layer = supply_pin.layer, - offset = vector(array_pin.center()[0], supply_pin.center()[1]), - directions=("V", "V")) + for array_pin in inst.get_pins('gnd'): + self.connect_side_pin(array_pin, "bottom", self.bottom_gnd_pin.cy(), + strap_pin=self.bottom_gnd_pin) if 'vdd' in left: inst = self.dummy_col_insts[0] if 'vdd' in inst.mod.pins: - array_pins = inst.get_pins('vdd') - for array_pin in array_pins: - supply_pin = self.left_vdd_pin - self.add_path(array_pin.layer, [array_pin.center(), vector(supply_pin.center()[0], array_pin.center()[1])]) - self.add_via_stack_center(from_layer = array_pin.layer, - to_layer = supply_pin.layer, - offset = vector(supply_pin.center()[0], array_pin.center()[1]), - directions=("H", "H")) + for array_pin in inst.get_pins('vdd'): + self.connect_side_pin(array_pin, "left", self.left_vdd_pin.cx(), + strap_pin=self.left_vdd_pin) if 'gnd' in left: inst = self.dummy_col_insts[0] if 'gnd' in inst.mod.pins: - array_pins = inst.get_pins('gnd') - for array_pin in array_pins: - supply_pin = self.left_gnd_pin - self.add_path(array_pin.layer, [array_pin.center(), vector(supply_pin.center()[0], array_pin.center()[1])]) - self.add_via_stack_center(from_layer = array_pin.layer, - to_layer = supply_pin.layer, - offset = vector(supply_pin.center()[0], array_pin.center()[1]), - directions=("H", "H")) + for array_pin in inst.get_pins('gnd'): + self.connect_side_pin(array_pin, "left", self.left_gnd_pin.cx(), + strap_pin=self.left_gnd_pin) if 'vdd' in right: inst = self.dummy_col_insts[1] if 'vdd' in inst.mod.pins: - array_pins = inst.get_pins('vdd') - for array_pin in array_pins: - supply_pin = self.right_vdd_pin - self.add_path(array_pin.layer, [array_pin.center(), vector(supply_pin.center()[0], array_pin.center()[1])]) - self.add_via_stack_center(from_layer = array_pin.layer, - to_layer = supply_pin.layer, - offset = vector(supply_pin.center()[0], array_pin.center()[1]), - directions=("H", "H")) + for array_pin in inst.get_pins('vdd'): + self.connect_side_pin(array_pin, "right", self.right_vdd_pin.cx(), + strap_pin=self.right_vdd_pin) if 'gnd' in right: inst = self.dummy_col_insts[1] if 'gnd' in inst.mod.pins: - array_pins = inst.get_pins('gnd') - for array_pin in array_pins: - supply_pin = self.right_gnd_pin - self.add_path(array_pin.layer, [array_pin.center(), vector(supply_pin.center()[0], array_pin.center()[1])]) - self.add_via_stack_center(from_layer = array_pin.layer, - to_layer = supply_pin.layer, - offset = vector(supply_pin.center()[0], array_pin.center()[1]), - directions=("H", "H")) + for array_pin in inst.get_pins('gnd'): + self.connect_side_pin(array_pin, "right", self.right_gnd_pin.cx(), + strap_pin=self.right_gnd_pin) def route_unused_wordlines(self): """ @@ -459,14 +426,18 @@ class capped_replica_bitcell_array(bitcell_base_array): for inst in self.dummy_row_insts: for wl_name in inst.mod.get_wordline_names(): pin = inst.get_pin(wl_name) - self.connect_side_pin(pin, "left", self.left_gnd_pin.cx()) - self.connect_side_pin(pin, "right", self.right_gnd_pin.cx()) + self.connect_side_pin(pin, "left", self.left_gnd_pin.cx(), + strap_pin=self.left_gnd_pin) + self.connect_side_pin(pin, "right", self.right_gnd_pin.cx(), + strap_pin=self.right_gnd_pin) # Ground the unused replica wordlines for wl_name in self.unused_wordline_names: pin = self.replica_bitcell_array_inst.get_pin(wl_name) - self.connect_side_pin(pin, "left", self.left_gnd_pin.cx()) - self.connect_side_pin(pin, "right", self.right_gnd_pin.cx()) + self.connect_side_pin(pin, "left", self.left_gnd_pin.cx(), + strap_pin=self.left_gnd_pin) + self.connect_side_pin(pin, "right", self.right_gnd_pin.cx(), + strap_pin=self.right_gnd_pin) def route_side_pin(self, name, side, offset_multiple=1): """ @@ -527,49 +498,333 @@ class capped_replica_bitcell_array(bitcell_base_array): return (left_loc, right_loc) - def connect_side_pin(self, pin, side, offset): + def connect_side_pin(self, pin, side, offset, strap_pin=None): """ - Used to connect a pin to the a horizontal or vertical strap - offset gives the location of the strap + Connect a pin to the horizontal or vertical supply strap. + offset is the strap coordinate (y for top/bottom, x for left/right). + strap_pin is the ring segment pin; its layer is used as the via top target + (same as legacy route_supplies to_layer=supply_pin.layer). + + Each tap is recorded in ``_strap_routing_endpoints``; after all supplies and + wordline grounds are routed, ``_bridge_close_strap_taps`` widens strap metal + along each rail group (min–max tap extent) to merge same-net strap shapes. """ if side in ["left", "right"]: - self.connect_vertical_side_pin(pin, offset) + self.connect_vertical_side_pin(pin, offset, strap_pin=strap_pin, side=side) elif side in ["top", "bottom", "bot"]: - self.connect_horizontal_side_pin(pin, offset) + self.connect_horizontal_side_pin(pin, offset, strap_pin=strap_pin, side=side) else: debug.error("Invalid side {}".format(side), -1) - def connect_horizontal_side_pin(self, pin, yoffset): + def connect_horizontal_side_pin(self, pin, yoffset, strap_pin=None, side=None): """ - Used to connect a pin to the top/bottom horizontal straps + Used to connect a pin to the top/bottom horizontal straps. """ cell_loc = pin.center() pin_loc = vector(cell_loc.x, yoffset) + to_layer = strap_pin.layer if strap_pin is not None else self.supply_stack[0] - # Place the pins a track outside of the array self.add_via_stack_center(offset=pin_loc, from_layer=pin.layer, - to_layer=self.supply_stack[0], + to_layer=to_layer, directions=("V", "V")) - - # Add a path to connect to the array self.add_path(pin.layer, [cell_loc, pin_loc]) + if strap_pin is not None and side is not None: + self._strap_routing_endpoints.append({"kind": "horizontal", + "side": side, + "strap": strap_pin, + "from_layer": pin.layer, + "pin_name": getattr(pin, "name", ""), + "center": pin_loc}) - def connect_vertical_side_pin(self, pin, xoffset): + def connect_vertical_side_pin(self, pin, xoffset, strap_pin=None, side=None): """ - Used to connect a pin to the left/right vertical straps + Used to connect a pin to the left/right vertical straps. """ cell_loc = pin.center() pin_loc = vector(xoffset, cell_loc.y) + to_layer = strap_pin.layer if strap_pin is not None else self.supply_stack[2] - # Place the pins a track outside of the array self.add_via_stack_center(offset=pin_loc, from_layer=pin.layer, - to_layer=self.supply_stack[2], + to_layer=to_layer, directions=("H", "H")) - - # Add a path to connect to the array self.add_path(pin.layer, [cell_loc, pin_loc]) + if strap_pin is not None and side is not None: + self._strap_routing_endpoints.append({"kind": "vertical", + "side": side, + "strap": strap_pin, + "from_layer": pin.layer, + "pin_name": getattr(pin, "name", ""), + "center": pin_loc}) + + def _strap_side_key(self, side): + return "bottom" if side == "bot" else side + + def debug_print_strap_routing_endpoints(self, direction): + """ + Debug: print all entries in ``_strap_routing_endpoints`` for one strap + direction, sorted along the rail (y for left/right, x for top/bottom). + + direction: ``'left'``, ``'right'``, ``'top'``, ``'bottom'``, or ``'bot'``. + + Call after ``route_supplies`` / ``route_unused_wordlines`` and before + ``_bridge_close_strap_taps`` (the bridge step clears the list). + """ + want = self._strap_side_key(direction) + if want not in ("left", "right", "top", "bottom"): + print("debug_print_strap_routing_endpoints: invalid direction {!r} (use left, right, top, bottom, bot)".format(direction)) + return + + recs = getattr(self, "_strap_routing_endpoints", None) or [] + filtered = [] + for r in recs: + sk = self._strap_side_key(r["side"]) + if sk != want: + continue + if want in ("left", "right") and r["kind"] != "vertical": + continue + if want in ("top", "bottom") and r["kind"] != "horizontal": + continue + filtered.append(r) + + if want in ("left", "right"): + filtered.sort(key=lambda r: (r["center"].y, r["center"].x, r.get("pin_name", ""))) + sort_axis = "y" + else: + filtered.sort(key=lambda r: (r["center"].x, r["center"].y, r.get("pin_name", ""))) + sort_axis = "x" + + sep = "-" * 88 + print(sep) + print("{} strap_routing_endpoints side={!r} ({} taps, sort by {})".format( + self.name, want, len(filtered), sort_axis)) + print(sep) + hdr = "{:>4} {:>12} {:>12} {:>6} {:>6} {:<20} {}".format( + "idx", "cx", "cy", "strap", "from", "pin", "strap_c") + print(hdr) + print(sep) + for i, r in enumerate(filtered): + c = r["center"] + sp = r["strap"] + print("{:>4} {:12.4f} {:12.4f} {:>6} {:>6} {:<20} ({:.4f},{:.4f})".format( + i, c.x, c.y, sp.layer, r["from_layer"], + (r.get("pin_name") or "-")[:20], + sp.cx(), sp.cy())) + print(sep) + + def _strap_bridge_supply_stack_contact_extent(self, vertical_rail_taps): + """ + Fallback: m3 (``supply_stack[0]``) first-layer span from the same + ``directions`` as ``connect_side_pin``, without walking a pin-specific + via stack. + """ + st = self.supply_stack + if isinstance(st, (list, tuple)) and len(st) >= 3: + if vertical_rail_taps: + return contact(layer_stack=st, directions=("H", "H")).first_layer_width + return contact(layer_stack=st, directions=("V", "V")).first_layer_height + return self.supply_rail_width + + def _strap_bridge_merge_draw_width(self, vertical_rail_taps, from_layers, strap_layer): + """ + Perpendicular merge-segment width matching ``add_via_stack_center`` plus + ``add_min_area_rect_center`` on ``supply_stack[0]`` for each tap + ``from_layer`` up to ``strap_layer``. + """ + st = self.supply_stack + if not isinstance(st, (list, tuple)) or len(st) < 3: + return self.supply_rail_width + metal_layer = st[0] + dirs = ("H", "H") if vertical_rail_taps else ("V", "V") + horizontal_extent = vertical_rail_taps + best = 0.0 + for fl in from_layers: + ext = self.via_stack_metal_extent_after_min_area(fl, strap_layer, dirs, + metal_layer, horizontal_extent) + best = max(best, ext) + if best > 0: + return best + return self._strap_bridge_supply_stack_contact_extent(vertical_rail_taps) + + def _strap_supply_bottom_metal(self): + """Bottom routing metal in ``supply_stack`` (e.g. ``m3`` for ``m3_stack``).""" + st = self.supply_stack + if isinstance(st, (list, tuple)) and len(st) >= 3: + return st[0] + return None + + def _strap_supply_top_metal(self): + """Top routing metal in ``supply_stack`` (e.g. ``m4`` for ``m3_stack``).""" + st = self.supply_stack + if isinstance(st, (list, tuple)) and len(st) >= 3: + return st[2] + return None + + def _strap_bridge_top_metal_via_width(self, vertical_rail_taps): + """ + Perpendicular span of ``supply_stack[2]`` (second metal) from the same + ``contact(layer_stack=supply_stack, directions=...)`` as the side routes: + ``second_layer_width`` with ``("H","H")`` (``route_vertical_side_pin``), + ``second_layer_height`` with ``("V","V")`` (horizontal ring uses m3 width + from first layer; top metal thickness here matches the via's m4 enclosure). + """ + st = self.supply_stack + if not isinstance(st, (list, tuple)) or len(st) < 3: + return 0.0 + if vertical_rail_taps: + return contact(layer_stack=st, directions=("H", "H")).second_layer_width + return contact(layer_stack=st, directions=("V", "V")).second_layer_height + + def _strap_bridge_layer_spacing_thickness(self, rail_layer, strap_width_along_rail): + """ + Return ``(segment_layer, min_center_spacing)`` for merge bars on the + supply-stack **bottom metal** (e.g. ``m3``), not the via cut layer. + + Tap-center spacing uses that metal's minwidth + same-layer spacing. + Drawn merge width is computed separately by ``_strap_bridge_merge_draw_width``. + """ + metal = self._strap_supply_bottom_metal() + if metal is not None: + thick = drc("minwidth_{}".format(metal)) + same = "{}_to_{}".format(metal, metal) + sp = drc(same) if same in drc else 0.0 + min_sep = max(self.supply_rail_pitch, thick + sp) + return metal, min_sep + same = "{}_to_{}".format(rail_layer, rail_layer) + sp = drc(same) if same in drc else 0.0 + min_sep = max(self.supply_rail_pitch, strap_width_along_rail + sp) + return rail_layer, min_sep + + def _bridge_close_strap_taps(self): + """ + After routing strap taps, add merge segments on the supply-stack **bottom + metal** (e.g. ``m3``), not the via cut layer, between consecutive taps that + are closer than DRC-safe spacing on that metal. When taps are on the + supply-stack **top** metal (e.g. ``m4``), the same merge also draws on that + layer using the via's second-metal width (``route_vertical_side_pin`` / + ``contact`` ``second_layer_*``). Each segment runs from the outer **metal** + extent at one tap to the next along the rail (same stack as + ``add_via_stack_center``), not only between tap centers. Falls back to strap + routing metal if no 3-tuple ``supply_stack`` is defined. + """ + if not getattr(self, "_strap_routing_endpoints", None): + return + + w_vert = getattr(self, "_strap_width_vertical_rail", self.supply_rail_width) + w_horiz = getattr(self, "_strap_width_horizontal_rail", self.supply_rail_width) + eps = 1e-9 + + def bridge_vertical_cluster(cx, rail_layer, recs_sorted): + if len(recs_sorted) < 2: + return + from_layers = {r["from_layer"] for r in recs_sorted} + seg_layer, min_sep = self._strap_bridge_layer_spacing_thickness( + rail_layer, w_vert) + draw_w = self._strap_bridge_merge_draw_width(True, from_layers, rail_layer) + dirs = ("H", "H") + metal_lm = self._strap_supply_bottom_metal() + for i in range(len(recs_sorted) - 1): + r0, r1 = recs_sorted[i], recs_sorted[i + 1] + y0, y1 = r0["center"].y, r1["center"].y + gap = y1 - y0 + if gap <= eps: + continue + if gap < min_sep: + if metal_lm is not None: + ext0 = self.via_stack_metal_layer_extent_parallel_to_rail( + r0["from_layer"], rail_layer, dirs, metal_lm, True) + ext1 = self.via_stack_metal_layer_extent_parallel_to_rail( + r1["from_layer"], rail_layer, dirs, metal_lm, True) + else: + ext0 = ext1 = 0.0 + ya = y0 - 0.5 * ext0 + yb = y1 + 0.5 * ext1 + if yb <= ya + eps: + ya, yb = y0, y1 + self.add_segment_center(layer=seg_layer, + start=vector(cx, ya), + end=vector(cx, yb), + width=draw_w) + top_m = self._strap_supply_top_metal() + if top_m is not None and rail_layer == top_m: + w_top = self._strap_bridge_top_metal_via_width(True) + if w_top > 0: + self.add_segment_center(layer=top_m, + start=vector(cx, ya), + end=vector(cx, yb), + width=w_top) + + def bridge_horizontal_cluster(cy, rail_layer, recs_sorted): + if len(recs_sorted) < 2: + return + from_layers = {r["from_layer"] for r in recs_sorted} + seg_layer, min_sep = self._strap_bridge_layer_spacing_thickness( + rail_layer, w_horiz) + draw_w = self._strap_bridge_merge_draw_width(False, from_layers, rail_layer) + dirs = ("V", "V") + metal_lm = self._strap_supply_bottom_metal() + for i in range(len(recs_sorted) - 1): + r0, r1 = recs_sorted[i], recs_sorted[i + 1] + x0, x1 = r0["center"].x, r1["center"].x + gap = x1 - x0 + if gap <= eps: + continue + if gap < min_sep: + if metal_lm is not None: + ext0 = self.via_stack_metal_layer_extent_parallel_to_rail( + r0["from_layer"], rail_layer, dirs, metal_lm, False) + ext1 = self.via_stack_metal_layer_extent_parallel_to_rail( + r1["from_layer"], rail_layer, dirs, metal_lm, False) + else: + ext0 = ext1 = 0.0 + xa = x0 - 0.5 * ext0 + xb = x1 + 0.5 * ext1 + if xb <= xa + eps: + xa, xb = x0, x1 + self.add_segment_center(layer=seg_layer, + start=vector(xa, cy), + end=vector(xb, cy), + width=draw_w) + top_m = self._strap_supply_top_metal() + if top_m is not None and rail_layer == top_m: + w_top = self._strap_bridge_top_metal_via_width(False) + if w_top > 0: + self.add_segment_center(layer=top_m, + start=vector(xa, cy), + end=vector(xb, cy), + width=w_top) + + vertical_groups = {} + horizontal_groups = {} + for rec in self._strap_routing_endpoints: + strap = rec["strap"] + c = rec["center"] + sk = self._strap_side_key(rec["side"]) + if rec["kind"] == "vertical": + key = (sk, round(strap.cx(), 9), strap.layer) + vertical_groups.setdefault(key, []).append(rec) + elif rec["kind"] == "horizontal": + key = (sk, round(strap.cy(), 9), strap.layer) + horizontal_groups.setdefault(key, []).append(rec) + + for key, recs in vertical_groups.items(): + if len(recs) < 2: + continue + recs.sort(key=lambda r: (r["center"].y, r["center"].x)) + cx = recs[0]["center"].x + rail_layer = key[2] + bridge_vertical_cluster(cx, rail_layer, recs) + + for key, recs in horizontal_groups.items(): + if len(recs) < 2: + continue + recs.sort(key=lambda r: (r["center"].x, r["center"].y)) + cy = recs[0]["center"].y + rail_layer = key[2] + bridge_horizontal_cluster(cy, rail_layer, recs) + + self._strap_routing_endpoints = [] def analytical_power(self, corner, load): """Power of Bitcell array and bitline in nW."""