Building Circuit Models from Layout Files¶
In this notebook, we will see how to load GDSII layouts and run both component- and circuit-level simulations. This allows designers to import GDSII files generated from other software tools and evaluate their optical responses.
With a simulation model, we can see how fabrication variations (such as sidewall angles, layer thicknesses, refractive indices, etc.) impact circuit-level performance.
In this demo, we’ll learn how to:
Import hierarchical designs from GDSII files;
Detect ports and assign component models;
Create circuit models; and
Update the fabrication parameters to see impact on the circuit level S matrix.
We begin by first loading libraries for PhotonForge, Tidy3D and the open-source SiEPIC OpenEBL PDK, as well as and others we will require. To install the SiEPIC PDK please follow these steps.
[1]:
import matplotlib.pyplot as plt
import numpy as np
import photonforge as pf
import photonforge.live_viewer as live_viewer
import siepic_forge as siepic_pdk
import tidy3d as td
Assign open-source Siepic PDK as our default technolgy and set some configurations.
[2]:
tech = siepic_pdk.ebeam()
pf.config.default_technology = tech
pf.config.default_mesh_refinement = 12 # Decreased accuracy only to run the example
pf.config.svg_labels = False
Let’s begin by loading the GDSII file external_mzi.gds using the load_layout function. This file was created using an open-source PDK software package for fabrication in SiEPIC foundry, meaning the layer-stacks and PDK components adhere to the specifications provided by the foundry.
We can inspect the data, which is loaded as a dictionary of components (GDSII cells) indexed by name.
[3]:
gds_data = pf.load_layout("external_mzi.gds")
for component_name in gds_data.keys():
print(component_name)
straight_L100_N2_CSstrip
delayline_DL0
delayline_DL200
bend_euler_CSstrip_A180
bend_euler_CSstrip
straight_L20p358_N2_CSx_23d87c0c
straight_L58p878_N2_CSx_33fc2739
straight_L17p043_N2_CSx_3c6d3aa8
ebeam_y_1550
gc_te1550_broadband
mzi_DL200
mzi_with_gratings_DL200
We are interested in creating a simulation model for the unbalanced MZI with grating couplers.
[4]:
top_level = pf.find_top_level(*gds_data.values())
for component in top_level:
print(component.name)
mzi_with_gratings_DL200
[5]:
mzi_circuit = top_level[0]
mzi_circuit
[5]:
[6]:
type(mzi_circuit)
[6]:
photonforge.Component
If we want to inspect the device geometry carefully, we can use the browser-based layout viewer.
[7]:
from photonforge.live_viewer import LiveViewer
viewer = LiveViewer()
LiveViewer started at http://localhost:32771
[8]:
viewer.display(mzi_circuit)
[8]:
We can also visualize all hierarchical nature of how this GDSII file was created.
Feel free to set interactive = True to visualize the components in this heirarchy. We see several independent components such as ebeam_y_1550, gc_te1550_broadband, and bend_euler_CSstrip that make up the circuit.
[9]:
mzi_circuit.tree_view(interactive=False)
[9]:
├─ mzi_DL200
│ ├─ ebeam_y_1550
│ ├─ delayline_DL200
│ │ ├─ bend_euler_CSstrip
│ │ ├─ straight_L100_N2_CSstrip
│ │ └─ bend_euler_CSstrip_A180
│ └─ delayline_DL0
│ ├─ bend_euler_CSstrip
│ └─ bend_euler_CSstrip_A180
├─ gc_te1550_broadband
├─ bend_euler_CSstrip
├─ straight_L17p043_N2_CSx_3c6d3aa8
├─ straight_L58p878_N2_CSx_33fc2739
└─ straight_L20p358_N2_CSx_23d87c0c
GDSII files contains only layout information. In order to build a circuit model, we will identify all components with no dependencies (leaves in the dependency tree) and specify their ports and models to calculate their individual S parameters. PhotonForge will then use the physical connections between them to build the circuit model.
Let us start by looking at all the dependencies of the mzi_circuit component. To identify independent components we need to find the ones that have no further references (references in PhotonForge are like instances of components).
[10]:
# Gather all components with no dependencies (no references to other components)
independent_components = [
c for c in mzi_circuit.dependencies() if len(c.references) == 0
]
# Sort the list by name
independent_components.sort(key=lambda c: c.name)
print("Independent components:")
for i, component in enumerate(independent_components):
print(f"- [{i}] {component.name}")
Independent components:
- [0] bend_euler_CSstrip
- [1] bend_euler_CSstrip_A180
- [2] ebeam_y_1550
- [3] gc_te1550_broadband
- [4] straight_L100_N2_CSstrip
- [5] straight_L17p043_N2_CSx_3c6d3aa8
- [6] straight_L20p358_N2_CSx_23d87c0c
- [7] straight_L58p878_N2_CSx_33fc2739
Sub-component setup¶
Now depending on the type of the component we are looking at we might have to add different ports and models. For example, for gc_te1550_broadband, we want to add a data model and an additional Gaussian port. For ebeam_y_1550 we might want to run FDTD simulations. For straight waveguides (or bends with insignificant loss) we might want a semi-analytical waveguide
model.
We will add ports and models to the independent components one by one to show a few details in the process. Later, this process can be completely automated, if needed, based on the expected GDSII contents.
Component 0¶
[11]:
component = independent_components[0]
viewer.display(component)
[11]:
By inspecting the component in the live viewer, we see that the routes correspond to a 500 nm wide silicon strip waveguide. The PDK provides the default port specification “TE_1550_500” that matches this profile, so we will be using that to find ports. Note that we can easily inspect the ports of any technology.
[12]:
port_spec = tech.ports["TE_1550_500"]
wg_width, _ = port_spec.path_profile_for((1, 0))
wg_width
[12]:
0.5
[13]:
# Automatically detect ports
edge_ports = component.detect_ports([port_spec], on_boundary="xy")
for port in edge_ports:
print(port)
Port at (0, 0) at 0 deg with spec "Strip TE 1550 nm, w=500 nm"
Port at (10, 10) at 270 deg with spec "Strip TE 1550 nm, w=500 nm"
[14]:
# Add ports
component.add_port(edge_ports)
viewer.display(component)
[14]:
For 2-port routes where we don’t need to include bend losses, the waveguide model can be used to efficiently compute the S parameters from a single mode-solving calculation and the route length. Otherwise, full FDTD models can always be used to completely characterize the geometry.
[15]:
# Use the area of the waveguide polygon in the layer (1,0) and waveguide width to estimate the route length
area = sum(s.area() for s in component.structures[1, 0])
length = area / wg_width
component.add_model(pf.WaveguideModel(length=length), "Waveguide")
print(f"The length of this path is {length:.3f} μm")
The length of this path is 16.637 μm
Component 1¶
The second component is very similar to the first we processed.
[16]:
component = independent_components[1]
viewer.display(component)
[16]:
[17]:
# We can limit the search for port to a single boundary
edge_ports = component.detect_ports([port_spec], on_boundary="-x")
for port in edge_ports:
print(port)
Port at (0, 0) at 0 deg with spec "Strip TE 1550 nm, w=500 nm"
Port at (0, 20) at 0 deg with spec "Strip TE 1550 nm, w=500 nm"
[18]:
# Add ports
component.add_port(edge_ports)
# Add model
length = sum(s.area() for s in component.structures[1, 0]) / wg_width
component.add_model(pf.WaveguideModel(length=length), "Waveguide")
print(f"The length of this route is {length:.3f} μm")
viewer.display(component)
The length of this route is 42.817 μm
[18]:
Component 2¶
[19]:
component = independent_components[2]
viewer.display(component)
[19]:
Because the port on the -x side has a rectangular box around the actual port edge, looking for ports at the exact boundaries will not work, so we add a 200 nm margin to the search:
[20]:
edge_ports = component.detect_ports([port_spec], on_boundary="x", boundary_margin=0.2)
for port in edge_ports:
print(port)
Port at (-7.4, 0) at 0 deg with spec "Strip TE 1550 nm, w=500 nm"
Port at (7.4, -2.75) at 180 deg with spec "Strip TE 1550 nm, w=500 nm"
Port at (7.4, 2.75) at 180 deg with spec "Strip TE 1550 nm, w=500 nm"
[21]:
# Add ports
component.add_port(edge_ports)
# Add Tidy3D model
component.add_model(pf.Tidy3DModel(port_symmetries=[("P0", "P2", "P1")]), "Tidy3D")
viewer.display(component)
[21]:
Component 3¶
[22]:
component = independent_components[3]
viewer.display(component)
[22]:
[23]:
edge_ports = component.detect_ports([port_spec], on_boundary="x", boundary_margin=0.2)
component.add_port(edge_ports)
viewer.display(component)
[23]:
The free-space port for the grating coupler is best modeled by a Gaussian port, which can be used to accurately compute the S matrix based on an Tidy3D model. Alternatively, if we have the data available, a data model can be used as in the foundry PIC design example.
If we are not really interested in adding the effects of the grating coupler, a simpler alternative is to add another waveguide port (even if the geometry does not match it) and an analytical 2-port model, as we do below.
[24]:
# Dummy waveguide port
component.add_port(
pf.Port(component["P0"].center + (30, 0), input_direction=180, spec=port_spec)
)
component.add_model(pf.TwoPortModel(), "Analytical")
viewer.display(component)
[24]:
Components 4 – 7¶
For the rest of the components which are simply straight waveguides, we add semi-analytical models again. For straight waveguides, PhotonForge automatically calculates the length so we do not need to explicitly specific length argument when specifying WaveguideModel.
[25]:
for component in independent_components[4:]:
edge_ports = component.detect_ports([port_spec], on_boundary="x")
component.add_port(edge_ports)
component.add_model(pf.WaveguideModel(), "WaveguideModel")
[26]:
viewer.display(independent_components[7])
[26]:
Now let’s look at the overall circuit. We can enable reference port drawing to see that all connections are accounted for.
[27]:
pf.config.svg_reference_ports = True
viewer.display(mzi_circuit)
[27]:
We can see that, once ports are defined for individual components, PhotonForge detects the connections, which we can see by inspecting the netlist. The warnings tell us that there are still dangling ports, but that’s expected, because we are not done yet: we did not add ports to the intermediate-level components.
[28]:
mzi_circuit.get_netlist()["connections"]
/tmp/ipykernel_541148/3086015821.py:1: RuntimeWarning: These ports are not connected and will be ignored: 'P1' at (-34.443, -98.878) from reference to 'gc_te1550_broadband', 'P1' at (-7.4, 0) from reference to 'straight_L17p043_N2_CSx_3c6d3aa8', 'P1' at (92.558, -98.878) from reference to 'gc_te1550_broadband', 'P1' at (62.2, 0) from reference to 'straight_L20p358_N2_CSx_23d87c0c'.
mzi_circuit.get_netlist()["connections"]
[28]:
[((6, 'P0', 1), (8, 'P0', 1)),
((6, 'P1', 1), (7, 'P0', 1)),
((3, 'P0', 1), (5, 'P0', 1)),
((3, 'P1', 1), (4, 'P0', 1)),
((2, 'P0', 1), (8, 'P1', 1)),
((1, 'P0', 1), (5, 'P1', 1))]
Circuit ports and models¶
In our case we want the ports for the grating coupler references to be the final ports. Because the component has a hierarchical structure, we also need to add reference ports and models to each sub-component like mzi_DL200, delayline_DL200 and so on. The add_reference_ports function allows us to recursively add ports and circuit models to these hierarchical
sub-components.
[29]:
mzi_circuit.add_reference_ports(include_dependencies=True, add_model=pf.CircuitModel())
pf.config.svg_reference_ports = False
viewer.display(mzi_circuit)
[29]:
S-matrix computation¶
Now that ports and models are defined, all we need to do is to call s_matrix on the top-level component.
[30]:
wavelengths = np.linspace(1.545, 1.55, 101)
s_matrix_initial = mzi_circuit.s_matrix(pf.C_0 / wavelengths)
Uploading task 'P0@0…'
Uploading task 'P1@0…'
Uploading task 'Mode-StripTE1550nmw500nm…'
Uploading task 'Mode-StripTE1550nmw500nm…'
Uploading task 'Mode-StripTE1550nmw500nm…'
Uploading task 'Mode-StripTE1550nmw500nm…'
Uploading task 'Mode-StripTE1550nmw500nm…'
Uploading task 'Mode-StripTE1550nmw500nm…'
Uploading task 'Mode-StripTE1550nmw500nm…'
Uploading task 'Mode-StripTE1550nmw500nm…'
Uploading task 'P0@0…'
Uploading task 'P1@0…'
Starting task 'P1@0': https://tidy3d.simulation.cloud/workbench?taskId=fdve-2af2be9e-fc72-4262-820e-8a8b9d339389
Starting task 'P0@0': https://tidy3d.simulation.cloud/workbench?taskId=fdve-72b50c1f-2682-4432-89d5-3504499cd28e
Starting task 'Mode-StripTE1550nmw500nm': https://tidy3d.simulation.cloud/workbench?taskId=mo-3bd55eb4-02c9-40b6-8521-df6e7b4b7cb5
Starting task 'Mode-StripTE1550nmw500nm': https://tidy3d.simulation.cloud/workbench?taskId=mo-b4ea37c9-57d4-45d6-b865-2df5b3d106ad
Starting task 'Mode-StripTE1550nmw500nm': https://tidy3d.simulation.cloud/workbench?taskId=mo-1c8762c3-0468-40e4-af29-4d2c4185878e
Starting task 'Mode-StripTE1550nmw500nm': https://tidy3d.simulation.cloud/workbench?taskId=mo-1ed66d20-7075-463b-8f6c-fc433029d9bb
Starting task 'Mode-StripTE1550nmw500nm': https://tidy3d.simulation.cloud/workbench?taskId=mo-d2fc94ac-fb1a-45b8-a00e-c9be16154985
Starting task 'Mode-StripTE1550nmw500nm': https://tidy3d.simulation.cloud/workbench?taskId=mo-c4d38f58-952d-4ab5-99bc-35eb4d44420d
Starting task 'Mode-StripTE1550nmw500nm': https://tidy3d.simulation.cloud/workbench?taskId=mo-3f6ddef3-0513-412e-aacd-d5c92d5cdd1a
Starting task 'Mode-StripTE1550nmw500nm': https://tidy3d.simulation.cloud/workbench?taskId=mo-e97bea5c-42ef-4d4b-9db9-e04bc39a4887
Starting task 'P0@0': https://tidy3d.simulation.cloud/workbench?taskId=fdve-c91876cf-60b4-42f4-8b3c-a5d68ba75c94
Starting task 'P1@0': https://tidy3d.simulation.cloud/workbench?taskId=fdve-047b0c40-1072-435c-91dd-2c82e21322ea
Downloading data from 'Mode-StripTE1550nmw500nm'…
Downloading data from 'Mode-StripTE1550nmw500nm'…
Downloading data from 'Mode-StripTE1550nmw500nm'…
Downloading data from 'Mode-StripTE1550nmw500nm'…
Downloading data from 'Mode-StripTE1550nmw500nm'…
Downloading data from 'Mode-StripTE1550nmw500nm'…
Downloading data from 'Mode-StripTE1550nmw500nm'…
Downloading data from 'Mode-StripTE1550nmw500nm'…
Downloading data from 'P1@0'…
Downloading data from 'P1@0'…
Downloading data from 'P0@0'…
Downloading data from 'P0@0'…
Progress: 100%
[31]:
_ = pf.plot_s_matrix(s_matrix_initial, input_ports=["P0"])
Let us calculate the FSR for the MZI based on the group index of the routing waveguide and compare to the value we can extract from the S parameters.
[32]:
lda = 1.55
mode_solver = pf.port_modes(
port_spec, frequencies=[pf.C_0 / lda], mesh_refinement=40, group_index=True
)
mode_solver.data.to_dataframe()
Starting…
11:51:56 -03 Loading simulation from local cache. View cached task using web UI at 'https://tidy3d.simulation.cloud/workbench?taskId=mo-9390c659-7794- 4b2f-b881-1d1a2d7b5764'.
Progress: 100%
[32]:
| wavelength | n eff | k eff | loss (dB/cm) | TE (Ey) fraction | wg TE fraction | wg TM fraction | mode area | group index | dispersion (ps/(nm km)) | ||
|---|---|---|---|---|---|---|---|---|---|---|---|
| f | mode_index | ||||||||||
| 1.934145e+14 | 0 | 1.55 | 2.442662 | 0.0 | 0.0 | 0.983483 | 0.763836 | 0.817709 | 0.190931 | 4.186253 | 508.873435 |
[33]:
dl = 200
ng = mode_solver.data.n_group.isel(mode_index=0, f=0).item()
fsr = lda**2 / (ng * dl)
print(
f"Estimated FSR from group index calculation using mode solver is {fsr * 1e3:.2f} nm"
)
Estimated FSR from group index calculation using mode solver is 2.87 nm
We can get the FSR from the S parameters using scipy to find the transmission dips:
[34]:
from scipy.signal import find_peaks
# We can calculate FSR from the data
transmission = np.abs(s_matrix_initial["P0@0", "P1@0"]) ** 2
# Find dips by inverting the signal: adjust prominence and distance as needed
dips, properties = find_peaks(-transmission, prominence=0.01, distance=5)
# Plot original data
_, ax = plt.subplots(1, 1)
ax.plot(wavelengths, transmission, label="Transmission")
# Plot identified dips
for dip in dips:
ax.axvline(wavelengths[dip], ls="--", color="tab:gray")
ax.plot(wavelengths[dips], transmission[dips], "o", label="Identified dips")
ax.set(xlabel="Wavelength (µm)", ylabel="Transmission")
ax.legend()
fsr_s = wavelengths[dips[1]] - wavelengths[dips[0]]
print(f"FSR from S-matrix is: {fsr_s * 1e3:.2f} nm")
FSR from S-matrix is: 2.85 nm
Technology variations¶
During fabrication a number of parameters like the thickness of silicon, sidewall angle, and even material parameters may vary across the wafer. In order to study the impact of these parameters on the performance of the circuit, we can simply update the technology and re-run S-matrix computation.
[35]:
# Since the technology itself is parametric we can inspect what parameters we can update
for k, v in tech.parametric_kwargs.items():
s = str(v)
if len(s) > 16:
s = f"{v.__class__.__name__}(…)"
print(f"- {k} = {s}")
- si_thickness = 0.22
- si_slab_thickness = 0.09
- si_mask_dilation = 0.0
- si_slab_mask_dilation = 0.0
- sidewall_angle = 0.0
- heater_thickness = 0.2
- router_thickness = 0.6
- bottom_oxide_thickness = 2.0
- top_oxide_thickness = 2.2
- passivation_oxide_thickness = 0.3
- sio2 = dict(…)
- si = dict(…)
- router_metal = dict(…)
- heater_metal = dict(…)
- opening = Medium(…)
Now, let’s update the silicon thickness and see the impact of that on our s-matrix.
[36]:
_ = tech.update(si_thickness=0.215)
s_matrix_thin = mzi_circuit.s_matrix(pf.C_0 / wavelengths)
# It's a good idea to reset the value to the original, in case we forget later
_ = tech.update(si_thickness=0.2)
Uploading task 'P0@0…'
Uploading task 'P1@0…'
Uploading task 'Mode-StripTE1550nmw500nm…'
Uploading task 'Mode-StripTE1550nmw500nm…'
Uploading task 'Mode-StripTE1550nmw500nm…'
Uploading task 'Mode-StripTE1550nmw500nm…'
Uploading task 'Mode-StripTE1550nmw500nm…'
Uploading task 'Mode-StripTE1550nmw500nm…'
Uploading task 'Mode-StripTE1550nmw500nm…'
Uploading task 'Mode-StripTE1550nmw500nm…'
Uploading task 'P0@0…'
Uploading task 'P1@0…'
Starting task 'Mode-StripTE1550nmw500nm': https://tidy3d.simulation.cloud/workbench?taskId=mo-4d06ec97-f7bf-4e73-b44b-b7f7f1b7a72c
Starting task 'P0@0': https://tidy3d.simulation.cloud/workbench?taskId=fdve-04dce54c-6c30-41af-9a3e-181b2a7165a9
Starting task 'Mode-StripTE1550nmw500nm': https://tidy3d.simulation.cloud/workbench?taskId=mo-834224d1-f8a5-414e-8925-a1a09b46ed1a
Starting task 'Mode-StripTE1550nmw500nm': https://tidy3d.simulation.cloud/workbench?taskId=mo-f241060f-b159-4693-a5de-fda75fcd1650
Starting task 'Mode-StripTE1550nmw500nm': https://tidy3d.simulation.cloud/workbench?taskId=mo-4574310e-7d95-40de-a09b-2ef46baebe59
Starting task 'Mode-StripTE1550nmw500nm': https://tidy3d.simulation.cloud/workbench?taskId=mo-c75328ce-f340-4407-b1e5-e37d8c8ddec5
Starting task 'Mode-StripTE1550nmw500nm': https://tidy3d.simulation.cloud/workbench?taskId=mo-6ca3f44e-ba7e-4cd8-b7a7-af74de20e4fb
Starting task 'P1@0': https://tidy3d.simulation.cloud/workbench?taskId=fdve-656eb667-80e7-4f08-995c-8375b97412d0
Starting task 'Mode-StripTE1550nmw500nm': https://tidy3d.simulation.cloud/workbench?taskId=mo-96d70a4f-06c0-400f-b508-32f049bbfd62
Starting task 'Mode-StripTE1550nmw500nm': https://tidy3d.simulation.cloud/workbench?taskId=mo-43d44874-8428-486b-b085-007a9503767e
Starting task 'P0@0': https://tidy3d.simulation.cloud/workbench?taskId=fdve-fd8dfb9c-5ff0-4bc2-a95a-329c03106e1a
Starting task 'P1@0': https://tidy3d.simulation.cloud/workbench?taskId=fdve-b1075e05-71ff-4793-9945-c1c7fbac59c5
Downloading data from 'Mode-StripTE1550nmw500nm'…
Downloading data from 'Mode-StripTE1550nmw500nm'…
Downloading data from 'Mode-StripTE1550nmw500nm'…
Downloading data from 'Mode-StripTE1550nmw500nm'…
Downloading data from 'Mode-StripTE1550nmw500nm'…
Downloading data from 'Mode-StripTE1550nmw500nm'…
Downloading data from 'Mode-StripTE1550nmw500nm'…
Downloading data from 'Mode-StripTE1550nmw500nm'…
Downloading data from 'P0@0'…
Downloading data from 'P0@0'…
Downloading data from 'P1@0'…
Downloading data from 'P1@0'…
Progress: 100%
[37]:
transmission2 = np.abs(s_matrix_thin["P0@0", "P1@0"]) ** 2
_, ax = plt.subplots(1, 1)
ax.plot(wavelengths, transmission, label="Si thickness: 220 nm")
ax.plot(wavelengths, transmission2, label="Si thickness: 200 nm")
ax.set(xlabel="Wavelength (µm)", ylabel="Transmission")
_ = ax.legend()
As expected, changing silicon waveguide thinkness across the wafer even by 5 nm will dramatically impact the unbalanced MZM response. One can also perform Monte-Carlo simulations to look at how fabrication variations impact overall circuit performance.
Removing a delay arm¶
Now, let’s see what happens to the circuit performance if we delete one of the delay arms. We can use the tree view to get the reference numbers within each component:
[38]:
mzi_circuit.tree_view(by_reference=True)
[38]:
├─[0] mzi_DL200
│ ├─[0] ebeam_y_1550
│ ├─[1] delayline_DL200
│ │ ├─[0] bend_euler_CSstrip
│ │ ├─[1] straight_L100_N2_CSstrip
│ │ ├─[2] bend_euler_CSstrip_A180
│ │ ├─[3] straight_L100_N2_CSstrip
│ │ └─[4] bend_euler_CSstrip
│ ├─[2] delayline_DL0
│ │ ├─[0] bend_euler_CSstrip
│ │ ├─[1] bend_euler_CSstrip_A180
│ │ └─[2] bend_euler_CSstrip
│ └─[3] ebeam_y_1550
├─[1] gc_te1550_broadband
├─[2] gc_te1550_broadband
├─[3] bend_euler_CSstrip
├─[4] straight_L17p043_N2_CSx_3c6d3aa8
├─[5] straight_L58p878_N2_CSx_33fc2739
├─[6] bend_euler_CSstrip
├─[7] straight_L20p358_N2_CSx_23d87c0c
└─[8] straight_L58p878_N2_CSx_33fc2739
[39]:
# from the tree view, remove delayline_DL0 from mzi_DL200
mzi_circuit.references[0].component.remove(
mzi_circuit.references[0].component.references[2]
)
[39]:
[40]:
viewer.display(mzi_circuit)
[40]:
[41]:
s_matrix_new = mzi_circuit.s_matrix(pf.C_0 / wavelengths)
Starting…
/home/lucas/Flexcompute/dev/compute/src/src/photonforge/.venv/lib/python3.14/site-packages/photonforge/circuit_base.py:545: RuntimeWarning: These ports are not connected and will be ignored: 'P1' at (7.4, -2.75) from reference to 'ebeam_y_1550', 'P1' at (47.4, -2.75) from reference to 'ebeam_y_1550'.
netlist = component.get_netlist()
Uploading task 'P0@0…'
Uploading task 'P1@0…'
Uploading task 'Mode-StripTE1550nmw500nm…'
Uploading task 'Mode-StripTE1550nmw500nm…'
Uploading task 'Mode-StripTE1550nmw500nm…'
Uploading task 'Mode-StripTE1550nmw500nm…'
Uploading task 'Mode-StripTE1550nmw500nm…'
Uploading task 'Mode-StripTE1550nmw500nm…'
Uploading task 'Mode-StripTE1550nmw500nm…'
Uploading task 'P0@0…'
Uploading task 'P1@0…'
Starting task 'Mode-StripTE1550nmw500nm': https://tidy3d.simulation.cloud/workbench?taskId=mo-a8033593-816e-4cf0-8db7-0fd187d87b7a
Starting task 'Mode-StripTE1550nmw500nm': https://tidy3d.simulation.cloud/workbench?taskId=mo-bdf99686-c23f-47ef-8456-38e55a785027
Starting task 'Mode-StripTE1550nmw500nm': https://tidy3d.simulation.cloud/workbench?taskId=mo-9e60563a-54b5-44c5-bdb4-0573f52579da
Starting task 'P1@0': https://tidy3d.simulation.cloud/workbench?taskId=fdve-b1f4be72-d685-4abc-b969-948d8c0cfed0
Starting task 'Mode-StripTE1550nmw500nm': https://tidy3d.simulation.cloud/workbench?taskId=mo-4570102c-6299-408b-89d0-2af315f1eff9
Starting task 'Mode-StripTE1550nmw500nm': https://tidy3d.simulation.cloud/workbench?taskId=mo-b1e8c118-9b35-4cef-a088-d61029e0764a
Starting task 'P0@0': https://tidy3d.simulation.cloud/workbench?taskId=fdve-aa9efc80-fb43-4f86-8fd7-70385011c00c
Starting task 'Mode-StripTE1550nmw500nm': https://tidy3d.simulation.cloud/workbench?taskId=mo-f1932ae0-a77b-4ad6-828c-a83d55222b10
Starting task 'P0@0': https://tidy3d.simulation.cloud/workbench?taskId=fdve-5138623c-8b0e-4d1d-9100-92557aac5385
Starting task 'Mode-StripTE1550nmw500nm': https://tidy3d.simulation.cloud/workbench?taskId=mo-97c9f20b-78a1-4b69-bd01-cd39f0c7f4c0
Starting task 'P1@0': https://tidy3d.simulation.cloud/workbench?taskId=fdve-018dd2df-3fd1-40fc-ae57-cb91cf989842
Downloading data from 'Mode-StripTE1550nmw500nm'…
Downloading data from 'Mode-StripTE1550nmw500nm'…
Downloading data from 'Mode-StripTE1550nmw500nm'…
Downloading data from 'Mode-StripTE1550nmw500nm'…
Downloading data from 'Mode-StripTE1550nmw500nm'…
Downloading data from 'Mode-StripTE1550nmw500nm'…
Downloading data from 'Mode-StripTE1550nmw500nm'…
Downloading data from 'P1@0'…
Downloading data from 'P0@0'…
Downloading data from 'P0@0'…
Downloading data from 'P1@0'…
Progress: 100%
[42]:
transmission3 = np.abs(s_matrix_new["P0@0", "P1@0"]) ** 2
_, ax = plt.subplots(1, 1)
ax.plot(wavelengths, 10 * np.log10(transmission), label="Original")
ax.plot(wavelengths, 10 * np.log10(transmission3), label="Disconnected")
ax.set(xlabel="Wavelength (µm)", ylabel="Transmission (dB)")
_ = ax.legend()
As expected, we see about 6 dB loss from the disconnected splitter.