Placement and routing specifications

This sample demonstrates a use case for the placement and routing specifications:

We use those to showcase how certain constraints can be easily met, such as ensuring minimal spacing of a component relative to a neighboring waveguide, ensuring waveguides (don’t) pass through a given part of the layout, doing multilayer electrical routing. For more details on placement and routing, or on particular specification, see Placement and Routing Reference. You will also find there more information about the specs not covered in this example, such as i3.Connect(Manhattan/Electrical)Bundle.

Placement specs are resolved using a constraint solver. This has two advantages: First, in a fully constrained solution, the order of the specs does not matter. Secondly, by using relative constraints we get a very flexible and powerful tool for placing our instances. We can define certain constraints that will be met regardless of the parameters of our PCells. Therefore, building a layout in IPKISS, it is often easier to provide a relative constraint to a position, rather than the absolute coordinate. Sometimes you only know a constraint (e.g. instance A must be vertically aligned with instance B) and you want the tool to resolve these dependencies for you.

Suppose we have two grating couplers at specific fixed positions with a waveguide connecting them. We would like to construct an MZI underneath it (in the blue area), but it is required by the foundry that waveguides have a minimum spacing of 25 micron between them. The ports of the MZI should also be aligned with the output ports of the grating couplers (see dashed lines).

import si_fab.all as pdk
import ipkiss3.all as i3
import matplotlib.pyplot as plt


min_spacing = 25

gc = pdk.FC_TE_1550()
insts = {
    "gc1": gc,
    "gc2": gc,
}
X0, Y0, X1, Y1 = 0, 100, 100, 0
dx, dy = 10, 10
specs_basic = [
    i3.Place("gc1:out", (X0, Y0)),
    i3.Place("gc2:out", (X1, Y1), angle=90),
    i3.ConnectManhattan("gc1:out", "gc2:out"),
]

circuit = i3.Circuit(insts=insts, specs=specs_basic)
fig = plt.figure()
circuit.Layout().visualize(figure=fig, show=False)

# plot annotations in the figure to mark the relevant constraints
ax = fig.gca()
ax.add_patch(plt.Rectangle((-10, -10), 100, 100, alpha=0.2))
ax.plot((0, 0), (110, -10), (110, -10), (0, 0), color="black", linestyle="--", linewidth=1.0)
plt.show()
plot place route specs

Given this layout, we want to place the Y-branches so that their inputs are aligned with the grating couplers in one axis, but that their outputs are at a fixed distance from the waveguide in the other axis. We can achieve this by constraining the X and Y positions separately.

yb = pdk.YBranch(bend_length=8, bend_height=8)
splitter_insts = {"splitter": yb, "combiner": yb}

specs_basic += [
    # instances, ports, bounding box references @N, @S, @E, @W, @C can be used with placement and routing specs
    # ensure minimum spacing
    i3.Place.Y("splitter:out2", -min_spacing, relative_to="gc1@C"),
    i3.Place.X("combiner:out1", -min_spacing, relative_to="gc2:out"),
    # align ports
    i3.AlignV("splitter:in1", "gc1:out"),
    i3.AlignH("combiner@S", "gc2@N"),
    # rotate
    i3.Place.Angle("combiner", 90),
]
# done to preserve the reference to the initial (placement) specs that won't change in this sample
specs = specs_basic + [
    # add connectors between the splitters and the combiners
    i3.ConnectManhattan("splitter:out1", "combiner:out2"),
    i3.ConnectManhattan("splitter:out2", "combiner:out1"),
]
circuit = i3.Circuit(insts={**insts, **splitter_insts}, specs=specs)
fig = plt.figure()
circuit.Layout().visualize(figure=fig, show=False)

# plot annotations in the figure to mark the relevant constraints
ax = fig.gca()
ax.plot((0, 0), (110, -10), (110, -10), (0, 0), color="black", linestyle="--", linewidth=1.0)
ax.annotate("", xy=(75, 35), xycoords="data", xytext=(100, 35), textcoords="data", arrowprops={"arrowstyle": "<->"})
ax.annotate("25", xy=(87, 35), xycoords="data", xytext=(-4, 5), textcoords="offset points")
ax.annotate("", xy=(35, 75), xycoords="data", xytext=(35, 100), textcoords="data", arrowprops={"arrowstyle": "<->"})
ax.annotate("25", xy=(35, 87), xycoords="data", xytext=(5, -4), textcoords="offset points")
plt.show()
plot place route specs

