Porting from IPKISS 2025.03 to IPKISS 2025.06

Porting layout generation code

A full guide on the layout view can be found in The Layout View.

In previous versions of IPKISS, a layout is defined by overriding _generate_elements, _generate_instances, and _generate_ports in the definition of your Layout class. Starting from IPKISS 2025.06, it’s possible to keep all layout generation code together in one method: generate(self, layout). This allows you to write cleaner code. The legacy syntax will continue to work, and there are no plans yet to deprecate it. Porting is not mandatory, but recommended.

This porting guide consists of:

Step by step conversion guide

To convert classes using legacy syntax into one using the new syntax using the generate(self, layout) method, perform the following steps.

  1. Move all the code (if any) from _generate_elements, _generate_instances and _generate_ports into a new generate(self, layout) method. As ports often come from instances, it’s a good idea to move the code that is responsible for the ports to the bottom of the function.

  2. Instead of adding the elements / instances / ports to elems / insts / ports, add them directly to layout using +=.

  3. Clean up your code, by moving all local variables to the top of the function, and check whether there’s any deduplication possible.

  4. Remove any references to self.elements, self.instances, and self.ports. Replace them with the object they refer to. That object could be stored in a variable. Named instances or ports that were added to layout can be retrieved using layout["name"].

  5. Run the code, verify it runs correctly, and check the generated layout is the same as before.

Example

Using the legacy syntax

import si_fab.all as pdk
import ipkiss3.all as i3


class MyCell(i3.PCell):
  width = i3.PositiveNumberProperty(default=200.0)
  gc = i3.ChildCellProperty(doc="Grating coupler used.")

  def _default_gc(self):
      return pdk.FC_TE_1550()

  class Layout(i3.LayoutView):
      def _generate_elements(self, elems):
          elems += i3.PolygonText(
              layer=pdk.TECH.PPLAYER.TEXT,
              text="REFERENCE",
              coordinate=(self.width / 2, -pdk.TECH.WG.CLADDING_WIDTH)
          )
          return elems

      def _generate_instances(self, insts):
          gc = self.gc
          instances = {
              "gc1": gc,
              "gc2": gc,
          }
          insts += i3.place_and_route(
              insts=instances,
              specs=[
                  i3.Place("gc1:out", position=(0, 0)),
                  i3.Place("gc2:out", position=(self.width, 0), angle=180),
                  i3.ConnectManhattan("gc1:out", "gc2:out"),
              ],
          )
          return insts

      def _generate_ports(self, ports):
          ports += i3.OpticalPort(position=(self.width / 2, 0.0), name="middle")
          ports += self.instances["gc1"].ports["out"].modified_copy(name="in")
          ports += self.instances["gc2"].ports["out"]
          return ports

Using the new syntax

import si_fab.all as pdk
import ipkiss3.all as i3

class MyCell(i3.PCell):
  width = i3.PositiveNumberProperty(default=200.0)
  gc = i3.ChildCellProperty(doc="Grating coupler used.")

  def _default_gc(self):
      return pdk.FC_TE_1550()

  class Layout(i3.LayoutView):
      def generate(self, layout):
          width = self.width
          gc = self.gc

          layout += i3.PolygonText(
              layer=pdk.TECH.PPLAYER.TEXT,
              text="REFERENCE",
              coordinate=(width / 2, -pdk.TECH.WG.CLADDING_WIDTH)
          )

          layout += i3.SRef(gc, name="gc1")
          layout += i3.SRef(gc, name="gc2")
          layout = i3.place_and_route(
              insts=layout,
              specs=[
                  i3.Place("gc1:out", position=(0, 0)),
                  i3.Place("gc2:out", position=(width, 0), angle=180),
                  i3.ConnectManhattan("gc1:out", "gc2:out"),
              ],
          )

          layout += i3.OpticalPort(position=(width / 2, 0.0), name="middle")
          layout += layout["gc1"].ports["out"].modified_copy(name="in")
          layout += layout["gc2"].ports["out"]

          return layout

Caveats

i3.place_and_route

i3.place_and_route has been updated such that its first insts argument can either take an InstanceDict or a dict as before, or the layout object.

If the input insts argument is an InstanceDict or dict, the function returns an InstanceDict containing the placed instances and any created waveguides. The resulting InstanceDict should then be added to the layout:

layout += i3.place_and_route(insts, specs)

If the input insts argument is the layout object, then i3.place_and_route returns a new layout object which includes the original ports and elements, along with the newly placed instances and created waveguides. In this case, we want the returned object to replace the existing layout. The references used in specs should already have been added to layout. E.g. in the example two SRefs with names gc1 and gc2 were added to layout, and thus specs can refer to these by their name.

layout = i3.place_and_route(layout, specs)

i3.expose_ports

i3.expose_ports has been updated such that its first insts argument can either take an InstanceDict or a dict as before, or the layout object. The object returned by this function can always be added to layout.

layout += i3.expose_ports(layout, port_name_map)

Using member fields within generate(self, layout)

Do not use self.elements, self.instances, or self.ports inside generate(self, layout). This will show an infinite recursion error as these fields are computed using generate(self, layout).

Instead, use the objects you created within the function. If you’ve already added a named instance or port to the layout, you can refer to it using layout["name"].

Mixing the previous and the new syntax

It’s always possible to add a reference to a PCell that uses the previous syntax in a PCell that uses the previous syntax and vice versa. The code might not work as expected when the new generation method generate(self, layout) and any of the legacy methods (_generate_elements, _generate_instances, _generate_ports) appear within the same class, or within the same class hierarchy. The code will not raise an Exception, but a warning will be shown.

This is the case for the following scenarios:

  • Having both generate(self, layout) and any of the legacy methods defined in the same class.

  • A class defining generate(self, layout) that inherits from a class (that’s not i3.LayoutView) defining any of the legacy methods.

  • Having a Layout class that inherits (through multiple inheritance) from one class using generate(self, layout) and another class using the legacy methods.