Location Aware Variability Analysis of a demultiplexer

This example demonstrates how to perform a Location Aware Variability Analysis on a wavelength demultiplexer (Mux2) from the pteam_library_si_fab.

Location Aware Variability Analysis simulates the circuit’s performance at different locations on a wafer, considering process variations such as waveguide width and thickness which are modeled as spatially correlated random variables.

This example will:
  1. Instantiate a Mux2 component.

  2. Define wafer positions and variability maps for thickness and width.

  3. Visualize the variability maps.

  4. Perform a wafer sweep simulation using the IPKISS circuit analyzer.

  5. Plot the transmission spectra for all wafer positions to visualize the impact of process variations.

The analysis relies on a configuration folder (ca_config) which defines how process variations map to model parameters. This folder contains two key files:

models.yaml:

This file defines the process parameters that are subject to variation. For each parameter, it specifies documentation, a default value, and corner values for corner analysis. It also links to a Python function in which the designer defines the parameters to be passed to the compact models.

version: 1
libraries:
  si_fab:
    cells:
      GenericWaveguide:
        parameters:
          core_width_variation:
            doc: "width variation"
            default: 0
            corners: [ -0.5, 0, 0.5 ]
          core_height_variation:
            doc: "height variation"
            default: 0
            corners: [ -2, 0, 2 ]
        generate_parameters: "models.waveguide_parameters"
models.py:

This file contains the Python functions referenced in the YAML file. The waveguide_parameters function, for example, calculates the effective refractive index (n_eff) based on the nominal waveguide dimensions and the sampled variations for core width and height.

import si_fab.all as pdk
import numpy as np


def waveguide_parameters(self: pdk.GenericWaveguide, **parameters):
    template = self.template
    width = template.cell.Layout().core_width + parameters["core_width_variation"] * 1e-3
    height = template.core_height + parameters["core_height_variation"] * 1e-3
    n_eff = template.get_n_eff_interp_for_design_parameters(width, height)  # for si wire
    return {
        "n_eff": np.array(n_eff),
    }
import matplotlib.pyplot as plt
import numpy as np
import pteam_library_si_fab.all as pt_lib
import si_fab.all as pdk

import circuit_analyzer.all as ca
import ipkiss3.all as i3
from circuit_analyzer.capheve import view  # noqa
from circuit_analyzer.capheve.variability_map import CoherentNoiseMap
from circuit_analyzer.utilities.analysis import plot_wafer_positions
  1. Instantiate Mux2 and get models

mux = pt_lib.Mux2(name="my_mux2_for_wafer_analysis")
mux_layout = mux.Layout()
cm = mux.get_default_view(i3.CircuitModelView)

# Add VE_sampling_period for waveguides; this is how waveguides get sampled along the longitudinal axis
pdk.SiWireWaveguideTemplate.CircuitModel.VE_sampling_period = 10
cm.visualize_VE_sample_points()
plot mux wafermap variability
  1. Define simulation parameters

# Define wavelength range for the simulation
start_wavelength: float = 1.5
end_wavelength: float = 1.55
n_wavelength_points: int = 501
wavelengths = np.linspace(start_wavelength, end_wavelength, n_wavelength_points)

# Define die positions on the wafer
die_step = i3.Coord2(20000.0, 20000.0)  # die step

