Porting from Ipkiss 3.6 to Ipkiss 3.7

Porting CircuitCell (Luceda Academy) to IPKISS 3.7

Luceda Academy contained CircuitCell, a class to facilitate defining circuits based on other cells and connectors. It also contained a few workarounds, e.g. for exposing electrical ports.

Academy’s CircuitCell has now been deprecated in favor of the new i3.Circuit functionality. The API has been changed from CircuitCell in order to obtain a consistent experience across IPKISS.

The following changes need to be applied to your code:

  • Remove the import from circuit.all import CircuitCell.

  • Rename CircuitCell to i3.Circuit.

  • Rename child_cells to insts (has been renamed for consistency with IPKISS).

  • Bring the connectors and place_specs together in 1 argument specs.

  • Port the connectors to those in ipkiss3 where applicable (see above).

  • Rename external_port_names to exposed_ports.

See examples below.

Porting simple cells

If you simply called CircuitCell to create a basic (non-parametric) circuit as follows:

# 1. We define the child cells of our circuit.
child_cells = {
    "ybranch": splitter,
    "fgc_1": fgc,
    "fgc_2": fgc,
    "fgc_3": fgc
}

# 2. We define the joins (list of tuples), which contain all the ports to be snapped to each other.
joins = [
    ("fgc_1:opt1", "ybranch:opt1"),
    ("ybranch:opt2", "fgc_2:opt1"),
    ("ybranch:opt3", "fgc_3:opt1"),
]

# 3. We define specs, containing all the transformations that apply to each component.
place_specs = [
    i3.Place("ybranch:opt1", (0, 0)),
    i3.FlipH("fgc_2"),
    i3.FlipH("fgc_3")
]

# 4. We define the names of the external ports that we want to access.
external_port_names = {
    "fgc_1:fiber": "in",
    "fgc_2:fiber": "out1",
    "fgc_3:fiber": "out2"
}

# 5. We instantiate the CircuitCell class to create the circuit.
splitter_test = CircuitCell(
    name="splitter_test",
    child_cells=child_cells,
    joins=joins,
    place_specs=place_specs,
    external_port_names=external_port_names
)

Then this becomes:

# 1. We instantiate the Circuit class to create the circuit
splitter_test = i3.Circuit(

    # 2. We define the instances in our circuit.
    insts={
        "ybranch": splitter,
        "fgc_1": fgc,
        "fgc_2": fgc,
        "fgc_3": fgc
    }

    # 3. We define the placement & routing specs
    specs=[
        i3.Place("ybranch:opt1", (0, 0)),
        i3.Join([
            ("fgc_1:opt1", "ybranch:opt1"),
            ("ybranch:opt2", "fgc_2:opt1"),
            ("ybranch:opt3", "fgc_3:opt1")
        ])
    ]

     # 4. We define the names of the ports we want to expose externally
    exposed_ports={
        "fgc_1:fiber": "in",
        "fgc_2:fiber": "out1",
        "fgc_3:fiber": "out2"
    }
)

Porting parametric cells (inheritance)

If you inherited from CircuitCell to define a parametric circuit as follows:

