Fabrication-Aware Simulation: Two Coupled Ring Resonator Filter

a3b02789d52146a088c53074eb05a85a

In this notebook we demonstrate how we can perform fabrication-aware simulations in PhotonForge by modeling a 2-ring coupled resonator filter and running a Monte Carlo analysis on key process-variation parameters. We focus on variations in the coupler gap, silicon wafer thickness, and sidewall angle, then visualize their impact on spectral response.

We use the couple mode theory presented in [1] for the filter design.

References

  1. Little, Brent E., et al. “Microring resonator channel dropping filters.” Journal of lightwave technology 1997 15 (6), 998-1005, doi: 10.1109/50.588673.

[1]:
import matplotlib.pyplot as plt
import numpy as np
import photonforge as pf
import tidy3d as td
from scipy.signal import find_peaks
import pandas as pd


# Reduce non-critical Tidy3D logs to keep the notebook output clean
td.config.logging_level = "ERROR"

Setting Up Technology and Simulation Defaults

Here we load and configure the default technology, mesh, and wavelength settings that will be used throughout the notebook.
We start from PhotonForge’s basic technology, set it as the default, and define a bending radius and port specification. We also fix a mesh refinement level and specify the simulation wavelength range around 1.55 µm — the telecom C-band region where most silicon photonic devices operate.
[2]:
# Load the basic technology
tech = pf.basic_technology()

# Set this as the default technology for all subsequent components
pf.config.default_technology = tech

# Extract the standard single-mode strip waveguide port specification
port_spec = tech.ports["Strip"]

# Set some default parameters for all components (e.g., bend radius)
pf.config.default_kwargs = {
    "radius": 5,  # bend radius in microns
    "port_spec": port_spec,
}

# Define the default mesh refinement level
pf.config.default_mesh_refinement = 15

# Define the wavelength range for optical simulations [µm]
wavelengths = np.linspace(1.543, 1.557, 401)

Parametric 2-Ring Filter Construction

We define a parametric function to build a two coupled ring resonator (2-RR) filter using PhotonForge primitives. The function instantiates two types of couplers — a bus-to-ring coupler and a ring-to-ring coupler — and then stitches them with a netlist that sets up instances, interconnects ports, and exposes the device I/O.

What we parameterize

  • coupling_length: the straight interaction length in the couplers, controlling the coupling coefficient.

  • bus_coupling_distance: the nominal gap between the bus waveguide and each ring.

  • ring_coupling_distance: the nominal gap between the two rings in the inter-ring coupler.

Netlist assembly We instantiate:

  • Two bus couplers (bus_coupler0, bus_coupler1)

  • One ring-to-ring coupler (ring_coupler0)

We then connect their ports to form the 2-RR filter topology and publish four external ports.

Finally, we build the component from the netlist and create a default instance for quick visualization.

Flat-Top (Maximally-Flat) Design Condition

To achieve a flat-top (maximally-flat) passband in the two-ring add–drop filter (with identical rings), we choose the coupling gaps such that the bus-to-ring and ring-to-ring coupling coefficients satisfy the synthesis relation derived in [1]:

\(\kappa_{\text{int}} = \frac{1}{2} \kappa_{\text{ext}}^2\)

where:

  • \(\kappa_{\text{ext}}\) — bus-to-ring (external) power coupling coefficient (\(\kappa_{\text{ext}} \approx 0.4\) in our design)

  • \(\kappa_{\text{int}}\) — ring-to-ring (internal) power coupling coefficient (\(\kappa_{\text{int}} \approx 0.08\) in our design)

This condition ensures a maximally-flat spectral response by properly balancing external and internal coupling strengths.

