Note
Go to the end to download the full example code
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:
Instantiate a Mux2 component.
Define wafer positions and variability maps for thickness and width.
Visualize the variability maps.
Perform a wafer sweep simulation using the IPKISS circuit analyzer.
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
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()

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)
]
- 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()
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",
)
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()

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