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.
We define a class Mux4
that inherits from i3.Circuit
and uses Mux2
as default PCell for the first and the second stage.
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.Place("mux_1_0:in1", (self.spacing_x, -self.spacing_y / 2), relative_to="mux_0_0:out1"),
i3.Place("mux_1_1:in1", (self.spacing_x, +self.spacing_y / 2), relative_to="mux_0_0:out2"),
]
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:
Layout:
# 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")
Transmission spectrum of the first stage:
Transmission spectrum of the second stage, bottom branch:
Transmission spectrum of the second stage, top branch:
Total transmission spectrum of the MUX4:
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.
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.
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 + f"_stage2_{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[f"mux_1_{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(
f"mux_1_{cnt}:in1",
(x0 + self.spacing_x, (-3 + 2 * cnt) * self.spacing_y / 4.0),
)
)
for cnt in range(4)
]
specs += [
i3.ConnectManhattan(
f"mux_0_0:out{cnt + 1}",
f"mux_1_{cnt}:in1",
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[f"mux_1_{cnt}:out1"] = f"out{2 * cnt + 1}"
exposed_ports[f"mux_1_{cnt}:out2"] = f"out{2 * cnt + 2}"
return exposed_ports
We can now visualize the layout and the circuit simulation:
Layout:
# 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")
Total transmission spectrum of the MUX8:
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.
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 + f"_stage2_{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[f"mux_1_{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(f"mux_1_{cnt}:in1", (x0 + x, -tot_y / 2.0 + cnt * y)) for cnt in range(self.n_channels() // 2)
]
specs += [
i3.ConnectManhattan(
f"mux_0_0:out{cnt + 1}",
f"mux_1_{cnt}:in1",
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[f"mux_0_0:in{cnt + 1}"] = f"in{cnt + 1}"
exposed_ports[f"mux_1_{cnt}:in2"] = f"in{(2**self.n_stages) + cnt + 1}"
exposed_ports[f"mux_1_{cnt}:out1"] = f"out{2 * cnt + 1}"
exposed_ports[f"mux_1_{cnt}:out2"] = f"out{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:
Layout:
# 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(f"mux{cell.n_channels()}.gds")
Total transmission of the CWDM: