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 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¶
[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]:
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%
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…
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!
[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.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 ---