Custom Parametric Components

Components usually depend on a large number of parameters, not only geometrical, but also material and fabrication-related. Working from Python, it is easy enough to create functions that return components based on (some of) those parameters.

However, PhotonForge enables a more fundamental type of parametric component that can be updated in place, i.e., without creating a new component. They are created by using the parametric_component decorator to a function returning a component.

[1]:
import photonforge as pf

pf.config.default_technology = pf.basic_technology()
[2]:
@pf.parametric_component
def parametric_coupler(*, port_spec, gap=0.05, length=3, radius=4):
    if isinstance(port_spec, str):
        port_spec = pf.config.default_technology.ports[port_spec]

    core_width, _ = port_spec.path_profile_for("WG_CORE")

    # If the returned component has no name, one is automatically set based on the parameters
    coupler = pf.Component()

    # Add straight section
    for layer, path in port_spec.get_paths((0, 0)):
        path.segment((2 * radius + length, 0))
        coupler.add(layer, path)

    # Add racetrack section
    for layer, path in port_spec.get_paths((0, core_width + gap + radius)):
        path.arc(180, 270, radius, euler_fraction=0.5)
        path.segment((length, 0), relative=True)
        path.arc(-90, 0, radius, euler_fraction=0.5)
        coupler.add(layer, path)

    # Add ports
    coupler.add_port(coupler.detect_ports([port_spec]))

    return coupler


coupler = parametric_coupler(port_spec="Strip")

print(coupler.name)
coupler
parametric_coupler_port_spec_Strip
[2]:
../_images/guides_Custom_Parametric_Components_2_1.svg
[3]:
coupler.update(port_spec="Rib", gap=0.12)

print(coupler.name)
coupler
parametric_coupler_gap_0_12_port_spec_Rib
[3]:
../_images/guides_Custom_Parametric_Components_3_1.svg

By updating the component in-place, if it is used as a sub-component in a larger system, the update function can be used to change all internally-used instances without having to rebuild the system (of course, if the update changes the sub-component geometry in ways that invalidate its connections to other sub-components, the whole system might need to be updated as well).

Keyword Arguments

It should be noted in our previous definition that we enforce the use of keyword arguments only when calling the parametric component function by using * as the first function argument. Keyword arguments are those that are set by name in a function call, for example, in print("Hello", end="\n"), the argument "Hello" is a positional argument, whereas end="\n" is a keyword argument. This distinction is important in parametric component functions because keyword arguments are remembered by the component, so that future calls to update don’t have to repeat arguments that don’t need to be updated.

The keyword arguments for a parametric component are stored in the parametric_kwargs attribute:

[4]:
print(coupler.parametric_kwargs)

coupler.update(radius=2)

print(coupler.parametric_kwargs)

coupler
{'port_spec': 'Rib', 'gap': 0.12}
{'port_spec': 'Rib', 'gap': 0.12, 'radius': 2}
[4]:
../_images/guides_Custom_Parametric_Components_6_1.svg

Saving and Loading

When a parametric component is stored in a phf file, it remains parametric. However, when the phf file is loaded, updating the component will only work if the original function is available in the current python session. That is because the component stores only its original function name, not the whole function (which would be a security breach when loading phf files).

[5]:
pf.write_phf("parametric_coupler.phf", coupler)

loaded = pf.load_phf("parametric_coupler.phf")["components"][0]

print(loaded.parametric_function)

loaded.update(length=1)
__main__.parametric_coupler
[5]:
../_images/guides_Custom_Parametric_Components_8_1.svg

If we artificially change the parametric function name in our component, we’ll see the error raised when the original function is not available. In this case, the “component registry” is an internal cache populated by the parametric_component decorator, which one of the reasons for using it in the first place.

[6]:
loaded.parametric_function = "invalid"

try:
    loaded.update()
except RuntimeError as e:
    print("Error:", e)
Error: Parametric component function not found in component registry.

Therefore, in order to share parametric components between sessions and among several users, it is a good practice to define them in a separate python file or module that can be imported when necessary (i.e. a personal library of parametric components). For example, when PhotonForge itself is imported, all internal parametric component functions are imported and registered. The same thing happens for PDK modules that define their own parametric component functions.