Example #1
0
def labelITerm(iterm, pin_name, iotype, all_shapes_flag=False):
    net_name = pin_name
    net = chip_block.findNet(net_name)
    if net is None:
        net = odb.dbNet_create(chip_block, net_name)

    pin_bterm = chip_block.findBTerm(pin_name)
    if pin_bterm is None:
        pin_bterm = odb.dbBTerm_create(net, pin_name)

    assert pin_bterm is not None, "Failed to create or find " + pin_name

    pin_bterm.setIoType(iotype)

    pin_bpin = odb.dbBPin_create(pin_bterm)
    pin_bpin.setPlacementStatus("PLACED")

    if not all_shapes_flag:
        boxes = getBiggestBox(pad_iterm)
    else:
        boxes = getAllBoxes(pad_iterm)

    for box in boxes:
        layer, ll, ur = box
        odb.dbBox_create(pin_bpin, layer, ll.getX(), ll.getY(), ur.getX(),
                         ur.getY())

    pad_iterm.connect(net)
    pin_bterm.connect(net)
Example #2
0
    return box


def getTechMaxSpacing(layers=tech.getLayers()):
    max_spacing = -1
    for layer in layers:
        max_spacing = max(max_spacing, layer.getSpacing())
    # print("Max spacing for", layers, "is", max_spacing)
    return max_spacing


print("Top-level design name:", top_design_name)

# create special nets
for special_net_name in SPECIAL_NETS:
    net = odb.dbNet_create(block_top, special_net_name)
    net.setSpecial()
    net.setWildConnected()

    wire = odb.dbSWire_create(net, "ROUTED")

    SPECIAL_NETS[special_net_name]["net"] = net
    SPECIAL_NETS[special_net_name]["wire"] = wire

ALL_BOXES = []

# disconnect all iterms from anywhere else !!! (is this even needed?)
for inst in block_top.getInsts():
    iterms = inst.getITerms()
    ALL_BOXES += getInstObs(inst)
    for iterm in iterms:
