3. Four-way WDM

As a next step, we increase the complexity of the demultiplexer. We create a four-way wavelength demultiplexer with a passband of 25% of the FSR at the provided center wavelength. It is implemented by staging a two-way demultiplexer on half the FSR, followed by two two-way demultiplexers tuned to the frequencies coming out of the first stage. The operating principle is shown in the figure below. The first stage (S1) splits the odd- and even-numbered frequency channels. In the second stage, the MUX2 at the bottom (S2 down) splits the two incoming odd-numbered channels, while the MUX2 at the top (S2 up) splits the even ones.

../../../_images/mux4_scheme.png

We define a class Mux4 that inherits from i3.Circuit and uses Mux2 as default PCell for the first and the second stage.

luceda-academy/training/topical_training/wdm_transmitter_mzi/mux4.py
class Mux4(i3.Circuit):
    """Four-way wavelength demultiplexer with a passband of 25% of the FSR at the center_wavelength.
    It is implemented by staging a two-way demultiplexer on half the FSR, followed by two two-way
    de-multiplexers tuned to the frequencies coming out of the first stage.
    """

    spacing_x = i3.PositiveNumberProperty(default=100.0, doc="Port-to-port spacing between the MUXs in the x-direction")
    spacing_y = i3.PositiveNumberProperty(default=150.0, doc="Port-to-port spacing between the MUXs in the y-direction")
    bend_radius = i3.PositiveNumberProperty(default=5.0, doc="Bend radius")
    fsr = i3.PositiveNumberProperty(default=0.02, doc="Free spectral range of the MUX4")
    center_wavelength = i3.PositiveNumberProperty(default=1.55, doc="Center wavelength")
    stage_1 = i3.ChildCellProperty(doc="MUX2 for the first stage")
    stage_2_up = i3.ChildCellProperty(doc="MUX2 for the second stage (up)")
    stage_2_down = i3.ChildCellProperty(doc="MUX2 for the second stage (down)")

    def _default_stage_1(self):
        return pt_lib.Mux2(
            center_wavelength=self.center_wavelength,
            fsr=self.fsr / 2.0,
            name=self.name + "_stage1",
        )

    def _default_stage_2_up(self):
        return pt_lib.Mux2(
            center_wavelength=self.center_wavelength + self.fsr / 4,
            fsr=self.fsr,
            name=self.name + "_stage2_up",
        )

    def _default_stage_2_down(self):
        return pt_lib.Mux2(
            center_wavelength=self.center_wavelength,
            fsr=self.fsr,
            name=self.name + "_stage2_down",
        )

    def _default_insts(self):
        return {
            "mux_0_0": self.stage_1,
            "mux_1_0": self.stage_2_down,
            "mux_1_1": self.stage_2_up,
        }

    def _default_specs(self):
        specs = [
            i3.Place("mux_0_0", (0, 0)),
            i3.PlaceRelative("mux_1_0:in1", "mux_0_0:out1", (self.spacing_x, -self.spacing_y / 2)),
            i3.PlaceRelative("mux_1_1:in1", "mux_0_0:out2", (self.spacing_x, +self.spacing_y / 2)),
        ]

        specs += [
            i3.ConnectManhattan(
                [
                    ("mux_0_0:out1", "mux_1_0:in1"),
                    ("mux_0_0:out2", "mux_1_1:in1"),
                ],
                bend_radius=self.bend_radius,
            )
        ]
        return specs

    def _default_exposed_ports(self):
        return {
            "mux_0_0:in1": "in1",
            "mux_0_0:in2": "in2",
            "mux_1_0:in2": "in3",
            "mux_1_1:in2": "in4",
            "mux_1_0:out1": "out1",
            "mux_1_0:out2": "out2",
            "mux_1_1:out1": "out3",
            "mux_1_1:out2": "out4",
        }

Two more properties are added to Mux4 compared to Mux2 and can be adapted by the user:

  • spacing_x: the port-to-port spacing between the demultiplexers in the x-direction.

  • spacing_y: the port-to-port spacing between the demultiplexers in the y-direction.

As for the two-way demultiplexer, we can visualize the layout and the circuit simulation:

  1. Layout:

    luceda-academy/training/topical_training/wdm_transmitter_mzi/mux4.py
        # Writing the layout
        cell = Mux4(
            name="MUX4",
            fsr=0.05,
            center_wavelength=1.55,
            spacing_x=50,
            spacing_y=80,
        )
        cell_lv = cell.Layout()
        cell_lv.visualize(annotate=True)
        cell_lv.write_gdsii("mux4.gds")
    
    ../../../_images/layout1.png
  2. Transmission spectrum of the first stage:

    ../../../_images/S1.png
  3. Transmission spectrum of the second stage, bottom branch:

    ../../../_images/S2_down.png
  4. Transmission spectrum of the second stage, top branch:

    ../../../_images/S2_up.png
  5. Total transmission spectrum of the MUX4:

    ../../../_images/S_total2.png

4. Eight-way WDM

Similarly, we can now create an eight-way demultiplexer with a passband of 12.5% of the FSR at the provided center wavelength. It is implemented by staging a four-way demultiplexer on half the FSR, followed by four two-way demultiplexers tuned to the frequencies coming out of the first stage. The operating principle is shown in the figure below. In the first stage (S1), the MUX4 splits the incoming frequencies in four pairs (1-4, 3-7, 2-6, 4-8). In the second stage, we use four MUX2 (S2_0, S2_1, S2_2, S2_3) to split in two each pair of frequencies coming out of the first stage.

../../../_images/mux8_scheme.png

We define a class Mux8 that inherits from i3.Circuit. It uses Mux4 as default PCell for the first stage and Mux2 for the second stage.

luceda-academy/training/topical_training/wdm_transmitter_mzi/mux8.py
class Mux8(i3.Circuit):
    """Eight-way wavelength demultiplexer with a passband of 12.5% of the FSR at the center_wavelength.
    It is implemented by staging a four-way multiplexer on half the FSR, followed by four two-way
    demultiplexers tuned to the frequencies coming out of the first stage.
    """

    spacing_x = i3.PositiveNumberProperty(default=150.0, doc="Port-to-port spacing between the MUXs in the x-direction")
    spacing_y = i3.PositiveNumberProperty(default=200.0, doc="Port-to-port spacing between the MUXs in the y-direction")
    bend_radius = i3.PositiveNumberProperty(default=5.0, doc="Bend radius")
    fsr = i3.PositiveNumberProperty(default=0.01, doc="Free spectral range of the MUX8")
    center_wavelength = i3.PositiveNumberProperty(default=1.55, doc="Center wavelength")
    stage_1 = i3.ChildCellProperty(doc="MUX4 for the first stage")
    stage_2 = i3.ChildCellListProperty(doc="MUX2 for the second stages")

    def _default_stage_1(self):
        return Mux4(
            center_wavelength=self.center_wavelength,
            fsr=self.fsr / 2.0,
            name=self.name + "_stage1",
            spacing_x=self.spacing_x,
            spacing_y=self.spacing_y,
        )

    def _default_stage_2(self):
        stages = []
        channels = [1, 3, 2, 4]
        for cnt, channel in enumerate(channels):
            cell = pt_lib.Mux2(
                center_wavelength=self.center_wavelength + (channel - 1) * self.fsr / 8.0,
                fsr=self.fsr,
                name=self.name + "_stage2_{}".format(cnt),
            )
            stages.append(cell)
        return stages

    def _default_insts(self):
        instances = dict()
        instances["mux_0_0"] = self.stage_1
        for cnt, stage in enumerate(self.stage_2):
            instances["mux_1_{}".format(cnt)] = stage
        return instances

    def _default_specs(self):
        x0 = self.stage_1.get_default_view(i3.LayoutView).size_info().east

        specs = [i3.Place("mux_0_0", (0, 0))]
        specs += [
            (
                i3.Place(
                    "mux_1_{}:in1".format(cnt),
                    (x0 + self.spacing_x, (-3 + 2 * cnt) * self.spacing_y / 4.0),
                )
            )
            for cnt in range(4)
        ]
        specs += [
            i3.ConnectManhattan(
                "mux_0_0:out{}".format(cnt + 1),
                "mux_1_{}:in1".format(cnt),
                bend_radius=self.bend_radius,
            )
            for cnt in range(4)
        ]
        return specs

    def _default_exposed_ports(self):
        exposed_ports = dict()
        exposed_ports["mux_0_0:in1"] = "in1"
        exposed_ports["mux_0_0:in2"] = "in2"
        exposed_ports["mux_0_0:in3"] = "in3"
        exposed_ports["mux_0_0:in4"] = "in4"

        exposed_ports["mux_1_0:in2"] = "in5"
        exposed_ports["mux_1_1:in2"] = "in6"
        exposed_ports["mux_1_2:in2"] = "in7"
        exposed_ports["mux_1_3:in2"] = "in8"
        for cnt in range(4):
            exposed_ports["mux_1_{}:out1".format(cnt)] = "out{}".format(2 * cnt + 1)
            exposed_ports["mux_1_{}:out2".format(cnt)] = "out{}".format(2 * cnt + 2)
        return exposed_ports

