3.5. AWG simulation and analysis

3.5.1. Simulation

At this point we have a complete AWG with a layout, a logical netlist and a simulation model. We can now simply ask Caphe (IPKISS’ built-in circuit simulator) to calculate its S-parameters and save the results. This is done by the simulate_awg function that we have defined in rect_awg/simulate.py. This function takes the AWG PCell and a set of wavelengths as input and returns the simulated S-matrix.

Listing 3.40 luceda-academy/training/topical_training/cwdm_awg/rect_awg/simulate.py
from ipkiss3 import all as i3
import joblib
import os


def simulate_awg(awg, wavelengths, save_dir=None, tag=""):
    """Custom function to simulate an AWG. If you make an AWG, you should write one of your own.
    Parameters are the awg cell you want to simulate and things that control your simulation as well as
    parameters that control where you save your data.

    Parameters
    ----------
    awg : i3.Pcell
          AWG PCell
    wavelengths: ndarray
                 Simulation wavelengths
    save_dir: str, default: None, optional
              If specified, a GDSII file is saved in this directory
    tag: str, optional
         String used to give a name to saved files

    Returns
    -------
    awg_s:
           S-matrix of the simulated AWG

    """
    cwdm_awg_cm = awg.get_default_view(i3.CircuitModelView)
    print("Simulating AWG...\n")
    awg_s = cwdm_awg_cm.get_smatrix(wavelengths)
    if save_dir:
        smatrix_path = os.path.join(save_dir, "smatrix_{}.z".format(tag))
        joblib.dump(awg_s, smatrix_path)
        print("{} written".format(smatrix_path))
    return awg_s

3.5.2. Analysis

Next, the function analyze_awg, defined in rect_awg/analyze.py, will take the S-matrix and check it against the figure of merit. In this analysis function we define the AWG specifications and check the behaviour of the AWG according to those specs, using helper functions provided in the IPKISS AWG Designer. Let’s have a look a the function signature.

Listing 3.41 luceda-academy/training/topical_training/cwdm_awg/rect_awg/analyze.py
def analyze_awg(awg_s, plot=True, save_dir=None, tag=""):
    """Custom AWG analysis function. If you make an AWG, you should write an analysis function of your own.
    The goal of the analyze_awg function is to answer the question: how does the AWG perform
    according to my specs? In an analysis function you define the specs and check the behaviour
    of the AWG according to those specs using helper functions provided in the IPKISS AWG Designer.
    The parameters are the things you want to vary as well as options to control whether you
    plot and where you save your data.

    Parameters
    ----------
    awg_s:
           S-matrix of the AWG
    plot: bool, default: True, optional
           If true the spectrum  is plotted
    save_dir: str, default: None, optional
              If specified, a GDSII file is saved in this directory
    tag: str, optional
         String used to give a name to saved files

    Returns
    ------
    report: dict
            Dictionary containing analysis specs
    """

3.5.2.1. Specifications

The first step of the analysis function is to define the specifications. Here we hard code the specifications of the required transmission channels. If necessary, parametric specifications may be inserted in the signature of the analysis function.

Listing 3.42 luceda-academy/training/topical_training/cwdm_awg/rect_awg/analyze.py
    # Specs
    channel_freq = np.array([229000.0, 229800.0, 230600.0, 231400.0, 232200.0, 233000.0, 233800.0, 234600.0, 235400.0])
    channel_wav = awg.frequency_to_wavelength(channel_freq)
    channel_width = 10e-4
    sim_wav = awg_s.sweep_parameter_values
    sim_dw = sim_wav[1] - sim_wav[0]
    output_ports = ["out{}".format(p+1) for p in range(len(channel_wav))]

3.5.2.2. Insertion Loss

The insertion loss is calculated by using the IPKISS AWG Designer built-in peak detection functions. In order to account for the “flat-top” behaviour, we detect the peak of the overlap of the transmission with a step function that has the width of the required pass-band.