class SplitterTree2Levels(CircuitCell):
    # 1. We define the properties of the PCell.
    splitter = i3.ChildCellProperty()
    spacing_x = i3.PositiveNumberProperty(default=100.0)
    spacing_y = i3.PositiveNumberProperty(default=50.0)

    def _default_splitter(self):
        return pdk.MMI1x2Optimized()

    # 2. We define the child cells of our circuit.
    def _default_child_cells(self):
        return {
            "sp_0_0": self.splitter,
            "sp_1_0": self.splitter,
            "sp_1_1": self.splitter,
        }

    # 3. We define connectors (list of tuples): ports to be connected + algorithm to connect them (here a Bezier s-bend).
    def _default_connectors(self):
        return [
            ("sp_0_0:out1", "sp_1_0:in1", bezier_sbend, {"adiabatic_angle": 1.0}),
            ("sp_0_0:out2", "sp_1_1:in1", bezier_sbend),
        ]

    # 4. We define placement specs
    def _default_place_specs(self):
        return [
            i3.Place("sp_0_0:in1", (0, 0)),
            i3.PlaceRelative("sp_1_0:in1", "sp_0_0:out1", (self.spacing_x, -self.spacing_y / 2)),
            i3.PlaceRelative("sp_1_1:in1", "sp_0_0:out2", (self.spacing_x, self.spacing_y / 2)),
        ]

    # 5. We define the names of the external ports.
    def _default_external_port_names(self):
        return {
            "sp_0_0:in1": "in",
            "sp_1_0:out1": "out1",
            "sp_1_0:out2": "out2",
            "sp_1_1:out1": "out3",
            "sp_1_1:out2": "out4",
        }

Then this becomes now:

class SplitterTree2Levels(i3.Circuit):
    # 1. We define the properties of the PCell.
    splitter = i3.ChildCellProperty()
    spacing_x = i3.PositiveNumberProperty(default=100.0)
    spacing_y = i3.PositiveNumberProperty(default=50.0)

    def _default_splitter(self):
        return pdk.MMI1x2Optimized()

    # 2. We define the instances in our circuit.
    def _default_insts(self):
        return {
            "sp_0_0": self.splitter,
            "sp_1_0": self.splitter,
            "sp_1_1": self.splitter,
        }

    # 3. We define the placement and routing specifications
    def _default_specs(self):
        bezier_rounding_1 = i3.SplineRoundingAlgorithm(adiabatic_angles=(1.0, 1.0))
        bezier_rounding_default = i3.SplineRoundingAlgorithm()
        return [
            i3.Place("sp_0_0:in1", (0, 0)),
            i3.PlaceRelative("sp_1_0:in1", "sp_0_0:out1", (self.spacing_x, -self.spacing_y / 2)),
            i3.PlaceRelative("sp_1_1:in1", "sp_0_0:out2", (self.spacing_x, self.spacing_y / 2)),
            i3.ConnectBend("sp_0_0:out1", "sp_1_0:in1", rounding_algorithm=bezier_rounding1),
            i3.ConnectBend("sp_0_0:out2", "sp_1_1:in1", rounding_algorithm=bezier_rounding_default),
        ]

    # 4. We define the names of the external ports.
    def _default_exposed_ports(self):
        return {
            "sp_0_0:in1": "in",
            "sp_1_0:out1": "out1",
            "sp_1_0:out2": "out2",
            "sp_1_1:out1": "out3",
            "sp_1_1:out2": "out4",
        }

Porting PlaceComponents/PlaceAndAutoRoute

i3.PlaceComponents and PlaceAndAutoRoute have been deprecated in favor of the more versatile i3.place_and_route and its convenience class i3.Circuit

Replacing your circuits that use PlaceComponents is a matter of renaming the parameters and using placement specs instead of child_transformations.

Example porting PlaceComponents:

from technologies import silicon_photonics
from ipkiss3 import all as i3

from picazzo3.filters.ring import RingRect180DropFilter, RingRectNotchFilter
from picazzo3.wg.splitters import WgY90Splitter

from picazzo3.routing.place_route import PlaceComponents

ring1 = RingRectNotchFilter()
ring2 = RingRect180DropFilter()
splitter = WgY90Splitter()


pc = PlaceComponents(child_cells={"spl": splitter,
                                   "com": splitter,
                                   "arm1": ring1,
                                   "arm2": ring2},
                     )

layout = pc.Layout(child_transformations={"arm1": (30, -30),
                                          "arm2": (30, 30),
                                          "com": i3.HMirror(0.0)+i3.Translation((60, 0))}
                   )

layout.visualize(annotate=True)

Becomes the following:

