Fabrication-Aware Simulation: Two Coupled Ring Resonator Filter¶
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
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 pandas as pd
import photonforge as pf
import tidy3d as td
from scipy.signal import find_peaks
# Reduce non-critical Tidy3D logs to keep the notebook output clean
td.config.logging_level = "ERROR"
Setting Up Technology and Simulation Defaults¶
[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,
model=pf.Tidy3DModel(
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]:
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"])
Uploading task 'Mode-Stripwaveguide…'
Uploading task 'Mode-Stripwaveguide…'
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-ecab13f5-8d42-4e61-9403-55db3f755a5f
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-6853e6e7-3126-490c-a067-7326827c6eb0
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Progress: 100%
Monte Carlo Analysis¶
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 σ
[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…
Uploading task 'Mode-Stripwaveguide…'
Uploading task 'Mode-Stripwaveguide…'
Starting sample 2 of 22…
Uploading task 'Mode-Stripwaveguide…'
Uploading task 'Mode-Stripwaveguide…'
Starting sample 3 of 22…
Uploading task 'Mode-Stripwaveguide…'
Uploading task 'Mode-Stripwaveguide…'
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-e2b0e041-e676-4a92-ab74-bd80ce1216cd
Starting sample 4 of 22…
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-92933d9c-4b8a-495a-824c-05dcd303a0dc
Uploading task 'Mode-Stripwaveguide…'
Uploading task 'Mode-Stripwaveguide…'
Starting sample 5 of 22…
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-27d4ec9a-14d7-45b0-9d36-bd805e1c03b6
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-3b7f4097-3e35-479a-8d3a-22d61d9c9733
Uploading task 'Mode-Stripwaveguide…'
Uploading task 'Mode-Stripwaveguide…'
Starting sample 6 of 22…
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-6d75292e-d93c-4ecf-90af-a1259d0f4964
Uploading task 'Mode-Stripwaveguide…'
Uploading task 'Mode-Stripwaveguide…'
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-40e79c44-310c-4b6e-be86-9962fdc67b1b
Starting sample 7 of 22…
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-f75b035e-2669-42cb-a307-3a784a1b8194
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-33a2f490-bdb1-4697-a335-be6bfe0c9c96
Uploading task 'Mode-Stripwaveguide…'
Uploading task 'Mode-Stripwaveguide…'
Starting sample 8 of 22…
Uploading task 'Mode-Stripwaveguide…'
Uploading task 'Mode-Stripwaveguide…'
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-da30bde5-8b5b-48c4-b8b0-a325c7293ac1
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-e92a017c-6034-4a6b-85f4-0c612208c9a1
Starting sample 9 of 22…
Uploading task 'Mode-Stripwaveguide…'
Uploading task 'Mode-Stripwaveguide…'
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-45294332-9efc-49f1-8259-7b5476bb86dd
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-b466a991-0091-459b-a018-54ed7fb23189
Starting sample 10 of 22…
Uploading task 'Mode-Stripwaveguide…'
Uploading task 'Mode-Stripwaveguide…'
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-2887f5b6-f501-4b4f-ad1d-4afa178d1d36
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-859a5fcb-729b-4625-a35e-097370c47a54
Starting sample 11 of 22…
Uploading task 'Mode-Stripwaveguide…'
Uploading task 'Mode-Stripwaveguide…'
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-2597e51c-3081-46cd-bd2f-e9ab8a6ef718
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-9b6afd06-7f76-4ec5-ba92-c2b4d7a21be4
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-02edbebf-8003-4ff5-9b36-8946a03331f3
Starting sample 12 of 22…
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-0ba735ad-0cbe-4f5e-8923-3fbe27b7c701
Uploading task 'Mode-Stripwaveguide…'
Uploading task 'Mode-Stripwaveguide…'
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-ef5597ca-0f2d-4950-9f0d-22a574bbdbb3
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-5763a08f-2bf1-431a-bde7-75261b16875c
Starting sample 13 of 22…
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-80de9502-736f-469a-9872-326431b6e918
Uploading task 'Mode-Stripwaveguide…'
Uploading task 'Mode-Stripwaveguide…'
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-d9a7c620-885d-4d31-9783-bf3cd446aacc
Starting sample 14 of 22…
Uploading task 'Mode-Stripwaveguide…'
Uploading task 'Mode-Stripwaveguide…'
Starting sample 15 of 22…
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-814f0f9c-60e0-4cd7-8017-1bddb1f443e9
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-23ef9512-3112-4db5-88ba-e2625e342255
Uploading task 'Mode-Stripwaveguide…'
Uploading task 'Mode-Stripwaveguide…'
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-1c7e4ff8-fa64-485a-9451-62079eef1f0c
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-1405e5d3-2e41-4b8b-88ca-ff5387622e3e
Starting sample 16 of 22…
Uploading task 'Mode-Stripwaveguide…'
Uploading task 'Mode-Stripwaveguide…'
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-0d4c8eea-954e-48cb-89a8-9a0df30afee8
Starting sample 17 of 22…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Uploading task 'Mode-Stripwaveguide…'
Uploading task 'Mode-Stripwaveguide…'
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-639f221d-0a4a-480a-94d4-be6580eb33b0
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-4d553bd9-6f48-4a36-8968-602ba4ddc198
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-cbe87e9a-828b-4873-91f2-8e4732e7cbea
Starting sample 18 of 22…
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-faaaa90e-9d59-4f43-bbaf-f670224c21af
Uploading task 'Mode-Stripwaveguide…'
Uploading task 'Mode-Stripwaveguide…'
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-4c2b6c85-f4be-4bb2-93b8-4e9d794bc1fd
Starting sample 19 of 22…
Uploading task 'Mode-Stripwaveguide…'
Uploading task 'Mode-Stripwaveguide…'
Starting sample 20 of 22…
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-5d68eaf6-b603-4bcf-8e9a-b8e6159c739c
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-22aef202-8a4a-4ca1-8aed-92cfa305d1f9
Uploading task 'Mode-Stripwaveguide…'
Uploading task 'Mode-Stripwaveguide…'
Starting sample 21 of 22…
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-ec53d5f9-dacc-449a-82b3-4099c88519d1
Uploading task 'Mode-Stripwaveguide…'
Uploading task 'Mode-Stripwaveguide…'
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-c5fe46b9-deea-41cc-8b74-35b004830f93
Downloading data from 'Mode-Stripwaveguide'…
Starting sample 22 of 22…
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-95102c5e-c4cc-4bb6-bcc8-9be9553c7c10
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-5d353ef7-3a5f-4f40-b2bf-2c4552cb0dd9
Uploading task 'Mode-Stripwaveguide…'
Uploading task 'Mode-Stripwaveguide…'
Downloading data from 'Mode-Stripwaveguide'…
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-22b5aab5-d2ae-4bf5-af4c-6faef9724b0b
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-679aac60-5b1e-45ff-8cb1-b074a5417914
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-b0df8bc1-86c9-4743-9724-ea92ecd0bc2d
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-d47b723b-ebd9-47e4-ab9f-29b2a3343061
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-fd05a739-dd4c-44af-bd3a-ad3048418dc4
Starting task 'Mode-Stripwaveguide': https://tidy3d.simulation.cloud/workbench?taskId=mo-f77bf29a-ba26-4b47-ae7d-dd2dc0465120
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Sample 9 done.
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Downloading data from 'Mode-Stripwaveguide'…
Sample 8 done.
Sample 1 done.
Sample 2 done.
Sample 3 done.
Sample 4 done.
Sample 6 done.
Sample 5 done.
Sample 7 done.
Sample 10 done.
Sample 11 done.
Sample 13 done.
Sample 12 done.
Sample 14 done.
Sample 16 done.
Sample 15 done.
Sample 17 done.
Sample 19 done.
Sample 18 done.
Sample 20 done.
Sample 22 done.
Sample 21 done.
All samples done!
[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)")
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¶
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.293547 -0.474482 0.982906
1 0.680400 1553.585414 -0.525308 1.579173
2 0.699238 1549.683433 -0.384280 1.193134
3 0.713226 1550.319278 -0.760337 1.461445
4 0.703479 1548.134406 -0.285147 1.281189
--- Generating Custom Scatter Plot Grid with Correlation Values ---