Listing 3.43 luceda-academy/training/topical_training/cwdm_awg/rect_awg/analyze.py
    # Specs
    channel_freq = np.array([229000.0, 229800.0, 230600.0, 231400.0, 232200.0, 233000.0, 233800.0, 234600.0, 235400.0])
    channel_wav = awg.frequency_to_wavelength(channel_freq)
    channel_width = 10e-4
    sim_wav = awg_s.sweep_parameter_values
    sim_dw = sim_wav[1] - sim_wav[0]
    output_ports = ["out{}".format(p+1) for p in range(len(channel_wav))]

    # Peak wavelengths
    awg_auto_corr = copy.copy(awg_s)
    for p1, p2 in itertools.product(awg_s.term_modes.keys(), awg_s.term_modes.keys()):
        data = awg_s[p1, p2]
        channel = np.ones(np.ceil(channel_width/sim_dw))
        channel = channel/len(channel)
        corr = np.correlate(np.abs(data), channel, 'same')
        awg_auto_corr[p1, p2, :] = corr

    peaks = awg.get_peaks(
        awg_auto_corr,
        input_pm="in1:0",
        output_pms=["{}:0".format(p) for p in output_ports],
        channel_wavelengths=channel_wav,
    )

    peak_error = np.abs(channel_wav - peaks)

3.5.2.3. Cross talk

Similarly, we calculate the nearest-neighbour and overall crosstalks using built-in analysis functions.

Listing 3.44 luceda-academy/training/topical_training/cwdm_awg/rect_awg/analyze.py
    # Crosstalk
    cross_talk = awg.get_crosstalk(
        awg_auto_corr,
        input_pm="in1:0",
        output_pms=["{}:0".format(p) for p in output_ports],
        channel_wavelengths=channel_wav,
    )

    cross_talk_nn = awg.get_nearest_neighbor_crosstalk(
        awg_auto_corr,
        input_pm="in1:0",
        output_pms=["{}:0".format(p) for p in output_ports],
        channel_wavelengths=channel_wav,
    )

3.5.2.4. Plotting and report generation

Finally, we plot the transmission spectrum and write a report containing the analyzed specifications.

Listing 3.45 luceda-academy/training/topical_training/cwdm_awg/rect_awg/analyze.py
    report = dict()
    for cnt, port in enumerate(output_ports):
        channel_report = {
            "peak_expected": channel_wav[cnt],
            "peak_simulated": peaks[cnt],
            "peak_error": peak_error[cnt],
            "insertion_loss": insertion_loss[cnt],
            "cross_talk": cross_talk[cnt]-insertion_loss[cnt],
            "cross_talk_nn": cross_talk_nn[cnt]-insertion_loss[cnt],
        }

        report[port] = channel_report

    fig = plt.figure()
    for p, ws, wa in zip(output_ports, channel_wav, peaks):
        plt.plot(sim_wav, 10 * np.log10(np.abs(awg_s[p, "in1"]) ** 2), label=p)
        plt.axvline(x=ws)
        plt.axvline(x=wa)
    plt.xlim([sim_wav[0], sim_wav[-1]])
    plt.ylim([-80.0, 0.0])
    plt.xlabel('Wavelength [um]')
    plt.ylabel('Transmission')
    plt.legend()
    plt.title(tag.capitalize())

    if save_dir:
        def serialize_ndarray(obj):
            return obj.tolist() if isinstance(obj, np.ndarray) else obj

        report_path = os.path.join(save_dir, "report_{}.json".format(tag))
        with open(report_path, "w") as f:
            json.dump(report, f, sort_keys=True, default=serialize_ndarray, indent=0)
        print("{} written".format(report_path))

        png_path = os.path.join(save_dir, "spectrum_{}.png".format(tag))
        fig.savefig(os.path.join(save_dir, png_path), bbox_inches='tight')
        print("{} written".format(png_path))

    if plot:
        # Plot transmission spectrum
        plt.show()
    plt.close(fig)
../../../_images/spectrum_bare.png

Report JSON file: report_finalized.json