# You can increase n_rows and n_cols (to e.g. 7 or 10) to have more simulation results
n_cols = 4
n_rows = 4
positions = [
    ((i % n_cols - (n_cols - 1.0) / 2.0) * die_step.x, (i // n_cols - (n_rows - 1.0) / 2.0) * die_step.y)
    for i in range(n_cols * n_rows)
]
  1. Define and visualize variability maps

    for this we use CoherentNoiseMap function that generates a random map with a given radius of coherence. Wafer maps used here are similar to the maps used in doi: 10.1109/JSTQE.2019.2906271

map_thickness = CoherentNoiseMap(amplitude=2, radius=50000, seed=0)
map_width = CoherentNoiseMap(amplitude=0.5, radius=500.0, seed=0)

fig_map_thickness = plt.figure("Wafer thickness variability map")
map_thickness.visualize(show_contour=True, show=False, use_figure=fig_map_thickness)
axs = fig_map_thickness.get_axes()
axs[0].set_title("Wafer thickness variability map")
axs[0].set_xticks([-100000, 0, 100000])
axs[0].set_yticks([-100000, 0, 100000])

axs[0].set_xticklabels([-100, 0, 100])
axs[0].set_yticklabels([-100, 0, 100])

axs[0].set_xlabel("Position (mm)")
axs[0].set_ylabel("Position (mm)")
axs[1].set_ylabel("Wafer thickness variation (nm)")
plot_wafer_positions(positions, size_info=mux_layout.size_info(), diameter=200000)
plt.show()
fig_map_width = plt.figure("Waveguide width variability map")
map_width.visualize(show_contour=True, show=False, use_figure=fig_map_width)
axs = fig_map_width.get_axes()
axs[0].set_title("Wafer width variability map")
axs[0].set_xticks([-100000, 0, 100000])
axs[0].set_yticks([-100000, 0, 100000])

axs[0].set_xticklabels([-100, 0, 100])
axs[0].set_yticklabels([-100, 0, 100])

axs[0].set_xlabel("Position (mm)")
axs[0].set_ylabel("Position (mm)")
axs[1].set_ylabel("Wafer width variation (nm)")
plot_wafer_positions(positions, size_info=mux_layout.size_info(), diameter=200000)
plt.show()
  • Wafer thickness variability map
  • Wafer width variability map
  1. Perform wafer map sweep

with ca.setup("ca_config"):
    deviation_maps = {
        "core_height_variation": map_thickness,
        "core_width_variation": map_width,
    }

    smats = ca.sweep_wafermap(
        circuit_model=cm,
        wavelengths=wavelengths,
        deviation_maps=deviation_maps,
        positions=positions,
        save_path="wafer_sweep_mux2.dat",
    )
  1. Plot the results

plt.figure("Wafer Map Analysis", figsize=(10, 6))
plt.grid(True, color="gray", linestyle="--", linewidth=0.5, alpha=0.5)
plt.title("Mux2 Wafer Map Analysis")
plt.xlabel(r"Wavelength [$\mu$m]", fontsize=12)
plt.ylabel("Transmission (S21) [dB]", fontsize=12)

all_smat_values = []
for smat in smats:
    smat_values = 20 * np.log10(np.abs(smat["in1", "out1"]))
    all_smat_values.append(smat_values)
    # Plot each spectrum with some transparency
    plt.plot(smat.sweep_parameter_values, smat_values, "b-", linewidth=0.5, alpha=0.2)


# Plot the tolerance envelope and nominal response

all_smat_values = np.array(all_smat_values)
envelope_max = np.max(all_smat_values, axis=0)
envelope_min = np.min(all_smat_values, axis=0)

# Fill the area between the min and max corners
plt.fill_between(
    wavelengths,
    envelope_min,
    envelope_max,
    color="blue",
    alpha=0.2,
    label="Tolerance Envelope",
)
# Plot the envelope boundaries
plt.plot(wavelengths, envelope_min, color="blue", linewidth=1, linestyle="--", alpha=0.7)
plt.plot(wavelengths, envelope_max, color="blue", linewidth=1, linestyle="--", alpha=0.7)

# Add nominal curve for reference
nominal_smat = cm.get_smatrix(wavelengths=wavelengths)
nominal_values = 20 * np.log10(np.abs(nominal_smat["in1", "out1"]))
plt.plot(wavelengths, nominal_values, "k-", linewidth=1.5, label="Nominal")

plt.legend()
plt.show()
Mux2 Wafer Map Analysis

Total running time of the script: ( 3 minutes 10.924 seconds)