[3]:
@pf.parametric_component
def create_two_rr_filter(
    *,
    coupling_length: float = 13.5,
    bus_coupling_distance: float = 0.7,
    ring_coupling_distance: float = 0.81,
) -> pf.Component:
    """
    Build a 2-ring (2RR) filter from parametric couplers and return the component.
    """
    # Parametric subcomponents
    bus_to_ring = pf.parametric.dual_ring_coupler(
        coupling_distance=bus_coupling_distance,
        coupling_length=coupling_length,
        tidy3d_model_kwargs={
            "verbose": False,
            "port_symmetries": [
                ("P1", "P0", "P3", "P2"),
                ("P2", "P3", "P0", "P1"),
                ("P3", "P2", "P1", "P0"),
            ],
        },
    )

    ring_to_ring = bus_to_ring.copy().update(coupling_distance=ring_coupling_distance)

    # Netlist for assembling the 2RR resonator
    netlist_2RR = {
        "name": "filter",
        "instances": {
            "bus_coupler0": bus_to_ring,
            "bus_coupler1": bus_to_ring,
            "ring_coupler0": ring_to_ring,
        },
        "connections": [
            (("ring_coupler0", "P0"), ("bus_coupler0", "P1")),
            (("bus_coupler1", "P3"), ("ring_coupler0", "P1")),
        ],
        "ports": [
            ("bus_coupler0", "P0"),
            ("bus_coupler1", "P2"),
            ("bus_coupler0", "P2"),
            ("bus_coupler1", "P0"),
        ],
        "models": [pf.CircuitModel()],
    }

    two_rr = pf.component_from_netlist(netlist_2RR)
    return two_rr


two_rr = create_two_rr_filter()
two_rr
[3]:
../_images/examples_Fabrication_Aware_Simulation_5_0.svg

We now compute and visualize the spectral characteristics of the designed 2-RR filter.

[4]:
# Compute the scattering matrix (S-matrix) of the 2RR filter across wavelengths
s_matrix_2rr = two_rr.s_matrix(frequencies=pf.C_0 / wavelengths)

# Plot the S-matrix with input from port P0, showing response in dB
_ = pf.plot_s_matrix(s_matrix_2rr, input_ports=["P0"])
Loading cached simulation from /home/amin/.tidy3d/pf_cache/JOE/ms_info-XIP56TS7ZEUQSBBJ7Z7EZAFHCYKSGN6ODWUHSR4NIBHTT4VGYP2Q.json.
Loading cached simulation from /home/amin/.tidy3d/pf_cache/QUM/ms_info-Z47PPQEWJ3A5HIDN4R7I4RFVVSWZWYVVDSUHGZB7DBJTMHRSVTCA.json.
Progress: 100%
../_images/examples_Fabrication_Aware_Simulation_7_1.png

Monte Carlo Analysis

To evaluate fabrication tolerance and yield, we perform a Monte Carlo simulation on the 2-RR filter.
Each sample randomly perturbs selected geometric and material parameters within specified distributions, emulating real-world process variations.

Parameters under variation

  • core_thickness (technology): silicon layer thickness, mean 0.22 µm with a 3 nm standard deviation

  • sidewall_angle (technology): etched sidewall deviation, uniformly sampled between 0° and 5°

  • bus_coupling_distance (component): coupling gap between bus and ring, mean 0.7 µm with 10 nm σ

  • ring_coupling_distance (component): gap between rings, mean 0.79 µm with 10 nm σ

