Using Inheritance for component specialization

When building a class of components you can use class inheritance to create specialized verions of the component without duplicating code. This makes it easier to maintain code.

import demolib.all as pdk
from ipkiss3 import all as i3
from demolib.components.waveguides.wire import SiWireWaveguideTemplate

General Class: MMI

Here we define the class MMI, which is a general class. It has properties such as ‘n_inputs’ and ‘n_outputs’ that make it possible to set the number of inputs and the number of outputs of the mmi. The Layout (elements and ports) are written parametrically.

class MMI(i3.PCell):
    """
    MMI with a variable number of inputs and outputs with variable width and length of the taper.
    """
    n_inputs = i3.PositiveIntProperty(default=1, doc="Number of inputs")
    n_outputs = i3.PositiveIntProperty(default=2, doc="Number of outputs")
    trace_template = i3.TraceTemplateProperty(doc="Trace template at the ports")

    def _default_trace_template(self):
        return pdk.SiWireWaveguideTemplate(name=self.name + "tt")

    class Layout(i3.LayoutView):

        length = i3.PositiveNumberProperty(default=25.0, doc="Length of the mmi.")
        width = i3.PositiveNumberProperty(default=5.0, doc="Width of core layer at center of the mmi.")
        transition_length = i3.PositiveNumberProperty(default=5.0, doc="Length of the tapers.")
        wg_spacing_in = i3.NonNegativeNumberProperty(default=2.0,
                                                     doc="Center to center distance between the waveguides at the input.")
        wg_spacing_out = i3.NonNegativeNumberProperty(default=2.0,
                                                      doc="Center to center distance between the waveguides at the output.")
        taper_width = i3.PositiveNumberProperty(default=1.0,
                                                doc="Width of the core of the taper of the access waveguides of the mmi.")

        @i3.cache()
        def _get_input_taper_coords(self):
            # coordinates of the input tapers [(taper1_begin, taper1_end), (taper2_begin, taper2_end), ...]
            base_y = - (self.n_inputs - 1) * self.wg_spacing_in / 2.0
            taper_coords = [
                [(0, base_y + cnt * self.wg_spacing_in), (-self.transition_length, base_y + cnt * self.wg_spacing_in)]
                for cnt in range(self.n_inputs)]

            return taper_coords

        @i3.cache()
        def _get_output_taper_coords(self):
            # coordinates of the output tapers [(taper1_begin, taper1_end), (taper2_begin, taper2_end), ...]
            base_y = - (self.n_outputs - 1) * self.wg_spacing_out / 2.0
            taper_coords = [[(self.length, base_y + cnt * self.wg_spacing_out),
                             (self.length + self.transition_length, base_y + cnt * self.wg_spacing_out)]
                            for cnt in range(self.n_outputs)]

            return taper_coords

        def _generate_elements(self, elems):
            layer_core = self.trace_template.core_layer
            # mmi core
            elems += i3.Rectangle(layer=layer_core, center=(+self.length / 2.0, 0.0),
                                  box_size=(self.length, self.width))
            # input wedges
            for bc, ec in self._get_input_taper_coords():
                elems += i3.Wedge(layer_core, begin_coord=bc, end_coord=ec, begin_width=self.taper_width,
                                  end_width=self.trace_template.core_width)
            for bc, ec in self._get_output_taper_coords():
                elems += i3.Wedge(layer_core, begin_coord=bc, end_coord=ec, begin_width=self.taper_width,
                                  end_width=self.trace_template.core_width)
            return elems

        def _generate_ports(self, ports):

            for cnt, coords in enumerate(self._get_input_taper_coords(), 1):
                ports += i3.OpticalPort(name="in{}".format(cnt), position=coords[1], angle=180.0,
                                        trace_template=self.trace_template)

            for cnt, coords in enumerate(self._get_output_taper_coords(), 1):
                ports += i3.OpticalPort(name="out{}".format(cnt), position=coords[1], angle=0.0,
                                        trace_template=self.trace_template)

            return ports

    class Netlist(i3.NetlistFromLayout):
        pass


mmi = MMI(name="MMI")
mmi_lv = mmi.Layout()
mmi_lv.visualize(annotate=True)
../_images/sphx_glr_plot_mmi_hierarchy_001.png

Specialized Class: MMI_12

Say we want to create a class MMI_12 with 1 input and 2 output. All we have to do is inherit from the MMI class and use i3.LockedProperty() to lock the properties that we don’t want the user of the MMI_12 class to change. The value of the properties is set through the default methods of the MMI_12 class. Two default methods ‘_default_n_inputs’ and ‘_default_n_outputs’ are added that override the default methods of the parent class (MMI). This means that as a PDK writer we are certain that each instance of MMI_12 will have one input port and two output ports.

class MMI_12(MMI):
    n_inputs = i3.LockedProperty()
    n_outputs = i3.LockedProperty()

    def _default_n_inputs(self):
        return 1

    def _default_n_outputs(self):
        return 2


mmi12 = MMI_12(name="MMI_12")
mmi12_lv = mmi12.Layout()
mmi12_lv.visualize(annotate=True)
../_images/sphx_glr_plot_mmi_hierarchy_002.png

Specialized Class: MMI_21

We can now create MMI_21 with two input ports and one output port.

class MMI_21(MMI):

    n_inputs = i3.LockedProperty()
    n_outputs = i3.LockedProperty()

    def _default_n_inputs(self):
        return 2

    def _default_n_outputs(self):
        return 1


mmi21 = MMI_21(name="MMI_21")
mmi21_lv = mmi21.Layout()
mmi21_lv.visualize(annotate=True)
../_images/sphx_glr_plot_mmi_hierarchy_003.png

Specialized Class: MMI_22

Same story for MMI_22

class MMI_22(MMI):

    n_inputs = i3.LockedProperty()
    n_outputs = i3.LockedProperty()

    def _default_n_inputs(self):
        return 2

    def _default_n_outputs(self):
        return 2


mmi22 = MMI_22(name="MMI_22")
mmi22_lv = mmi22.Layout()
mmi22_lv.visualize(annotate=True)
../_images/sphx_glr_plot_mmi_hierarchy_004.png

Specialized Class: SquareMMI

One can also override the properties at the Layout level. Here the class SquareMMI locks the width property in the layout view and equals it to the length in the ‘_default_width’ method. Here the class SquareMMI locks the width property in the layout view and equals it to the length in the ‘_default_width’ method. Also we inherit from MMI and n_inputs and n_outputs are not locked and settable by the user of the class.

class SquareMMI(MMI):
    class Layout(MMI.Layout):
        width = i3.LockedProperty()

        def _default_width(self):
            return self.length


sq_mmi = SquareMMI(name="SquareMMI", n_inputs=2, n_outputs=3)
sq_mmi_lv = sq_mmi.Layout(length=20.0)
sq_mmi_lv.visualize(annotate=True)
../_images/sphx_glr_plot_mmi_hierarchy_005.png