from technologies import silicon_photonics
from ipkiss3 import all as i3

from picazzo3.filters.ring import RingRect180DropFilter, RingRectNotchFilter
from picazzo3.wg.splitters import WgY90Splitter

from picazzo3.routing.place_route import PlaceComponents

ring1 = RingRectNotchFilter()
ring2 = RingRect180DropFilter()
splitter = WgY90Splitter()


circuit = i3.Circuit(
  insts={
    'spl': splitter,
    'com': splitter,
    'arm1': ring1,
    'arm2': ring2,
  },
  specs=[
    i3.Place('arm1', (30, -30)),
    i3.Place('arm2', (30, 30)),
    i3.Place('com', (60, 0)),
    i3.FlipH('com'),
  ]
)

lay = circuit.Layout()
lay.visualize(annotate=True)

Porting PlaceAndAutoRoute goes in a similar way. Attention should be paid to the waveguide template used for the connecting waveguides, since the behavior is slightly different:

  • In PlaceAndAutoRoute, by default i3.TECH.PCELL.WG.DEFAULT defined in the technology (PDK) is used as the waveguide template for the connections.

  • In a connector such as i3.ConnectManhattan, by default the trace_template of the start port is used.

Therefore if code based on PlaceAndAutoRoute relied on default parameters, you may need to explicitly specify the waveguide template. See Connector Reference for full information on using connectors.

Example porting PlaceAndAutoRoute:

from technologies import silicon_photonics
from ipkiss3 import all as i3

from picazzo3.filters.ring import RingRect180DropFilter, RingRectNotchFilter
from picazzo3.wg.splitters import WgY90Splitter

from picazzo3.routing.place_route import PlaceAndAutoRoute

ring1 = RingRectNotchFilter()
ring2 = RingRect180DropFilter()
splitter = WgY90Splitter()


pr = PlaceAndAutoRoute(child_cells={"spl": splitter,
                                    "com": splitter,
                                    "arm1": ring1,
                                    "arm2": ring2},
                       links=[("spl:arm1", "arm1:in"),
                              ("arm1:out", "com:arm1"),
                              ("spl:arm2", "arm2:in1"),
                              ("arm2:out1", "com:arm2")]
                       )

layout = pr.Layout(child_transformations={"arm1": (30, -30),
                                          "arm2": (30, 30),
                                          "com": i3.HMirror(0.0)+i3.Translation((60, 0))},
                   bend_radius=10.0,
                   manhattan=True
                   )

layout.visualize(annotate=True)

Becomes:

from technologies import silicon_photonics
from ipkiss3 import all as i3

from picazzo3.filters.ring import RingRect180DropFilter, RingRectNotchFilter
from picazzo3.wg.splitters import WgY90Splitter

ring1 = RingRectNotchFilter()
ring2 = RingRect180DropFilter()
splitter = WgY90Splitter()


circuit = i3.Circuit(
  insts={
    "spl": splitter,
    "com": splitter,
    "arm1": ring1,
    "arm2": ring2
  },
  specs=[
    i3.Place('arm1', (30, -30)),
    i3.Place('arm2', (30, 30)),
    i3.Place('com', (60, 0)),
    i3.FlipH('com'),
    i3.ConnectManhattan([
      ("spl:arm1", "arm1:in"),
      ("arm1:out", "com:arm1"),
      ("spl:arm2", "arm2:in1"),
      ("arm2:out1", "com:arm2")
    ], bend_radius=10)
  ]
)

lay = circuit.Layout()

lay.visualize(annotate=True)

Note

The highlight_waveguide_crossing parameter of PlaceAndAutoRoute is not available on i3.Circuit. If this feature is crucial to you, reach out to us at support@lucedaphotonics.com.

Porting i3.place_insts

As the newly added i3.place_and_route is a superset of i3.place_insts, the latter has been deprecated. You can just rename i3.place_insts to i3.place_and_route.