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
toi3.Circuit
.Rename
child_cells
toinsts
(has been renamed for consistency with IPKISS).Bring the
connectors
andplace_specs
together in 1 argumentspecs
.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 defaulti3.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 thetrace_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
.