We include both random and corner samples to span statistical variability and worst-case scenarios.
The resulting dataset contains a list of parameter realizations and their corresponding S-matrices.
[5]:
# Run a Monte Carlo S-matrix analysis to capture fabrication-induced variations
var, results = pf.monte_carlo.s_matrix(
    two_rr,
    pf.C_0 / wavelengths,
    ("core_thickness", "technology", {"value": 0.22, "stdev": 0.003}),
    ("sidewall_angle", "technology", {"value_range": [0, 5]}),
    ("bus_coupling_distance", two_rr, {"value": 0.7, "stdev": 0.01}),
    ("ring_coupling_distance", two_rr, {"value": 0.81, "stdev": 0.01}),
    random_samples=20,  # number of random samples
    corner_samples=2,  # corner cases (extreme combinations)
    random_seed=0,
    show_progress=True,
)
Starting sample 1 of 22…
Loading cached simulation from /home/amin/.tidy3d/pf_cache/LRU/ms_info-JSKJTDXWFBBR32NYCYBPQ5JJ7Y66JDWBZJ4HSSL37ILDARRWEIJQ.json.
Loading cached simulation from /home/amin/.tidy3d/pf_cache/BYE/ms_info-SXKDZJTEDUITOH6O6SEDSADVD6O2ZP6U5Y7ZDS45XLXBE76G3YSQ.json.
Starting sample 2 of 22…
Loading cached simulation from /home/amin/.tidy3d/pf_cache/PSC/ms_info-36ARABZQI5JRQBP2UCCWGG2XYYIMHNERKYANB4U4V4I3Y72OR4GQ.json.
Loading cached simulation from /home/amin/.tidy3d/pf_cache/MJT/ms_info-A4OS4MQNDWMFKKCYON6QG4LKFQCN65GG2MK4KRSVFBOHBMXZAJUQ.json.
Starting sample 3 of 22…
Loading cached simulation from /home/amin/.tidy3d/pf_cache/Y6J/ms_info-QRQ72JYYJ6UNX3R25UM7MABILVFD5E26GKV5LT3KT24KS7BL7XOQ.json.
Loading cached simulation from /home/amin/.tidy3d/pf_cache/TXD/ms_info-UR25HAJ6Y4JTLM452HME54CXNUZ4EZDQPTAGLVHM6YKUGU42KBEQ.json.
Starting sample 4 of 22…
Loading cached simulation from /home/amin/.tidy3d/pf_cache/53Z/ms_info-TA4QH27XK5VPAMQI4U3JHLNJBD6HAIJ4PDT2AD52EIL7ORQYHTNQ.json.
Loading cached simulation from /home/amin/.tidy3d/pf_cache/ETT/ms_info-DH7G2CE2SYUKLM6GUXOH7X7OI7L57XMT65CG7PNMFVF7ACKWT4GQ.json.
Starting sample 5 of 22…
Loading cached simulation from /home/amin/.tidy3d/pf_cache/N3Q/ms_info-DFMHVAQBHWZWWP5H4GZ4YY4DDSXAYNL3YYBQJDPIOGWWCYRRHPQA.json.
Loading cached simulation from /home/amin/.tidy3d/pf_cache/BXP/ms_info-QV7FFLPWAPUP726RJBZKFKMQHOTNJGCCQRUP4BQRFZZLB23XRGGQ.json.
Starting sample 6 of 22…
Loading cached simulation from /home/amin/.tidy3d/pf_cache/FTZ/ms_info-ESHC25TZYSTT24XPF4LIL3GOFZPNYLQ3ECBFORJYEEXZULONP7XA.json.
Loading cached simulation from /home/amin/.tidy3d/pf_cache/62X/ms_info-33U55QQPMU57FSEVALKSDDCRWT5GAYUMPVVDFWRRQUNJRW3RQC7A.json.
Starting sample 7 of 22…
Loading cached simulation from /home/amin/.tidy3d/pf_cache/B3U/ms_info-J62AH2RJZW2WTULF5MG2U5GJ7KMQBDVYQD6DW6X3Z625TEICTC6Q.json.
Loading cached simulation from /home/amin/.tidy3d/pf_cache/RK2/ms_info-GD2L6GLGNV6LLZ6PGW62WCTFAMDQ2UBAWDC3URMKW2P5O63A2D6A.json.
Starting sample 8 of 22…
Loading cached simulation from /home/amin/.tidy3d/pf_cache/ZHS/ms_info-4MXBBLFACQECRM4M3DH2CNFOSTZEPWL5MHBFWW6POYQQJX4N22OQ.json.
Loading cached simulation from /home/amin/.tidy3d/pf_cache/MS3/ms_info-REO53MBWD3Y4C2YWJTRV6RMGJAIQPKKIVXOPFDBWDAAYRNYI6TVA.json.
Starting sample 9 of 22…
Loading cached simulation from /home/amin/.tidy3d/pf_cache/SKB/ms_info-EEARAAG6OGLXSTGBSTHLBYEOEUYHPUDB3TDL55AMQUX5YR2UYA4A.json.
Loading cached simulation from /home/amin/.tidy3d/pf_cache/VLL/ms_info-UDCVBMDJYQBJJWHQEBBRQOHNKCI3ZXYEVXHDD4RHRUCJ3D4YFGXQ.json.
Starting sample 10 of 22…
Loading cached simulation from /home/amin/.tidy3d/pf_cache/AAO/ms_info-TPD62U3NFCNIC6VIEM2F3ZD2NILAHHDBUETBJJVSMWEPBBPV5DHA.json.
Loading cached simulation from /home/amin/.tidy3d/pf_cache/DWL/ms_info-MHZ6MJ2OJHJQALY4L7FDHDW7CMTSUQORWOUVCXSPC7EKN7R24FCQ.json.
Starting sample 11 of 22…
Loading cached simulation from /home/amin/.tidy3d/pf_cache/K77/ms_info-NSQUU5XPFEJ5S6YW6P5ZKIPXSP2DSSU4FEFFW3TD3WBVHUGKR5RQ.json.
Loading cached simulation from /home/amin/.tidy3d/pf_cache/B6M/ms_info-N5C55PF5GJ2YKWEEAXNJKZCPEIB6CRO4ARHAX3UWMYVMG5NZ2A6Q.json.
Starting sample 12 of 22…
Loading cached simulation from /home/amin/.tidy3d/pf_cache/LCE/ms_info-UEUKE4TDULDOMLXPCR7OKSOKYVDKQJ5R6CW4TER6D7OMGTW5V3RQ.json.
Loading cached simulation from /home/amin/.tidy3d/pf_cache/R24/ms_info-PPWWCCUMXFKPNKPW6JSXLO523QXSORHMJZOGRCFBGCIJBZBPW4OA.json.
Starting sample 13 of 22…
Loading cached simulation from /home/amin/.tidy3d/pf_cache/6X5/ms_info-34EYBBLP47RRFOD5OIIVWZIZXN6DKCSVPQLSBVIT5PP5GSZJKRIA.json.
Loading cached simulation from /home/amin/.tidy3d/pf_cache/NXE/ms_info-SO4OC7JEYKVH4ZAKXXIAMNYNVQ7LIXTUVF6JV4RG6KK3X4IMRBKQ.json.
Starting sample 14 of 22…
Loading cached simulation from /home/amin/.tidy3d/pf_cache/6HR/ms_info-MOKC2LPPD4VK2JOUWDHJZPU6YMUPQ5XA4XNNWSJ46MRBDUPZUHSA.json.
Loading cached simulation from /home/amin/.tidy3d/pf_cache/67W/ms_info-YMIM32P5I56LKYN6KNDBEVLMDAGTFD2Q6SMIDPN2CLR2OV3SFRKQ.json.
Starting sample 15 of 22…
Loading cached simulation from /home/amin/.tidy3d/pf_cache/HYH/ms_info-CYPB2PT37FTTN6L4ML65PYPR2KAFIWKEVV2TVW7CUOQP5O2M5VIQ.json.
Loading cached simulation from /home/amin/.tidy3d/pf_cache/GSH/ms_info-K2CA46NBUKWBV4EF55JOJE5UZJ2TVXHNEWRYVZPJFGMBPL55NMHA.json.
Starting sample 16 of 22…
Loading cached simulation from /home/amin/.tidy3d/pf_cache/TE6/ms_info-YSWU436BBGHT5DBXMA7MCGGH4KMEM3TNSCRL75QI5EPGNY2WWDJA.json.
Loading cached simulation from /home/amin/.tidy3d/pf_cache/VU4/ms_info-4UDM6HRA6JZWVI73Y5KOKXULWLCVENL44K33Q6LFTX36ZCQUF7UA.json.
Starting sample 17 of 22…
Loading cached simulation from /home/amin/.tidy3d/pf_cache/E7E/ms_info-QZYQ5SU7Y3KMCAJYEXAYB22O5XD323YG6DDOPVF4FQ6UKLZRBCUQ.json.
Loading cached simulation from /home/amin/.tidy3d/pf_cache/FKG/ms_info-HWMRGA4OVEZARBQJBWDVAQNDUBVNWZUDAL55CMIZP2MGJBOTBL6Q.json.
Starting sample 18 of 22…
Loading cached simulation from /home/amin/.tidy3d/pf_cache/ZQL/ms_info-INA3J2IICKHVZMTAALGI6HORBKYZU5PBCAIPIYSZBUW55CSYX2CQ.json.
Loading cached simulation from /home/amin/.tidy3d/pf_cache/OSO/ms_info-7K7J327LOKG6FXHV6XWYG73LR6IN2KKQ7XQNC4BEOCVI5CNIVVQA.json.
Starting sample 19 of 22…
Loading cached simulation from /home/amin/.tidy3d/pf_cache/QR2/ms_info-B6UZJUK2LKKYSBXSGM7O4ADURZYIPW3C2BFKDWRK4VIGJCD6ABGA.json.
Loading cached simulation from /home/amin/.tidy3d/pf_cache/BP5/ms_info-FYCSNNMQ6JRN6IKDNRK7MMT32DJIBWBLVIABEP2R75TO2KKHFPOQ.json.
Starting sample 20 of 22…
Loading cached simulation from /home/amin/.tidy3d/pf_cache/ZHH/ms_info-DQVAGFR42ZNDAQXXS33J7ICNJQD2QBWBE3IO6QZTVYGBDESNPLPA.json.
Loading cached simulation from /home/amin/.tidy3d/pf_cache/WHI/ms_info-G4WJYGS7K4NSZIU25VOBRD4NHUXQJU2LDFMC55JSQPOXJ6OOMMQQ.json.
Starting sample 21 of 22…
Loading cached simulation from /home/amin/.tidy3d/pf_cache/2VD/ms_info-HDZO2JR2FAIK7UAIBHUQ2UAYAYL2YH3CQEUDS3NUP2WR6JRHYNTQ.json.
Loading cached simulation from /home/amin/.tidy3d/pf_cache/WN4/ms_info-GL2R2UIDR26QI7AQOZ3KHK2TRWT53MHNPUIJ2FXLEZGO6524SWOQ.json.
Starting sample 22 of 22…
Loading cached simulation from /home/amin/.tidy3d/pf_cache/LSP/ms_info-IG3GQPSAD2RSOG2DJCH3TCFQGPCDWFZRHWXZL3HJLGEEADP55PTQ.json.
Loading cached simulation from /home/amin/.tidy3d/pf_cache/JTX/ms_info-P76645FACBWPCG26345BAIEZGRFG5TQ6F4EMW3GFXYGQ3633E46A.json.
Sample 1 done.
Sample 2 done.
Sample 3 done.
Sample 4 done.
Sample 5 done.
Sample 6 done.
Sample 7 done.
Sample 8 done.
Sample 9 done.
Sample 10 done.
Sample 11 done.
Sample 12 done.
Sample 13 done.
Sample 14 done.
Sample 15 done.
Sample 16 done.
Sample 17 done.
Sample 18 done.
Sample 19 done.
Sample 20 done.
Sample 21 done.
Sample 22 done.
All samples done!
We now plot the transmission spectra from port P0 (input) to port P3 (drop port) for all Monte Carlo samples.
Each curve represents one possible fabricated instance of the 2-RR filter, allowing us to observe the spread in resonance wavelength and insertion loss due to process variations.
[6]:
fig, ax = plt.subplots(1, 1)

