Importing a GDSII/OASIS Layout¶
Importing an existing GDSII or OASIS layout file is easy with PhotonForge. Each cell in the layout is loaded as a component and the hierarchical structure in the file is preserved as PhotonForge references.
One important aspect to notice is that GDSII and OASIS files do not include a standard way to specify the PDK they use. That means that we are responsible for loading the appropriate technology for the file we want to import.
In this example, we’ll load the GDSII file created in the MZI example. That file was created for use with the basic technology included in PhotonForge, but with a strip waveguide profile with 450 nm core, so we first set that as our default technology.
[1]:
import photonforge as pf
pf.config.default_technology = pf.basic_technology(strip_width=0.45)
With the technology set, loading the layout is done with the load_layout function, which returns a dictionary of components by name.
[2]:
# Load the gds file
gds_file = "../examples/MZI.gds"
components = pf.load_layout(gds_file)
print("Components:", list(components.keys()))
Components: ['DELAY_STRAIGHT_10', 'DELAY_ARM_10', 'COUPLER', 'BEND', 'DELAY_ARM_0', 'MZI']
[3]:
components["COUPLER"]
[3]:
If we don’t know which of the components are at the top-level in the cell hierarchy in the layout, the find_top_level function can provide that information.
[4]:
top_level = pf.find_top_level(*components.values())
print("Top level components:", [component.name for component in top_level])
Top level components: ['MZI']
[5]:
mzi = components["MZI"]
mzi
[5]:
Because we generated the default technology with the appropriate strip waveguide profile for this layout, adding ports is easy with port auto-detection:
[6]:
ports = mzi.detect_ports(["Strip"])
print("Detected ports:" + "\n".join([f" - {p}" for p in ports]))
Detected ports: - Port at (-4.25, -6.55) at 90 deg of type Strip waveguide
- Port at (-4.25, 0) at 270 deg of type Strip waveguide
- Port at (18.75, -6.55) at 90 deg of type Strip waveguide
- Port at (18.75, 0) at 270 deg of type Strip waveguide
[7]:
mzi.add_port(ports, "P")
mzi
[7]:
We can continue adding ports and models to all sub-components loaded from the layout to replicate the circuit simulation used in the original example.
[8]:
mzi.add_model(pf.CircuitModel(verbose=False), "Circuit")
[8]:
'Circuit'
[9]:
coupler = components["COUPLER"]
coupler.add_port(coupler.detect_ports(["Strip"]), "P")
assert len(coupler.ports) == 4
coupler.add_model(
pf.Tidy3DModel(
port_symmetries=[
("P0", "P1", {"P1": "P0", "P2": "P3", "P3": "P2"}),
("P0", "P2", {"P1": "P3", "P2": "P0", "P3": "P1"}),
("P0", "P3", {"P1": "P2", "P2": "P1", "P3": "P0"}),
],
verbose=False,
),
"Tidy3DModel",
)
coupler
[9]:
[10]:
arm_0 = components["DELAY_ARM_0"]
arm_0.add_port(arm_0.detect_ports(["Strip"]), "P")
assert len(arm_0.ports) == 2
arm_0.add_model(pf.CircuitModel(verbose=False), "CircuitModel")
arm_0
[10]:
[11]:
arm_10 = components["DELAY_ARM_10"]
arm_10.add_port(arm_10.detect_ports(["Strip"]), "P")
assert len(arm_10.ports) == 2
arm_10.add_model(pf.CircuitModel(verbose=False), "CircuitModel")
arm_10
[11]:
[12]:
bend = components["BEND"]
bend.add_port(bend.detect_ports(["Strip"]), "P")
assert len(bend.ports) == 2
bend.add_model(
pf.Tidy3DModel(port_symmetries=[("P0", "P1", {"P1": "P0"})], verbose=False), "Tidy3DModel"
)
bend
[12]:
[13]:
straight = components["DELAY_STRAIGHT_10"]
straight.add_port(straight.detect_ports(["Strip"]), "P")
assert len(straight.ports) == 2
straight.add_model(pf.WaveguideModel(verbose=False), "WaveguideModel")
straight
[13]:
We may check the netlists of the components with circuit models to make sure we connected all sub-components correctly. If any sub-component is disconnected or missing a model, we’ll get an error or warning when computing the scattering matrix.
[14]:
import numpy as np
s_matrix = mzi.s_matrix(pf.C_0 / np.linspace(1.5, 1.6, 51))
_ = pf.plot_s_matrix(s_matrix, input_ports=["P0"])
Starting...
Progress: 100%