Example #3
0
def power_route(
    output,
    input_lef,
    core_vdd_pin,
    core_gnd_pin,
    vdd_pad_pin_map,
    gnd_pad_pin_map,
    input_def,
):
    """
    Takes a pre-routed top-level layout, produces a DEF file with VDD and GND special nets\
    where the power pads are connected to the core ring
    """

    # TODO: expose through arguments
    ORIENT_LOC_MAP = {"R0": "N", "R90": "W", "R180": "S", "R270": "E"}

    # TODO: allow control
    VDD_NET_NAME = "VDD"
    GND_NET_NAME = "GND"

    # SKY130 DEFAULT
    SPECIAL_NETS = {
        VDD_NET_NAME: {
            "core_pin": core_vdd_pin,
            "covered": False,
            "map": []
        },
        GND_NET_NAME: {
            "core_pin": core_gnd_pin,
            "covered": False,
            "map": []
        },
    }

    if vdd_pad_pin_map is None:
        vdd_pad_pin_map = [{"pad_pin": "vccd", "pad_name_substr": "vccd"}]
    else:
        vdd_pad_pin_map = [{
            "pad_pin": mapping[0],
            "pad_name_substr": mapping[1]
        } for mapping in vdd_pad_pin_map]

    if gnd_pad_pin_map is None:
        gnd_pad_pin_map = [
            {
                "pad_pin": "vssd",
                "pad_name_substr": "vssd"
            },
            {
                "pad_pin": "vssa",
                "pad_name_substr": "vssa"
            },
            {
                "pad_pin": "vssio",
                "pad_name_substr": "vssio"
            },
        ]
    else:
        gnd_pad_pin_map = [{
            "pad_pin": mapping[0],
            "pad_name_substr": mapping[1]
        } for mapping in gnd_pad_pin_map]

    SPECIAL_NETS[VDD_NET_NAME]["map"] = vdd_pad_pin_map
    SPECIAL_NETS[GND_NET_NAME]["map"] = gnd_pad_pin_map

    ##################

    reader = OdbReader(input_lef, input_def)

    via_rules = reader.tech.getViaGenerateRules()
    print("Found", len(via_rules), "VIA GENERATE rules")

    # build dictionary of basic custom vias (ROW = COL = 1)
    custom_vias = {}
    for rule in via_rules:
        lower_rules = rule.getViaLayerRule(0)
        upper_rules = rule.getViaLayerRule(1)
        cut_rules = rule.getViaLayerRule(2)

        lower_layer = lower_rules.getLayer()
        upper_layer = upper_rules.getLayer()
        cut_layer = cut_rules.getLayer()

        if lower_layer.getName() in custom_vias:
            print("Skipping", rule.getName())
            continue

        via_params = odb.dbViaParams()

        via_params.setBottomLayer(lower_layer)
        via_params.setTopLayer(upper_layer)
        via_params.setCutLayer(cut_layer)

        cut_rect = cut_rules.getRect()
        via_params.setXCutSize(cut_rect.dx())
        via_params.setYCutSize(cut_rect.dy())

        cut_spacing = cut_rules.getSpacing()
        via_params.setXCutSpacing(cut_spacing[0] - cut_rect.dx())
        via_params.setYCutSpacing(cut_spacing[1] - cut_rect.dy())

        lower_enclosure = lower_rules.getEnclosure()
        upper_enclosure = upper_rules.getEnclosure()
        if "M1M2" in rule.getName():
            print(lower_enclosure)
        via_params.setXBottomEnclosure(lower_enclosure[0])
        via_params.setYBottomEnclosure(lower_enclosure[1])
        via_params.setXTopEnclosure(upper_enclosure[0])
        via_params.setYTopEnclosure(upper_enclosure[1])

        custom_vias[lower_layer.getName()] = {}
        custom_vias[lower_layer.getName()][upper_layer.getName()] = {
            "rule": rule,
            "params": via_params,
        }
        print("Added", rule.getName())

    def create_custom_via(layer1, layer2, width, height, reorient="R0"):
        assert width > 0 and height > 0

        if layer1.getRoutingLevel() < layer2.getRoutingLevel():
            lower_layer_name = layer1.getName()
            upper_layer_name = layer2.getName()
        elif layer1.getRoutingLevel() > layer2.getRoutingLevel():
            lower_layer_name = layer2.getName()
            upper_layer_name = layer1.getName()
        else:
            raise Exception(
                "Cannot create a custom via between two identical layers")

        if reorient in ["R90", "R270"]:
            width, height = height, width

        via_name = "via_%s_%s_%dx%d" % (
            lower_layer_name,
            upper_layer_name,
            width,
            height,
        )

        custom_via = reader.block.findVia(via_name)
        if not custom_via:
            print("Creating", via_name)

            via_params = custom_vias[lower_layer_name][upper_layer_name][
                "params"]
            via_rule = custom_vias[lower_layer_name][upper_layer_name]["rule"]

            cut_width = via_params.getXCutSize()
            cut_height = via_params.getYCutSize()
            cut_spacing_x = via_params.getXCutSpacing()
            cut_spacing_y = via_params.getXCutSpacing()
            lower_enclosure_x = via_params.getXBottomEnclosure()
            lower_enclosure_y = via_params.getYBottomEnclosure()
            upper_enclosure_x = via_params.getXTopEnclosure()
            upper_enclosure_y = via_params.getYTopEnclosure()

            custom_via = odb.dbVia_create(reader.block, via_name)
            custom_via.setViaGenerateRule(via_rule)

            array_width = width - 2 * max(lower_enclosure_x, upper_enclosure_x)
            array_height = height - 2 * max(lower_enclosure_y,
                                            upper_enclosure_y)

            # set ROWCOL
            rows = 1 + (array_height - cut_height) // (cut_spacing_y +
                                                       cut_height)
            cols = 1 + (array_width - cut_width) // (cut_spacing_x + cut_width)
            via_params.setNumCutRows(rows)
            via_params.setNumCutCols(cols)

            custom_via.setViaParams(via_params)

        return custom_via

    def boxes2Rects(boxes, transform):
        rects = []
        for box in boxes:
            ur = odb.Point(box.xMin(), box.yMin())
            ll = odb.Point(box.xMax(), box.yMax())
            transform.apply(ll)
            transform.apply(ur)
            pin_layer = box.getTechLayer()

            rects.append({"layer": pin_layer, "rect": odb.Rect(ll, ur)})
        return rects

    def getInstObs(inst):
        master = inst.getMaster()
        master_ox, master_oy = master.getOrigin()
        inst_ox, inst_oy = inst.getOrigin()
        px, py = master_ox + inst_ox, master_oy + inst_oy
        orient = inst.getOrient()
        transform = odb.dbTransform(orient, odb.Point(px, py))

        obstructions = master.getObstructions()
        rects = boxes2Rects(obstructions, transform)
        for rect in rects:
            rect["type"] = "obstruction"
        return rects

    def getITermBoxes(iterm):
        iterm_boxes = []
        inst = iterm.getInst()
        mterm = iterm.getMTerm()
        master_ox, master_oy = inst.getMaster().getOrigin()
        inst_ox, inst_oy = inst.getOrigin()
        px, py = master_ox + inst_ox, master_oy + inst_oy
        orient = inst.getOrient()
        transform = odb.dbTransform(orient, odb.Point(px, py))
        mpins = mterm.getMPins()
        if len(mpins) > 1:
            print(
                "Warning:",
                len(mpins),
                "mpins for iterm",
                inst.getName(),
                mterm.getName(),
            )
        for i in range(len(mpins)):
            mpin = mpins[i]
            boxes = mpin.getGeometry()
            iterm_boxes += boxes2Rects(boxes, transform)

        # filter duplications
        # TODO: OpenDB bug? duplicate mpins for some reason
        iterm_boxes_set = set()
        iterm_boxes_uniq = []
        for box in iterm_boxes:
            rect = box["rect"]
            llx, lly = rect.ll()
            urx, ury = rect.ur()
            set_item = (box["layer"].getName(), llx, lly, urx, ury)
            if set_item not in iterm_boxes_set:
                iterm_boxes_set.add(set_item)
                iterm_boxes_uniq.append(box)

        return iterm_boxes_uniq

    def getBiggestBoxAndIndex(boxes):
        biggest_area = -1
        biggest_box = None
        biggest_i = -1
        for i in range(len(boxes)):
            box = boxes[i]
            rect = box["rect"]
            area = rect.area()
            if area > biggest_area:
                biggest_area = area
                biggest_box = box
                biggest_i = i

        return biggest_box, biggest_i

    def rectOverlaps(rect1, rect2):
        return not (rect1.xMax() <= rect2.xMin()
                    or rect1.xMin() >= rect2.xMax()
                    or rect1.yMax() <= rect2.yMin()
                    or rect1.yMin() >= rect2.yMax())

    def rectMerge(rect1, rect2):
        rect = odb.Rect(
            min(rect1.xMin(), rect2.xMin()),
            min(rect1.yMin(), rect2.yMin()),
            max(rect1.xMax(), rect2.xMax()),
            max(rect1.yMax(), rect2.yMax()),
        )

        return rect

    def getShapesOverlappingBBox(llx,
                                 lly,
                                 urx,
                                 ury,
                                 layers=[],
                                 ext_orient="R0"):
        shapes_overlapping = []
        rect_bbox = odb.Rect(llx, lly, urx, ury)

        for box in ALL_BOXES:
            box_layer = box["layer"]
            ignore_box = len(layers) != 0
            for layer in layers:
                if equalLayers(layer, box_layer):
                    ignore_box = False
                    break
            if not ignore_box:
                rect_box = transformRect(box["rect"], ext_orient)
                if rectOverlaps(rect_box, rect_bbox):
                    shapes_overlapping.append(box)

        return shapes_overlapping

    def rectIntersection(rect1, rect2):
        rect = odb.Rect()
        if rectOverlaps(rect1, rect2):
            rect.set_xlo(max(rect1.xMin(), rect2.xMin()))
            rect.set_ylo(max(rect1.yMin(), rect2.yMin()))
            rect.set_xhi(min(rect1.xMax(), rect2.xMax()))
            rect.set_yhi(min(rect1.yMax(), rect2.yMax()))

        return rect

    def manhattanDistance(x1, y1, x2, y2):
        return odb.Point.manhattanDistance(odb.Point(x1, y1),
                                           odb.Point(x2, y2))

    def center(x1, y1, x2, y2):
        return (x1 + x2) // 2, (y1 + y2) // 2

    def gridify(rect):
        x1, y1 = rect.ll()
        x2, y2 = rect.ur()
        if (x2 - x1) % 2 != 0:
            x1 += 5  # 0.005 microns !
        return odb.Rect(x1, y1, x2, y2)

    def forward(point, orient, distance):
        x, y = point.x(), point.y()
        if orient == "R0":
            point_forward = odb.Point(x, y - distance)
        elif orient == "R90":
            point_forward = odb.Point(x + distance, y)
        elif orient == "R180":
            point_forward = odb.Point(x, y + distance)
        elif orient == "R270":
            point_forward = odb.Point(x - distance, y)
        else:
            print("Unknown orientation")
            sys.exit(1)
        return point_forward

    def transformRect(rect, orient):
        transform = odb.dbTransform(orient)
        rect_ll = odb.Point(*rect.ll())
        rect_ur = odb.Point(*rect.ur())
        transform.apply(rect_ll)
        transform.apply(rect_ur)
        rect = odb.Rect(rect_ll, rect_ur)
        return rect

    def isObstruction(box):
        return box["type"] == "obstruction"

    def isCorePin(box):
        return box["type"] == "core_pin"

    def isStripe(box):
        return isCorePin(box) and box["stripe_flag"]

    def isPadPin(box):
        return box["type"] == "pad_pin"

    def isHighestRoutingLayer(layer):
        return layer.getRoutingLevel() == reader.tech.getRoutingLayerCount()

    def isLowestRoutingLayer(layer):
        return layer.getRoutingLevel() == 1

    def isRoutingLayer(layer):
        return isinstance(layer,
                          odb.dbTechLayer) and layer.getRoutingLevel() != 0

    def getUpperRoutingLayer(layer):
        if not isRoutingLayer(layer):
            raise Exception(
                "Attempting to get upper routing layer of a non-routing layer")

        if isHighestRoutingLayer(layer):
            raise Exception(
                "Attempting to get upper routing layer of the highest routing layer"
            )

        return layer.getUpperLayer().getUpperLayer()

    def getLowerRoutingLayer(layer):
        if not isRoutingLayer(layer):
            raise Exception(
                "Attempting to get lower routing layer of a non-routing layer")

        if isLowestRoutingLayer(layer):
            raise Exception(
                "Attempting to get lower routing layer of the lowest routing layer"
            )

        return layer.getLowerLayer().getLowerLayer()

    def equalLayers(layer1, layer2):
        return layer1.getName() == layer2.getName()

    def layersBetween(layer1, layer2):
        # Inclusive
        layers_between = [layer1]

        if not equalLayers(layer1, layer2):
            if layer1.getRoutingLevel() < layer2.getRoutingLevel():
                layer1 = getUpperRoutingLayer(layer1)
                while not equalLayers(layer1, layer2):
                    layers_between.append(layer1)
                    layer1 = getUpperRoutingLayer(layer1)
            else:
                layer1 = getLowerRoutingLayer(layer1)
                while not equalLayers(layer1, layer2):
                    layers_between.append(layer1)
                    layer1 = getLowerRoutingLayer(layer1)
            layers_between.append(layer2)

        return layers_between

    def createWireBox(rect_width,
                      rect_height,
                      rect_x,
                      rect_y,
                      layer,
                      net,
                      reorient="R0"):
        rect = odb.Rect(0, 0, rect_width, rect_height)
        rect.moveTo(rect_x, rect_y)
        rect = transformRect(rect, reorient)

        box = {"rect": rect, "layer": layer, "net": net}

        return box

    def getTechMaxSpacing(layers=reader.tech.getLayers()):
        max_spacing = -1
        for layer in layers:
            max_spacing = max(max_spacing, layer.getSpacing())
        # print("Max spacing for", layers, "is", max_spacing)
        return max_spacing

    print("Top-level design name:", reader.name)

    # create special nets
    for special_net_name in SPECIAL_NETS:
        net = odb.dbNet_create(reader.block, special_net_name)
        net.setSpecial()
        net.setWildConnected()

        wire = odb.dbSWire_create(net, "ROUTED")

        SPECIAL_NETS[special_net_name]["net"] = net
        SPECIAL_NETS[special_net_name]["wire"] = wire

    ALL_BOXES = []

    # disconnect all iterms from anywhere else !!! (is this even needed?)
    for inst in reader.block.getInsts():
        iterms = inst.getITerms()
        ALL_BOXES += getInstObs(inst)
        for iterm in iterms:
            master_name = inst.getMaster().getName()
            pin_name = iterm.getMTerm().getName()
            matched_special_net_name = None
            for special_net_name in SPECIAL_NETS:
                net = SPECIAL_NETS[special_net_name]["net"]

                for mapping in SPECIAL_NETS[special_net_name]["map"]:
                    core_pin = SPECIAL_NETS[special_net_name]["core_pin"]
                    pad_pin = mapping["pad_pin"]
                    pad_name_substr = mapping["pad_name_substr"]

                    if (pin_name == pad_pin
                            and pad_name_substr in master_name):  # pad pin
                        matched_special_net_name = special_net_name
                        print(inst.getName(), "connected to", net.getName())
                        # iterm.connect(net)
                        # iterm.setSpecial()

                        # get the pin shapes
                        iterm_boxes = getITermBoxes(iterm)
                        pad_orient = iterm.getInst().getOrient()
                        for box in iterm_boxes:
                            box["net"] = special_net_name
                            box["type"] = "pad_pin"
                            box["orient"] = pad_orient
                        ALL_BOXES += iterm_boxes
                    elif pin_name == core_pin:  # macro ring
                        matched_special_net_name = special_net_name
                        print(
                            inst.getName(),
                            "will be connected to",
                            master_name,
                            "/",
                            pin_name,
                        )
                        # iterm.connect(net)
                        # iterm.setSpecial()

                        iterm_boxes = getITermBoxes(iterm)
                        for box in iterm_boxes:
                            box["net"] = special_net_name
                            box["type"] = "core_pin"
                            box["stripe_flag"] = False

                        h_stripes = []
                        v_stripes = []
                        for i in range(4):  # ASSUMPTION: 4 CORE RING-STRIPES
                            biggest_pin_box, biggest_pin_box_i = getBiggestBoxAndIndex(
                                iterm_boxes)
                            if biggest_pin_box is None:
                                continue

                            del iterm_boxes[biggest_pin_box_i]

                            biggest_pin_box["stripe_flag"] = True

                            rect = biggest_pin_box["rect"]
                            if rect.dx() > rect.dy():  # horizontal stripe
                                biggest_pin_box["orient"] = "R90"
                                h_stripes.append(biggest_pin_box)
                            else:
                                biggest_pin_box["orient"] = "R0"
                                v_stripes.append(biggest_pin_box)

                        if len(h_stripes) >= 2:
                            if (h_stripes[0]["rect"].yMin() <
                                    h_stripes[1]["rect"].yMin()):
                                h_stripes[0]["side"] = "S"
                                h_stripes[1]["side"] = "N"
                            else:
                                h_stripes[0]["side"] = "N"
                                h_stripes[1]["side"] = "S"
                        elif len(h_stripes) == 1:
                            print(
                                "Warning: only one horizontal stripe found for",
                                core_pin,
                            )
                            if (reader.block.getBBox().yMax() -
                                    h_stripes[0]["rect"].yMin() >
                                    reader.block.getBBox().yMin() -
                                    h_stripes[0]["rect"].yMin()):
                                h_stripes[0]["side"] = "S"
                            else:
                                h_stripes[0]["side"] = "N"
                        else:
                            print(
                                "Warning: No horizontal stripes in the design!"
                            )

                        if len(v_stripes) >= 2:
                            if (v_stripes[0]["rect"].xMin() <
                                    v_stripes[1]["rect"].xMin()):
                                v_stripes[0]["side"] = "W"
                                v_stripes[1]["side"] = "E"
                            else:
                                v_stripes[0]["side"] = "E"
                                v_stripes[1]["side"] = "W"
                        elif len(v_stripes) == 1:
                            print(
                                "Warning: only one vertical stripe found for",
                                core_pin)
                            if (reader.block.getBBox().xMax() -
                                    v_stripes[0]["rect"].xMin() >
                                    reader.block.getBBox().xMin() -
                                    v_stripes[0]["rect"].xMin()):
                                v_stripes[0]["side"] = "W"
                            else:
                                v_stripes[0]["side"] = "E"
                        else:
                            print(
                                "Warning: No vertical stripes in the design!")

                        ALL_BOXES += iterm_boxes + v_stripes + h_stripes
            if (matched_special_net_name is
                    None):  # other pins are obstructions for our purposes
                new_obstructions = getITermBoxes(iterm)
                for obs in new_obstructions:
                    obs["type"] = "obstruction"
                ALL_BOXES += new_obstructions

    # only types are: obstruction, core_pin, pad_pin
    assert len({box["type"]: None for box in ALL_BOXES}) == 3

    wire_boxes = []

    PAD_PINS = [box for box in ALL_BOXES if isPadPin(box)]
    CORE_STRIPES = [box for box in ALL_BOXES if isStripe(box)]

    # Go over power pad pins, find the nearest ring-stripe,
    # check how to connect to it
    connections_count = 0
    for box in PAD_PINS:
        pad_pin_orient = box["orient"]
        pad_pin_rect = box["rect"]

        ##
        # if connections_count > 29:
        #     pprint(wire_boxes)
        #     break
        ##

        nearest_stripe_box = None
        for stripe in CORE_STRIPES:
            if (box["net"] == stripe["net"]
                    and stripe["side"] == ORIENT_LOC_MAP[pad_pin_orient]):
                nearest_stripe_box = stripe

        if nearest_stripe_box is None:
            print(
                "Pad pin at",
                box["rect"].ll(),
                box["rect"].ur(),
                "doesn't have a facing stripe. Skipping.",
            )
            continue

        stripe_rect = nearest_stripe_box["rect"]

        # TRANSFORM TO R0 ORIENTATION
        pad_pin_orient_inv = "R" + str((360 - int(pad_pin_orient[1:])) % 360)
        assert pad_pin_orient_inv in ORIENT_LOC_MAP

        pad_pin_rect = transformRect(pad_pin_rect, pad_pin_orient_inv)
        stripe_rect = transformRect(stripe_rect, pad_pin_orient_inv)

        projection_x_min, projection_x_max = max(pad_pin_rect.xMin(),
                                                 stripe_rect.xMin()), min(
                                                     stripe_rect.xMax(),
                                                     pad_pin_rect.xMax())

        MIN_WIDTH = 5000
        if projection_x_max - projection_x_min < MIN_WIDTH:
            continue

        # account for obstructions

        from_layer = box["layer"]
        to_layer = nearest_stripe_box["layer"]

        # prefer the highest possible connecting layer (antenna reasons)
        connecting_layer = from_layer
        if not isHighestRoutingLayer(to_layer):
            connecting_layer = getUpperRoutingLayer(to_layer)
        elif not isLowestRoutingLayer(to_layer):
            connecting_layer = getLowerRoutingLayer(to_layer)

        print("Connecting with", connecting_layer.getName(), "to",
              to_layer.getName())

        wire_rect = odb.Rect(projection_x_min, pad_pin_rect.yMin(),
                             projection_x_max, stripe_rect.yMin())

        # adjust width
        MAX_SPACING = getTechMaxSpacing(
            (set(layersBetween(from_layer, connecting_layer)).union(
                layersBetween(connecting_layer, to_layer))))
        # note that the box is move away from the pad to evade interactions with
        # nearby pads
        overlapping_boxes = getShapesOverlappingBBox(
            wire_rect.xMin() - MAX_SPACING,
            wire_rect.yMin(),
            wire_rect.xMax() + MAX_SPACING,
            wire_rect.yMax() - MAX_SPACING,
            ext_orient=pad_pin_orient_inv,
            layers=list(
                set(layersBetween(connecting_layer, to_layer)) -
                set([to_layer])),
        )

        # find the new possible wire_rect width after subtracting the obstructions
        skip = False
        obs_boundaries = []
        for ov_box in overlapping_boxes:
            obs_rect = transformRect(ov_box["rect"], pad_pin_orient_inv)
            # if it is completely contained in an obstruction

            if (obs_rect.xMin() <= wire_rect.xMin() < wire_rect.xMax() <=
                    obs_rect.xMax()):
                skip = True
                break

            if (wire_rect.xMin() - MAX_SPACING <= obs_rect.xMin() <=
                    wire_rect.xMax() + MAX_SPACING):
                obs_boundaries.append(obs_rect.xMin())

            if (wire_rect.xMin() - MAX_SPACING <= obs_rect.xMax() <=
                    wire_rect.xMax() + MAX_SPACING):
                obs_boundaries.append(obs_rect.xMax())

        obs_boundaries.sort()

        if len(obs_boundaries) > 0:
            obs_min_x = obs_boundaries[0]
            obs_max_x = obs_boundaries[-1]

            print("Adjusting", wire_rect.ll(), wire_rect.ur())
            print(obs_max_x - obs_min_x)
            print(obs_min_x, obs_max_x)
            if (obs_min_x - (wire_rect.xMin() - MAX_SPACING) >
                (wire_rect.xMax() + MAX_SPACING) - obs_max_x):
                wire_rect.set_xhi(obs_min_x - MAX_SPACING)
            else:
                wire_rect.set_xlo(obs_max_x + MAX_SPACING)
            print("To", wire_rect.ll(), wire_rect.ur())

        # leave some space for routing
        from_layer_space = 2 * from_layer.getSpacing() + from_layer.getWidth()
        wire_width = wire_rect.dx() - from_layer_space
        wire_x = wire_rect.xMin() + from_layer_space
        wire_y = wire_rect.yMax()

        if wire_width < MIN_WIDTH:
            skip = True

        if skip:
            continue

        basic_rect = rectIntersection(wire_rect, stripe_rect)

        # 1. extend outside by half of the size of the "basic rectangle"
        wire_y -= basic_rect.dy() // 2
        wire_boxes.append(
            createWireBox(
                wire_width,
                basic_rect.dy() // 2,
                wire_x,
                wire_y,
                from_layer,
                box["net"],
                reorient=pad_pin_orient,
            ))

        # 2. vias up till the connecting_layer on the next basic rect
        wire_y -= basic_rect.dy()
        prev_layer = None
        for layer in layersBetween(from_layer, connecting_layer):
            if prev_layer is not None:
                via = create_custom_via(
                    prev_layer,
                    layer,
                    wire_width,
                    basic_rect.dy(),
                    reorient=pad_pin_orient,
                )

                wire_boxes.append(
                    createWireBox(
                        wire_width,
                        basic_rect.dy(),
                        wire_x,
                        wire_y,
                        via,
                        box["net"],
                        reorient=pad_pin_orient,
                    ))

            wire_boxes.append(
                createWireBox(
                    wire_width,
                    basic_rect.dy(),
                    wire_x,
                    wire_y,
                    layer,
                    box["net"],
                    reorient=pad_pin_orient,
                ))
            prev_layer = layer

        # 3. use the connecting layer to connect to the stripe
        wire_height = wire_y - wire_rect.yMin()
        wire_y = wire_rect.yMin()
        wire_boxes.append(
            createWireBox(
                wire_width,
                wire_height,
                wire_x,
                wire_y,
                connecting_layer,
                box["net"],
                reorient=pad_pin_orient,
            ))

        # 4. vias down from the connecting_layer to the to_layer (stripe layer)
        prev_layer = None
        for layer in layersBetween(connecting_layer, to_layer):
            if prev_layer is not None:
                via = create_custom_via(
                    prev_layer,
                    layer,
                    wire_width,
                    basic_rect.dy(),
                    reorient=pad_pin_orient,
                )

                wire_boxes.append(
                    createWireBox(
                        wire_width,
                        basic_rect.dy(),
                        wire_x,
                        wire_y,
                        via,
                        box["net"],
                        reorient=pad_pin_orient,
                    ))
            wire_boxes.append(
                createWireBox(
                    wire_width,
                    basic_rect.dy(),
                    wire_x,
                    wire_y,
                    layer,
                    box["net"],
                    reorient=pad_pin_orient,
                ))
            prev_layer = layer
        connections_count += 1

    # ROUTING
    print("creating", len(wire_boxes), "wires")
    for box in wire_boxes:
        net_name = box["net"]
        print(net_name, ": drawing a special wire on layer",
              box["layer"].getName())
        rect = gridify(box["rect"])
        if isRoutingLayer(box["layer"]):
            odb.dbSBox_create(
                SPECIAL_NETS[net_name]["wire"],
                box["layer"],
                *rect.ll(),
                *rect.ur(),
                "COREWIRE",
                odb.dbSBox.UNDEFINED,
            )
        else:
            odb.dbSBox_create(
                SPECIAL_NETS[net_name]["wire"],
                box["layer"],
                (rect.xMin() + rect.xMax()) // 2,
                (rect.yMin() + rect.yMax()) // 2,
                "COREWIRE",
            )

        SPECIAL_NETS[net_name]["covered"] = True

    uncovered_nets = [
        net_name for net_name in SPECIAL_NETS
        if not SPECIAL_NETS[net_name]["covered"]
    ]
    if len(uncovered_nets) > 0:
        print("No routes created on the following nets:")
        print(uncovered_nets)
        print("Make sure the pads to be connected are facing the core rings")
        sys.exit(1)

    # OUTPUT
    odb.write_def(reader.block, output)

    odb.write_lef(odb.dbLib_getLib(reader.db, 1), f"{output}.lef")