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]:
../_images/guides_Layout_Import_4_0.svg

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]:
../_images/guides_Layout_Import_7_0.svg

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]:
../_images/guides_Layout_Import_10_0.svg

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]:
../_images/guides_Layout_Import_13_0.svg
[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]:
../_images/guides_Layout_Import_14_0.svg
[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]:
../_images/guides_Layout_Import_15_0.svg
[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]:
../_images/guides_Layout_Import_16_0.svg
[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]:
../_images/guides_Layout_Import_17_0.svg

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%
../_images/guides_Layout_Import_19_1.png