for *_, s_matrix in results:
    ax.plot(
        s_matrix.wavelengths,
        20 * np.log10(np.abs(s_matrix[("P0@0", "P3@0")])),
        alpha=0.5,
        color="tab:orange",
    )

_ = ax.set(ylim=(-20, 0), xlabel="λ (µm)", ylabel="Transmission (dB)")
../_images/examples_Fabrication_Aware_Simulation_11_0.png

Discussion of Fabrication-Aware Results

The Monte Carlo simulation shows that fabrication variations can significantly impact the spectral response of the 2-RR filter:

  • Resonance wavelength drift:
    The resonance positions vary substantially between samples, indicating that small deviations in silicon thickness, sidewall angle, or coupling gaps lead to noticeable phase shifts and effective index changes.
    This explains why thermal tuning (via integrated heaters) is almost always required in fabricated ring filters to fine-tune or align the resonance wavelengths post-fabrication.
  • Insertion loss variation:
    The simulated through-port transmission fluctuates between approximately 0.3 dB and 1.0 dB, demonstrating that while loss performance changes slightly, the filter remains fairly robust to process deviations in this respect.

Overall, the analysis illustrates how PhotonForge’s fabrication-aware modeling can predict both resonance misalignment and performance spread, allowing designers to anticipate tuning requirements and optimize yield.

Correlation Analysis

Monte Carlo analysis allows for the extraction of correlations between a device’s response and its input variables. This section demonstrates the correlation analysis process. Critically, accurate results with four input variables require hundreds of data points. The findings presented here are, therefore, purely illustrative and not statistically significant.

Feature extraction

In this section we extract three filter features from each simulated S-matrix so we can study their correlation with fabrication variables. We target the resonance closest to a reference wavelength \(\lambda_0\) (default \(1.55 \mu\)m) and compute:

  • Center wavelength \(\lambda_c\): the midpoint between the two 3-dB crossing wavelengths around the chosen resonance.

  • Insertion loss IL (in dB): transmission value at the center wavelength.

  • 3-dB bandwidth \(\Delta\lambda_{3,\text{dB}}\): difference between the right and left 3-dB crossing wavelengths.

[7]:
def extract_filter_features(s_matrix, target_wavelength=1.55):
    """
    Extracts key features (insertion loss, center wavelength, bandwidth)
    from an optical filter's S-matrix data.

    This function identifies the resonance dip closest to a target wavelength
    and calculates its characteristics.

    Args:
        s_matrix: The S-matrix object from the simulation.
        target_wavelength (float): The wavelength (in µm) around which to
                                   search for the primary resonance.
                                   Defaults to 1.55 µm.

    Returns:
        dict: A dictionary containing the calculated 'center_wavelength',
              'insertion_loss' (in dB), and 'bandwidth' (in µm).
              Returns None if no resonance is found.
    """
    wavelengths = s_matrix.wavelengths
    # Calculate transmission in dB
    transmission_db = 20 * np.log10(np.abs(s_matrix[("P0@0", "P3@0")]))

    # The 'prominence' parameter helps ignore minor ripples and noise.
    peak_indices, _ = find_peaks(transmission_db, prominence=1)

    if peak_indices.size == 0:
        print("Warning: No resonance peaks found.")
        return None

    # Find the peak/resonance closest to the target wavelength
    resonant_wavelengths = wavelengths[peak_indices]
    closest_peak_index = peak_indices[np.argmin(np.abs(resonant_wavelengths - target_wavelength))]

    # --- 1. Calculate Maximum Transmission Wavelength and Insertion Loss ---
    max_wavelength = wavelengths[closest_peak_index]
    insertion_loss = transmission_db[closest_peak_index]

    # --- 2. Calculate 3-dB Bandwidth ---
    # The 3-dB level is 3 dB below the maximum transmission value (the IL)
    three_db_level = insertion_loss - 3

    # Find where the transmission crosses the 3-dB level on both sides of the resonance
    # Split the transmission data at the resonance point
    left_side = transmission_db[:closest_peak_index]
    right_side = transmission_db[closest_peak_index:]

    # Find the indices where the transmission is greater than the 3-dB level
    left_indices_below_3db = np.where(left_side < three_db_level)[0]
    right_indices_below_3db = np.where(right_side < three_db_level)[0]

    if left_indices_below_3db.size == 0 or right_indices_below_3db.size == 0:
         bandwidth = np.nan # Use NaN for cases where bandwidth can't be found
    else:
        # Get the points just before and after the crossing
        left_cross_idx = left_indices_below_3db[-1]
        right_cross_idx = right_indices_below_3db[0] + closest_peak_index

        # Interpolate to find the exact wavelength at the 3dB crossing for better accuracy
        w_left = np.interp(three_db_level,
                           [transmission_db[left_cross_idx], transmission_db[left_cross_idx + 1]],
                           [wavelengths[left_cross_idx], wavelengths[left_cross_idx + 1]])

        w_right = np.interp(three_db_level,
                            [transmission_db[right_cross_idx - 1], transmission_db[right_cross_idx]],
                            [wavelengths[right_cross_idx - 1], wavelengths[right_cross_idx]])

        # Compute bandwidth and center wavelength from crossings
        bandwidth = w_right - w_left
        center_wavelength = (w_right + w_left)/2
        insertion_loss = transmission_db[(left_cross_idx+right_cross_idx)//2]

    return {
        "center_wavelength": center_wavelength,
        "insertion_loss": insertion_loss,
        "bandwidth": bandwidth,
    }

Correlation Matrix

In this section we take the Monte Carlo results and visually explore how each fabrication parameter affects the main filter features.
We generate a grid of scatter plots, one for each input–output pair, annotated with the Pearson correlation coefficient (R).

Interpretation

  • R ≈ +1: Strong positive correlation — as the fabrication parameter increases, the feature value increases.

  • R ≈ -1: Strong negative correlation — the feature decreases as the parameter increases.

  • R ≈ 0: Weak or no linear correlation.

This visualization allows us to directly identify which fabrication tolerances most strongly impact spectral behavior such as resonance shift, insertion loss, or bandwidth broadening.

Important note: The correlation results shown here are intended for demonstration only and are not statistically valid because of small number of data points.

[8]:

def run_correlation_analysis(variables, simulation_results, target_wavelength=1.55): """ Processes Monte Carlo simulation results to visualize the relationship and correlation between each input variable and each output filter feature. Args: variables (list): The list of RandomVariable objects from the simulation. simulation_results (list): The list of result tuples from the simulation. Each tuple contains the input values and the s_matrix. target_wavelength (float): The target wavelength in micrometers. """ data = [] variable_names = [v.name for v in variables] print("Processing simulation results...") for result in simulation_results: s_matrix = result[-1] input_values = result[:-1] features = extract_filter_features(s_matrix, target_wavelength=target_wavelength) if features: run_data = {name: val for name, val in zip(variable_names, input_values)} run_data.update(features) data.append(run_data) if not data: print("Could not extract features from any simulation run. Aborting.") return print(f"Successfully processed {len(data)} data points.") df = pd.DataFrame(data) df['center_wavelength'] *= 1e3 df['bandwidth'] *= 1e3 df = df.rename(columns={ 'center_wavelength': 'center_wavelength_nm', 'bandwidth': 'bandwidth_nm' }) print("\n--- Data Head ---") print(df.head()) output_feature_names = ['insertion_loss', 'center_wavelength_nm', 'bandwidth_nm'] print("\n--- Generating Custom Scatter Plot Grid with Correlation Values ---") n_outputs = len(output_feature_names) n_inputs = len(variable_names) fig, axes = plt.subplots(n_outputs, n_inputs, figsize=(10, 8), sharex='col', sharey='row') if n_outputs == 1 or n_inputs == 1: axes = np.array(axes).reshape(n_outputs, n_inputs) for i, out_feature in enumerate(output_feature_names): for j, in_variable in enumerate(variable_names): ax = axes[i, j] ax.scatter(df[in_variable], df[out_feature], alpha=0.6, s=30, edgecolor='k', linewidth=0.5) corr = df[in_variable].corr(df[out_feature]) corr_text = f"R = {corr:.2f}" ax.text( 0.05, 0.95, corr_text, transform=ax.transAxes, fontsize=12, verticalalignment='top', bbox=dict(boxstyle='round,pad=0.3', facecolor='wheat', alpha=0.5) ) if j == 0: ax.set_ylabel(out_feature, fontsize=12) if i == n_outputs - 1: ax.set_xlabel(in_variable, fontsize=12) ax.tick_params(axis='x', rotation=45) ax.grid(True, linestyle='--', alpha=0.6) fig.suptitle('Output Features vs. Input Variables', fontsize=20) plt.tight_layout(rect=[0, 0.03, 1, 0.97]) plt.show() run_correlation_analysis(var, results)
Processing simulation results...
Successfully processed 22 data points.

--- Data Head ---
   sidewall_angle  core_thickness  ring_coupling_distance  \
0        5.000000        0.225880                0.829600
1        0.000000        0.214120                0.790400
2        4.517759        0.218700                0.817602
3        4.244655        0.219547                0.792679
4        2.785838        0.219909                0.808466

   bus_coupling_distance  center_wavelength_nm  insertion_loss  bandwidth_nm
0               0.719600           1551.289951       -0.484303      0.990097
1               0.680400           1553.650351       -0.523616      1.589298
2               0.699238           1549.683240       -0.377694      1.193520
3               0.713226           1550.297463       -0.751649      1.435075
4               0.703479           1548.130114       -0.277430      1.289771

--- Generating Custom Scatter Plot Grid with Correlation Values ---
../_images/examples_Fabrication_Aware_Simulation_16_1.png