Because the specs have been defined using relative constraints the conditions will always hold regardless of the parameters of our Y-branches.

yb_narrow = pdk.YBranch(bend_length=15, bend_height=2)
splitter_insts2 = {
    "splitter": yb_narrow,
    "combiner": yb_narrow,
}

circuit = i3.Circuit(insts={**insts, **splitter_insts2}, specs=specs)
fig = plt.figure()
circuit.Layout().visualize(figure=fig, show=False)

# plot annotations in the figure to mark the relevant constraints
ax = fig.gca()
ax.plot((0, 0), (110, -10), (110, -10), (0, 0), color="black", linestyle="--", linewidth=1.0)
ax.annotate("", xy=(75, 35), xycoords="data", xytext=(100, 35), textcoords="data", arrowprops={"arrowstyle": "<->"})
ax.annotate("25", xy=(87, 35), xycoords="data", xytext=(-4, 5), textcoords="offset points")
ax.annotate("", xy=(35, 75), xycoords="data", xytext=(35, 100), textcoords="data", arrowprops={"arrowstyle": "<->"})
ax.annotate("25", xy=(35, 87), xycoords="data", xytext=(5, -4), textcoords="offset points")
plt.show()
plot place route specs

Let’s replace one of the Manhattan routes and add two heater waveguides. Furthermore, since this circuit will have a larger footprint let’s also increase the distance between the instances, which can be easily done by changing the initial position, and redefining the specs_basic list. We can join one of the optical ports with either the splitter or the combiner.

heater = pdk.HeatedWaveguide(name="heater")
heater.Layout(shape=[(0.0, 0.0), (100.0, 0.0)])
heater_insts = {"heater1": heater, "heater2": heater}
# increase the spread between the instances
X0, Y0, X1, Y1 = 0, 200, 200, 0
dx, dy = 50, 50
specs_basic = [
    i3.Place("gc1:out", (X0, Y0)),
    i3.Place("gc2:out", (X1, Y1), angle=90),
    i3.ConnectManhattan("gc1:out", "gc2:out"),
    # ensure minimum spacing
    i3.Place.Y("splitter:out2", -min_spacing, relative_to="gc1:out"),
    i3.Place.X("combiner:out1", -min_spacing, relative_to="gc2:out"),
    # align ports
    i3.AlignV("splitter:in1", "gc1:out"),
    i3.AlignH("combiner:in1", "gc2:out"),
    # rotate
    i3.Place.Angle("combiner", 90),
    # join the heaters with splitter/combiner
    i3.Join("heater1:in", "splitter:out2"),
    i3.Join("heater2:out", "combiner:out1"),
]

specs = specs_basic + [
    # add connectors - connecting the heaters instead
    i3.ConnectManhattan("heater1:out", "heater2:in"),
    i3.ConnectManhattan("splitter:out1", "combiner:out2"),
]
# **syntax is used to unpack and combine all the individual dictionaries
circuit = i3.Circuit(insts={**insts, **splitter_insts, **heater_insts}, specs=specs)
circuit.Layout().visualize()
plot place route specs

Imagine the situation where the bottom Manhattan route needs to adhere to certain constraints (e.g. it needs to avoid obstacles). We can do this by providing a list of control point to ConnectManhattan. Those control points can be specified relative to other instances/their ports/bounding box/geometrical center in your circuit, or the starting/ending/previous control points in your route. Note the difference between control points (fixed in both dimensions) and control lines (where a single coordinate is fixed). To specify the control line using i3.H or i3.V, you could use “anchors” that define only a single coordinate:

  • @E and @W specify the x-coordinate/define vertical line,

  • @N and @S specify the y-coordinate/define horizontal line.

To specify the control points using i3.CP, you could use “anchors” that define only a 2D coordinate:

  • @C specifies the geometrical center of the instance bounding box,

  • @NE/@NW/@SE/@SW specify the top-left, top-right, bottom-left, and bottom-right corner of the instance bounding box, respectively.

# define text
class NoGoText(i3.PCell):
    class Layout(i3.LayoutView):
        def _generate_elements(self, elems):
            elems += i3.PolygonText(layer=i3.Layer(0), text="NO-GO ZONE", height=14)
            return elems


