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.
Move all the code (if any) from
_generate_elements
,_generate_instances
and_generate_ports
into a newgenerate(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.Instead of adding the elements / instances / ports to
elems
/insts
/ports
, add them directly tolayout
using+=
.Clean up your code, by moving all local variables to the top of the function, and check whether there’s any deduplication possible.
Remove any references to
self.elements
,self.instances
, andself.ports
. Replace them with the object they refer to. That object could be stored in a variable. Named instances or ports that were added tolayout
can be retrieved usinglayout["name"]
.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 noti3.LayoutView
) defining any of the legacy methods.Having a
Layout
class that inherits (through multiple inheritance) from one class usinggenerate(self, layout)
and another class using the legacy methods.