Make a Rectangular AWG

This simple example illustrates how to make a rectangular AWG and how to run a Caphe simulation on it. For this example, we use the Luceda AWG Designer module.

Getting started

We start by importing the technology with other required modules:

from si_fab import all as pdk  # noqa
from ipkiss3 import all as i3
import numpy

import awg_designer.all as awg
from si_fab_awg import all as awg_pdk

Next, create a template for the free propagation region. This defines the layers, slab modes, etc.

slab_t = awg_pdk.SiSlabTemplate()
slab_t.Layout()
slab_t.SlabModes(modes=[awg.SimpleSlabMode(name="TE0", n_eff=2.8, n_g=3.2, polarization="TE")])

N = 44  # number of arms
R = 150.0  # radius of the star couplers
W = 2.0  # aperture width
M = 8  # outputs

The aperture

Make a virtual aperture

ap = awg_pdk.SiRibAperture(
    slab_template=slab_t,
    aperture_core_width=W,
    aperture_cladding_width=1.0,
)
ap_lo = ap.Layout()
ap_sm = ap.FieldModelFromCamfr()
ap_cm = ap.CircuitModel(simulation_wavelengths=[1.55])

The input star coupler

Make a multi-aperture for the arms consisting of N apertures like these, arranged in a circle and get the transformations of the individual apertures.

angle_step = i3.RAD2DEG * (W + 0.2) / R
angles_arms = numpy.linspace(-angle_step * (N - 1) / 2.0, angle_step * (N - 1) / 2.0, N)
ap_arms_in, _, trans_arms_in, trans_ports_in = awg.get_star_coupler_apertures(
    apertures_arms=[ap] * N,
    apertures_ports=[ap],
    angles_arms=angles_arms,
    angles_ports=[0],
    radius=R,
    mounting="confocal",
    input=True,
)

Make the input star coupler

sc_in = awg.StarCoupler(aperture_in=ap, aperture_out=ap_arms_in)

sc_in_lo = sc_in.Layout(
    contour=awg.get_star_coupler_extended_contour(
        apertures_in=[ap],
        apertures_out=[ap] * N,
        trans_in=trans_ports_in,
        trans_out=trans_arms_in,
        radius_in=R,
        radius_out=R,
        extension_angles=(10, 5),
    )
)
sc_in_lo.visualize()
plot rect awg

The output star coupler

Make the multi-apertures for the outputs and get the transformations for the individual apertures.

angle_step = i3.RAD2DEG * (4.7) / R
angles_ports = numpy.linspace(angle_step * (M - 1) / 2.0, -angle_step * (M - 1) / 2.0, M)
ap_arms_out, ap_out, trans_arms_out, trans_ports_out = awg.get_star_coupler_apertures(
    apertures_arms=[ap] * N,
    apertures_ports=[ap] * M,
    angles_arms=angles_arms,
    angles_ports=angles_ports,
    radius=R,
    mounting="rowland",
    input=False,
)

Make the output star coupler

sc_out = awg.StarCoupler(aperture_in=ap_arms_out, aperture_out=ap_out)
sc_out_lo = sc_out.Layout(
    contour=awg.get_star_coupler_extended_contour(
        apertures_in=[ap] * N,
        apertures_out=[ap] * M,
        trans_in=trans_arms_out,
        trans_out=trans_ports_out,
        radius_in=R,
        radius_out=R / 2,
        extension_angles=(10, 10),
    )
)
sc_out_lo.visualize()
plot rect awg

The rectangular waveguide array

Make the RectangularWaveguideArray

delays = [50.0 * i for i in range(N)]
ports = [port for port in sc_in_lo.ports.y_sorted() if "out" in port.name]
waveguide_array = awg.RectangularWaveguideArray(start_ports=ports, delay_lengths=delays)
waveguide_array.Layout(bundle_spacing=40)

The Arrayed Waveguide Grating

Make an AWG with the 3 building blocks

rect_awg = awg.ArrayedWaveguideGrating(
    star_coupler_in=sc_in,
    star_coupler_out=sc_out,
    waveguide_array=waveguide_array,
)

awg_lo = rect_awg.Layout()
awg_lo.visualize()
plot rect awg

Running the Caphe simulation

print("Running Caphe simulation (with wavelength-independent star couplers)")
sc_in.CircuitModel(simulation_wavelengths=[1.55])
sc_out.CircuitModel(simulation_wavelengths=[1.55])
awg_cm = rect_awg.CircuitModel()
wavelengths = numpy.linspace(1.52, 1.58, 401)

import time  # noqa

t0 = time.time()
S = awg_cm.get_smatrix(wavelengths)
print("Calculation time:", time.time() - t0)

term_pairs = [("in1", f"out{i}") for i in range(1, M + 1)]
S.visualize(term_pairs=term_pairs, scale="dB")
plot rect awg
Running Caphe simulation (with wavelength-independent star couplers)
Calculation time: 28.98907709121704

Using SpectrumAnalyzer to analyze the results

We finally use i3.SpectrumAnalyzer to detect the peaks and calculate the crosstalk.

sa = i3.SpectrumAnalyzer(
    smatrix=S,
    input_port_mode="in1",
    output_port_modes=[f"out{i}" for i in range(1, M + 1)],
    peak_method="cwt",
    peak_threshold=-25,
)
sa.visualize(title="Peaks")
Peaks

Let’s calculate the passbands for a cutoff of 25 dB:

bands = sa.cutoff_passbands(-25)

And then calculate the nearest neighbor crosstalk for each channel. The value reported for out1 is the crosstalk caused by the neirest channels out8 and out2:

print(sa.near_crosstalk(bands))
OrderedDict({'out1': -31.23647722883299, 'out2': -28.541374107201726, 'out3': -28.884413846502884, 'out4': -29.58195008288385, 'out5': -29.071172423995584, 'out6': -28.661616260124084, 'out7': -28.43442558539528, 'out8': -31.465196996011255})

We also calculate the far neighbor crosstalk, which ignores nearest neighbors:

print(sa.far_crosstalk(bands))
OrderedDict({'out1': -34.567785629502374, 'out2': -34.6620449968023, 'out3': -35.45329942179792, 'out4': -36.85881897261201, 'out5': -35.37251774449044, 'out6': -35.21353059231582, 'out7': -34.47836914658963, 'out8': -34.594663877753106})