We can now visualize the layout and the circuit simulation:

  1. Layout:

    luceda-academy/training/topical_training/wdm_transmitter_mzi/mux8.py
        # Writing the layout
        cell = Mux8(
            name="MUX8",
            fsr=0.02,
            center_wavelength=1.55,
        )
        cell_lv = cell.Layout()
        cell_lv.visualize(annotate=True)
        cell_lv.write_gdsii("mux8.gds")
    
    ../../../_images/layout2.png
  2. Total transmission spectrum of the MUX8:

    ../../../_images/S_total3.png

5. Parametric WDM

Finally, following the same logic used to define Mux4 and Mux8, we create a parametric wavelength demultiplexer. If \(n\) is the number of stages, then the demultiplexer will have \(2^{n+1}\) channels and a passband of \(100/(2^{n+1})`% of the FSR at the specified center wavelength. We define a class ``MuxParametric`\) that inherits from i3.Circuit. It is implemented by staging a (\(n-1\))-way demultiplexer on half the FSR, followed by \(n\) two-way demultiplexers (Mux2) tuned to the frequencies coming out of the first stage.

luceda-academy/training/topical_training/wdm_transmitter_mzi/mux_parametric.py
class MuxParametric(i3.Circuit):
    """2**(n+1)-way wavelength demultiplexer with a passband of 100/2**(n+1)% of the FSR at the center_wavelength.
    It is implemented by staging a n-1-way demultiplexer on half the FSR, followed by n two-way
    demultiplexers tuned to the frequencies coming out of the first stage.
    """

    spacing_x = i3.PositiveNumberProperty(default=250.0, doc="Port-to-port spacing between the MUXs in the x-direction")
    spacing_y = i3.PositiveNumberProperty(default=200.0, doc="Port-to-port spacing between the MUXs in the y-direction")
    fsr = i3.PositiveNumberProperty(default=0.01, doc="Free spectral range of the MUX with N stages")
    n_stages = i3.IntProperty(default=3, doc="Number of stages")
    center_wavelength = i3.PositiveNumberProperty(default=1.55, doc="Center wavelength")
    bend_radius = i3.PositiveNumberProperty(default=5.0, doc="Bend radius")
    phase_error_width_deviation = i3.NonNegativeNumberProperty(default=0.0)
    phase_error_correlation_length = i3.NonNegativeNumberProperty(default=0.0)
    stage_1 = i3.ChildCellProperty(doc="MUX with N stages for the first stage")
    stage_2 = i3.ChildCellListProperty(doc="MUX2 for the second stages")

    def n_channels(self):
        return int(2 ** (self.n_stages + 1))

    def _default_stage_1(self):
        if self.n_stages > 0:
            cell = MuxParametric(
                name=self.name + "_stage1",
                center_wavelength=self.center_wavelength,
                fsr=self.fsr / 2.0,
                n_stages=self.n_stages - 1,
                spacing_x=self.spacing_x,
                spacing_y=self.spacing_y * 2,
                phase_error_width_deviation=self.phase_error_width_deviation,
                phase_error_correlation_length=self.phase_error_correlation_length,
            )
        else:
            cell = pt_lib.Mux2(
                name=self.name + "_stage1",
                center_wavelength=self.center_wavelength,
                fsr=self.fsr,
                phase_error_width_deviation=self.phase_error_width_deviation,
                phase_error_correlation_length=self.phase_error_correlation_length,
            )
        return cell

    def _default_stage_2(self):
        stages = []

        for cnt in range(self.n_channels() // 2):
            cell = pt_lib.Mux2(
                name=self.name + "_stage2_{}".format(cnt),
                center_wavelength=self.center_wavelength + cnt * self.fsr / self.n_channels(),
                fsr=self.fsr,
                phase_error_width_deviation=self.phase_error_width_deviation,
                phase_error_correlation_length=self.phase_error_correlation_length,
            )
            stages.append(cell)
        return stages

    def _default_insts(self):
        insts = dict()
        insts["mux_0_0"] = self.stage_1
        if self.n_stages > 0:
            for cnt, stage in enumerate(self.stage_2):
                insts["mux_1_{}".format(cnt)] = stage
        return insts

    def _default_specs(self):
        specs = [i3.Place("mux_0_0", (0, 0))]

        if self.n_stages > 0:
            x, y = self.spacing_x, self.spacing_y
            x0 = self.stage_1.get_default_view(i3.LayoutView).size_info().east
            tot_y = (self.n_channels() // 2 - 1) * y

            specs += [
                i3.Place("mux_1_{}:in1".format(cnt), (x0 + x, -tot_y / 2.0 + cnt * y))
                for cnt in range(self.n_channels() // 2)
            ]
            specs += [
                i3.ConnectManhattan(
                    "mux_0_0:out{}".format(cnt + 1),
                    "mux_1_{}:in1".format(cnt),
                    bend_radius=self.bend_radius,
                )
                for cnt in range(self.n_channels() // 2)
            ]
        return specs

    def _default_exposed_ports(self):
        exposed_ports = dict()
        exposed_ports["mux_0_0:in1"] = "in1"
        exposed_ports["mux_0_0:in2"] = "in2"

        if self.n_stages > 0:
            for cnt in range(self.n_channels() // 2):
                exposed_ports["mux_0_0:in{}".format(cnt + 1)] = "in{}".format(cnt + 1)
                exposed_ports["mux_1_{}:in2".format(cnt)] = "in{}".format((2**self.n_stages) + cnt + 1)

                exposed_ports["mux_1_{}:out1".format(cnt)] = "out{}".format(2 * cnt + 1)
                exposed_ports["mux_1_{}:out2".format(cnt)] = "out{}".format(2 * cnt + 2)
        else:
            exposed_ports["mux_0_0:out1"] = "out1"
            exposed_ports["mux_0_0:out2"] = "out2"
        return exposed_ports

We can now visualize the layout and the circuit simulation:

  1. Layout:

    luceda-academy/training/topical_training/wdm_transmitter_mzi/mux_parametric.py
        # Writing the layout
        cell = MuxParametric(
            name="MUX",
            n_stages=3,
            fsr=0.05,
            center_wavelength=1.55,
            spacing_x=40,
            spacing_y=60,
            phase_error_width_deviation=0.001,
            phase_error_correlation_length=1.0,
        )
        cell_lv = cell.Layout()
        cell_lv.visualize(annotate=True)
        cell_lv.write_gdsii("mux{}.gds".format(cell.n_channels()))
    
    ../../../_images/layout3.png
  2. Total transmission of the CWDM:

    ../../../_images/S_total4.png