# Control the Manhattan route
specs = specs_basic + [
    # add connectors - connecting heaters instead
    # adding control points to change the route between the splitter and the combiner
    i3.ConnectManhattan("heater1:out", "heater2:in"),
    i3.ConnectManhattan(
        "splitter:out1",
        "combiner:out2",
        control_points=[
            i3.V(i3.START + 20),
            i3.H(0, relative_to="heater2:in"),
            i3.V(0, relative_to="heater1@E"),
            i3.H(i3.END + 20),
        ],
    ),
    i3.Place("nogo", (-5, -25), relative_to="heater1@SE"),
]
text_inst = {"nogo": NoGoText()}
circuit = i3.Circuit(insts={**insts, **splitter_insts, **heater_insts, **text_inst}, specs=specs)
fig = plt.figure()
circuit.Layout().visualize(figure=fig, show=False)
# plot annotations in the figure to mark the relevant constraints
ax = fig.gca()
ax.add_patch(
    plt.Polygon(
        (
            (11, 161),
            (11, 171),
            (171, 171),
            (171, 11),
            (161, 11),
            (161, 34),
            (114, 34),
            (114, 114),
            (34, 114),
            (34, 161),
        ),
        alpha=0.2,
    )
)
plt.show()
plot place route specs

As a last step, let’s connect one of the heaters to a bondpad. Despite this circuit being simple, we’ll utilize the multilayer electrical routing to route in even and odd metal layers the vertical and the horizontal routes, respectively. You might want to do this when you want to guarantee an easy escape route in the electrical domain out of your circuit. This can be done by defining a VIA array that we’ll be using when defining i3.VIA(s) as control points in i3.ConnectElectrical.

class VIA_M1_M2_ARRAY(i3.PCell):
    box_size = i3.Size2Property(default=(10, 10))
    via_pitch = i3.Size2Property(default=i3.TECH.BONDPAD.VIA_PITCH, doc="2D pitch (center-to-center) of the vias")
    via = i3.ChildCellProperty(doc="Via used")

    def _default_via(self):
        return pdk.VIA_M1_M2()

    class Layout(i3.LayoutView):
        def _generate_elements(self, elems):
            elems += i3.Rectangle(layer=i3.TECH.PPLAYER.M1, box_size=self.box_size)
            elems += i3.Rectangle(layer=i3.TECH.PPLAYER.M2, box_size=self.box_size)
            return elems

        def _generate_instances(self, insts):
            periods_x = int(self.box_size[0] / self.via_pitch[0]) - 1
            periods_y = int(self.box_size[1] / self.via_pitch[1]) - 1

            insts += i3.ARef(
                reference=self.via,
                origin=(
                    -(periods_x - 1) * self.via_pitch[0] / 2.0,
                    -(periods_y - 1) * self.via_pitch[1] / 2.0,
                ),
                period=self.via_pitch,
                n_o_periods=(periods_x, periods_y),
            )
            return insts


via_array = VIA_M1_M2_ARRAY()
# we'll use M1 layer in the horizontal, and M2 layer in the vertical direction
tt_width = 5
M1_wire_tpl = pdk.M1WireTemplate().Layout(width=tt_width)
M2_wire_tpl = pdk.M2WireTemplate().Layout(width=tt_width)
bp = pdk.BondPad()
bondpad_inst = {"pad": bp}

Bondpad (pad) can be placed absolutely . We use VIAs to move between different metal layers and define the path of the electrical routes. To control where the VIAs are placed, we make use of the relative_to argument (same as in i3.CP/i3.H/i3.V).

What we want to achieve, is to have the minimal length Manhattan-like route, which can be achieve by placing VIAs relative to the bondpad center and the electrical ports of the heaters (e.g. i3.VIA((-1.5, 0), relative_to=(“heater1@E”, “pad@C”))). The small addition compensates for half of the width of the electrical ports. Note that the relative_to argument supports “anchors” referring to two different instances, which we readily use.

Finally, at the point where we place the VIA we also switch the metal trace, which ensures correct traces are used in each of the routing directions.

specs += [
    # adding bondpad
    i3.Place("pad@C", (dx, 1.2 * Y0)),
    # Use multiple vias in Electrical route
    i3.ConnectElectrical(
        "heater1:elec1",
        "pad:m1",
        trace_template=M2_wire_tpl,
        start_angle=90,
        end_angle=180,
        control_points=[
            i3.VIA(
                (1.5, 0),
                relative_to=("heater1@W", "pad@C"),
                direction_in=i3.SOUTH,
                direction_out=i3.EAST,
                trace_template=M1_wire_tpl,
                layout=via_array,
            ),
        ],
    ),
]
circuit = i3.Circuit(insts={**insts, **splitter_insts, **heater_insts, **bondpad_inst, **text_inst}, specs=specs)
circuit.Layout().visualize()
plot place route specs