Ports¶
Ports are terminals through which electromagnetic waves can enter or exit a system. In PhotonForge, they are 2D surfaces used to connect a component to the external world. The S parameters of a device describes how the electromagnetic fields entering through its ports are transmitted within the component and exit through those same ports.
A technology in PhotonForge comes with a number of predefined port specifications, which describe the waveguide cross-section represented by that port.
Inspecting Ports¶
We will use the basic technology as an example of technology from a PDK. The ports defined in the technology can be easily inspected:
[1]:
import photonforge as pf
tech = pf.basic_technology()
pf.config.default_technology = tech
tech.ports
[1]:
{'Rib': PortSpec(description="Rib waveguide", width=2.16, limits=(-1, 1.22), num_modes=1, polarization="", target_neff=4, path_profiles={(2.4, 0, (1, 0)), (2.4, 0, (3, 0)), (0.4, 0, (2, 0))}),
'Strip': PortSpec(description="Strip waveguide", width=2.25, limits=(-1, 1.22), num_modes=1, polarization="", target_neff=4, path_profiles={(2.5, 0, (1, 0)), (0.5, 0, (2, 0))})}
As we can see, the basic technology comes with 2 predefined port specifications: “Strip” and “Rib”, corresponding to waveguides of different cross sections.
The specification width defines the lateral dimension of the waveguide cross-section, including the margins required for evanescent fields in dielectric waveguides. Similarly, the limits contain the lower and upper boundary coordinates for the cross-section in the out-of-plane (z) direction. The actual shapes and materials of the waveguide are
defined by its path profiles, which contain (width, offset, layer)
tuples. These tuples are used to generate straight path sections when generating the actual waveguide.
The extrusion profile for the waveguides can be quickly inspected through the tidy3d_plot function (only available for Tidy3D versions 2.7.1 and above):
[2]:
_ = pf.tidy3d_plot(tech.ports["Rib"])
Other attributes of the port specification define its supported modes. Computing those modes can be easily done though Tidy3D’s mode solver by exporting the specification to tidy3d:
[3]:
rib = tech.ports["Rib"]
freqs = [pf.C_0 / 1.55]
mode_solver = rib.to_tidy3d(freqs, mesh_refinement=40)
mode_solver.plot_field("E", mode_index=0, f=freqs[0], robust=False)
mode_solver.data.to_dataframe()
[3]:
wavelength | n eff | k eff | TE (Ey) fraction | wg TE fraction | wg TM fraction | mode area | ||
---|---|---|---|---|---|---|---|---|
f | mode_index | |||||||
1.934145e+14 | 0 | 1.55 | 2.40691 | 0.0 | 0.97718 | 0.81759 | 0.804347 | 0.206934 |
Custom Port Specification¶
If we need to use a custom waveguide profile, it’s easy to create a new port specification. For example, let’s add a multimode strip profile with an 800 nm core width, and a vertical slot waveguide. Note that we follow the technology defaults of including the cladding region surrounding the waveguide core in the “WG_CLAD” layer (1, 0)
, expanding beyond the port width.
[4]:
strip_mm = pf.PortSpec(
"Multimode strip waveguide",
width=2.5,
limits=tech.ports["Strip"].limits,
num_modes=4,
target_neff=4,
path_profiles={(2.6, 0, (1, 0)), (0.75, 0, (2, 0))},
)
mode_solver = strip_mm.to_tidy3d(freqs, mesh_refinement=40)
mode_solver.plot_field("E", mode_index=3, f=freqs[0], robust=False)
mode_solver.data.to_dataframe()
[4]:
wavelength | n eff | k eff | TE (Ey) fraction | wg TE fraction | wg TM fraction | mode area | ||
---|---|---|---|---|---|---|---|---|
f | mode_index | |||||||
1.934145e+14 | 0 | 1.55 | 2.699595 | 0.0 | 0.995523 | 0.877616 | 0.816402 | 0.200188 |
1 | 1.55 | 2.097767 | 0.0 | 0.953773 | 0.615339 | 0.865615 | 0.345752 | |
2 | 1.55 | 1.955900 | 0.0 | 0.033955 | 0.639355 | 0.920360 | 0.377286 | |
3 | 1.55 | 1.527858 | 0.0 | 0.174635 | 0.836501 | 0.648379 | 0.682208 |
[5]:
slot = pf.PortSpec(
"Slot waveguide",
width=2.1,
limits=tech.ports["Strip"].limits,
num_modes=1,
target_neff=4,
path_profiles={(2.2, 0, (1, 0)), (0.2, -0.15, (2, 0)), (0.2, 0.15, (2, 0))},
)
mode_solver = slot.to_tidy3d(freqs, mesh_refinement=40)
mode_solver.plot_field("E", mode_index=0, f=freqs[0], robust=False)
mode_solver.data.to_dataframe()
[5]:
wavelength | n eff | k eff | TE (Ey) fraction | wg TE fraction | wg TM fraction | mode area | ||
---|---|---|---|---|---|---|---|---|
f | mode_index | |||||||
1.934145e+14 | 0 | 1.55 | 1.687996 | 0.0 | 0.965267 | 0.871396 | 0.855663 | 0.158515 |
We can even add our new specifications to the technology, if we want to:
[6]:
tech.add_port("Strip MM", strip_mm)
tech.add_port("Slot", slot)
tech.ports
[6]:
{'Slot': PortSpec(description="Slot waveguide", width=2.1, limits=(-1, 1.22), num_modes=1, polarization="", target_neff=4, path_profiles={(2.2, 0, (1, 0)), (0.2, -0.15, (2, 0)), (0.2, 0.15, (2, 0))}),
'Strip MM': PortSpec(description="Multimode strip waveguide", width=2.5, limits=(-1, 1.22), num_modes=4, polarization="", target_neff=4, path_profiles={(0.75, 0, (2, 0)), (2.6, 0, (1, 0))}),
'Rib': PortSpec(description="Rib waveguide", width=2.16, limits=(-1, 1.22), num_modes=1, polarization="", target_neff=4, path_profiles={(2.4, 0, (1, 0)), (2.4, 0, (3, 0)), (0.4, 0, (2, 0))}),
'Strip': PortSpec(description="Strip waveguide", width=2.25, limits=(-1, 1.22), num_modes=1, polarization="", target_neff=4, path_profiles={(2.5, 0, (1, 0)), (0.5, 0, (2, 0))})}
Modifying Parametric Technologies¶
If a parametric technology is updated (in a Monte Carlo analysis, for example), the added port specifications will be lost. The proper way of adding a port specification to a pre-existing parametric technology is to wrap it:
[7]:
@pf.parametric_technology
def custom_basic_technology(*args, **kwargs):
technology = pf.basic_technology(*args, **kwargs)
slot = pf.PortSpec(
"Slot waveguide",
width=2.1,
limits=tech.ports["Strip"].limits,
num_modes=1,
target_neff=4,
path_profiles={(2.2, 0, (1, 0)), (0.2, -0.15, (2, 0)), (0.2, 0.15, (2, 0))},
)
technology.add_port("Slot", slot)
return technology
custom_tech = custom_basic_technology()
pf.config.default_technology = custom_tech
custom_tech.ports
[7]:
{'Slot': PortSpec(description="Slot waveguide", width=2.1, limits=(-1, 1.22), num_modes=1, polarization="", target_neff=4, path_profiles={(2.2, 0, (1, 0)), (0.2, -0.15, (2, 0)), (0.2, 0.15, (2, 0))}),
'Rib': PortSpec(description="Rib waveguide", width=2.16, limits=(-1, 1.22), num_modes=1, polarization="", target_neff=4, path_profiles={(2.4, 0, (1, 0)), (2.4, 0, (3, 0)), (0.4, 0, (2, 0))}),
'Strip': PortSpec(description="Strip waveguide", width=2.25, limits=(-1, 1.22), num_modes=1, polarization="", target_neff=4, path_profiles={(2.5, 0, (1, 0)), (0.5, 0, (2, 0))})}
[8]:
custom_tech.update(core_thickness=0.1)
mode_solver = custom_tech.ports["Slot"].to_tidy3d(freqs, mesh_refinement=40)
mode_solver.plot_field("E", mode_index=0, f=freqs[0], robust=False)
mode_solver.data.to_dataframe()
[8]:
wavelength | n eff | k eff | TE (Ey) fraction | wg TE fraction | wg TM fraction | mode area | ||
---|---|---|---|---|---|---|---|---|
f | mode_index | |||||||
1.934145e+14 | 0 | 1.55 | 1.477255 | 0.0 | 0.981416 | 0.95209 | 0.90721 | 0.743313 |
Waveguides¶
We can easily create a waveguide corresponding to a port specification using the get_paths function:
[9]:
strip = tech.ports["Slot"]
# Use a component for setting layers and visualization
connection = pf.Component("CONNECTION")
for layer, path in strip.get_paths((0, 0)):
path.segment((5, 0)).turn(90, 4).segment((9, 6))
connection.add(layer, path)
connection
[9]:
Adding Ports to a Component¶
To add a port to a component, we first need to create it by setting its position, orientation (input direction) and specification.
[10]:
port0 = pf.Port((0, 0), 0, "Slot")
# Set the port name to "P0"
connection.add_port(port0, "P0")
connection
[10]:
Another option is to use port auto-detection to look for possible ports in the component.
Auto-detection works by matching the required path profiled in the port specifications to the edges in the component geometry, which means:
There can be false positives if a port profile is very simple and it matches several edges in the component geometry.
Ports will not be found if, for example, a clad layer is required to surround the waveguide core, but only the core edge is found
[11]:
detected_ports = connection.detect_ports(["Slot"])
detected_ports
[11]:
[Port(center=(9, 6), input_direction=270, spec=PortSpec(description="Slot waveguide", width=2.1, limits=(-1, 1.22), num_modes=1, polarization="", target_neff=4, path_profiles={(2.2, 0, (1, 0)), (0.2, -0.15, (2, 0)), (0.2, 0.15, (2, 0))}), extended=True, inverted=False)]
Note that the auto-detection skipped the port at (0, 0) because it was already present in the component (it does not create duplicates).
[12]:
connection.add_port(detected_ports[0], "P1")
connection
[12]: