Writing a user-defined PCell in PhotoCAD

The following gpdk > examples > example_pcell_dataclass.py is an example of a custom RingResonator.

Import module

from typing import cast

from fnpcell import all as fp
from gpdk import all as pdk
from gpdk.technology import get_technology  # get TECH definition
from gpdk.technology.interfaces import CoreCladdingWaveguideType

First import the function modules that need to be used in the script, such as the fnpcell function module; RingResonator will call the parameter units in gpdk, so import gpdk.

Define RingResonator

class RingResonator(fp.PCell, band="C"):
    ring_radius: float = 5
    top_spacing: float = fp.PositiveFloatParam(default=0.2)
    bottom_spacing: float = fp.PositiveFloatParam(default=0.2)
    ring_type: CoreCladdingWaveguideType = fp.WaveguideTypeParam(type=CoreCladdingWaveguideType)
    top_type: CoreCladdingWaveguideType = fp.WaveguideTypeParam(type=CoreCladdingWaveguideType)
    bottom_type: CoreCladdingWaveguideType = fp.WaveguideTypeParam(type=CoreCladdingWaveguideType)
    port_names: fp.IPortOptions = fp.PortOptionsParam(count=4, default=["op_0", "op_1", "op_2", "op_3"])

    def _default_bottom_type(self):
        return get_technology().WG.FWG.C.WIRE

    def __post_pcell_init__(self):
        assert self.ring_radius > 0, "ring_radius must be > 0"

    def build(self):
        insts, elems, ports = fp.InstanceSet(), fp.ElementSet(), fp.PortSet()
        # fmt: off

        min_radius_of_type = cast(float, self.ring_type.BEND_CIRCULAR.radius_eff)  # type: ignore

        ring = self.ring_type(curve=fp.g.EllipticalArc(radius=self.ring_radius)).with_ports((None, None)).with_name("ring")
        insts += ring
        ring_core_width = self.ring_type.core_width
        ring_cladding_width = self.ring_type.cladding_width

        line_length = self.ring_radius * 2 + ring_cladding_width

        top_core_width = self.top_type.core_width
        top = pdk.Straight(name="top", length=line_length, waveguide_type=self.top_type, transform=fp.translate(-line_length / 2, self.ring_radius + self.top_spacing + top_core_width / 2 + ring_core_width / 2))
        insts += top
        ports += top["op_0"].with_name(self.port_names[0])
        ports += top["op_1"].with_name(self.port_names[3])
        bottom_core_width = self.bottom_type.core_width
        bottom = pdk.Straight(name="bottom", length=line_length, waveguide_type=self.bottom_type, transform=fp.translate(-line_length / 2, -(self.ring_radius + self.bottom_spacing + bottom_core_width / 2 + ring_core_width / 2)))
        insts += bottom
        ports += bottom["op_0"].with_name(self.port_names[1])
        ports += bottom["op_1"].with_name(self.port_names[2])

        # fmt: on
        return insts, elems, ports

Create a class called RingResonator and RingResonator will be used as a prefix for the name of the layout cell in the GDS file; fp.PCell then declares this to be a pcell via the module in fnpcell:

class RingResonator(fp.PCell, band="C")

Defines parameter ring_radius in the RingResonator() class; it is a positive floating point number and has a default value of 5:

ring_radius: float = 5

Define the upper waveguide width parameter top_spacing, default 0.2, and the lower waveguide width parameter bottom_spacing, default 0.2, for the device, respectively:

top_spacing: float = fp.PositiveFloatParam(default=0.2).as_field()
bottom_spacing: float = fp.PositiveFloatParam(default=0.2).as_field()

Define the waveguide type parameters for the ring and the upper and lower waveguides. The waveguide type will contain core_width, core_layer, cladding_layer, bend_radius:

ring_type: CoreCladdingWaveguideType = fp.WaveguideTypeParam(type=CoreCladdingWaveguideType).as_field()
top_type: CoreCladdingWaveguideType = fp.WaveguideTypeParam(type=CoreCladdingWaveguideType).as_field()
bottom_type: CoreCladdingWaveguideType = fp.WaveguideTypeParam(type=CoreCladdingWaveguideType,).as_field()

Defines the number of ports, default to 4; also given the four default names:

port_names: fp.IPortOptions = fp.PortOptionsParam(count=4, default=["op_0", "op_1", "op_2", "op_3"]).as_field()

Define the function that returns the default waveguide type:

def _default_bottom_type(self):
  return get_technology().WG.FWG.C.WIRE

Declare a function for the radius parameter, requiring that ring_radius must be greater than 0 or an error will be raised:

def __post_pcell_init__(self):
  assert self.ring_radius > 0, "ring_radius must be > 0"

Each pcell function takes transform as the last parameter, which is used to pass position information when generating the layout:

transform=fp.TransformParam()

Defining the build function to plot the component:

def build(self):

Create insts, elems, ports and using them as fixed templates:

insts, elems, ports = fp.InstanceSet(), fp.ElementSet(), fp.PortSet()

Define the minimum radius of the ring resonator:

min_radius_of_type = cast(float, self.ring_type.BEND_CIRCULAR.radius_eff)

Define the ring in the ring resonator as an instance of a waveguide, with parameters containing the waveguide name ring, the curve graph curve, where the curve graph is generated by means of an elliptic curve, and the radius parameter radius of the curve:

ring =self.ring_type(curve=fp.g.EllipticalArc(radius=self.ring_radius)).with_ports((None, None)).with_name("ring")

Instantiate the ring:

insts += ring

Define a set of parameters to obtain data on the dimensions of the graph in the ring resonator:

ring_core_width = self.ring_type.core_width
ring_cladding_width = self.ring_type.cladding_width
line_length = self.ring_radius * 2 + ring_cladding_width

Define the upper waveguide as an instance of a straight waveguide, the parameters of which include name, length, waveguide_type, and relative position coordinate transform:

top_core_width = self.top_type.core_width
top = pdk.Straight(name="top", length=line_length, waveguide_type=self.top_type, transform=fp.translate(-line_length / 2, self.ring_radius + self.top_spacing + top_core_width / 2 + ring_core_width / 2))

Use the upper waveguide top as an instance:

insts += top

Add the ports and their names at both ends of the upper waveguide:

ports += top["op_0"].with_name(self.port_names[0])
ports += top["op_1"].with_name(self.port_names[3])

Define the lower waveguide as an instance of a straight waveguide, the parameters of which include name, length, waveguide_type, and relative position coordinates transform:

bottom_core_width = self.bottom_type.core_width
bottom = pdk.Straight(name="bottom", length=line_length, waveguide_type=self.bottom_type, transform=fp.translate(-line_length / 2, -(self.ring_radius + self.bottom_spacing + bottom_core_width / 2 + ring_core_width / 2)))

Use lower waveguide bottom as an instance:

insts += bottom

Add the ports and their names at both ends of the lower waveguide:

ports += bottom["op_0"].with_name(self.port_names[1])
ports += bottom["op_1"].with_name(self.port_names[2])

Returns the ring, upper waveguide, lower waveguide instance, elements, and ports defined above:

return insts, elems, ports

Define RingResonator2

class RingResonator2(RingResonator, band="C"):
    ring_radius: float = fp.PositiveFloatParam(default=10)
    computed_value: float = field(init=False)
    computed_v2: float = 7

    def _default_bottom_type(self):
        return get_technology().WG.SWG.C.WIRE

    def __post_pcell_init__(self):
        self.computed_value = self.ring_radius * 2
        self.computed_v2 = 8

Define a class named RingResonator2, which inherits from the RingResonator class and can use all the RingResonator class’s parent public properties and methods, while restricting the type of all the bands in the class to C:

class RingResonator2(RingResonator, band="C")

Define the parameter ring_radius default to 10; parameter computed_value; parameter computed_v2 default to 7:

ring_radius: float = fp.PositiveFloatParam(default=10).as_field()
computed_value: float = field(init=False)
computed_v2: float = 7

Define functions that return the default lower waveguide type:

   def _default_bottom_type(self):
     return get_technology().WG.SWG.C.WIRE

Define built-in functions that assign values to the parameters ``computed_value`` and ``computed_v2``::

    def __post_pcell_init__(self):
       self.computed_value = self.ring_radius * 2
       self.computed_v2 = 8

Create examples(layout units) using classes RingResonator and RingResonator2

if __name__ == "__main__":
    from pathlib import Path
    gds_file = Path(__file__).parent / "local" / Path(__file__).with_suffix(".gds").name
    library = fp.Library()

    TECH = get_technology()

    # =============================================================
    # fmt: off
    r0 = RingResonator(ring_radius=60, ring_type=TECH.WG.FWG.C.WIRE, top_type=TECH.WG.FWG.C.WIRE, bottom_type=TECH.WG.MWG.C.WIRE)
    library += r0
    r1 = RingResonator(ring_type=TECH.WG.FWG.C.WIRE, top_type=TECH.WG.FWG.C.WIRE).translated(20, 0)
    library += r1
    r2 = RingResonator2(ring_type=TECH.WG.FWG.C.WIRE, top_type=TECH.WG.FWG.C.WIRE).translated(50, 0)
    library += r2
    # fmt: on
    # =============================================================
    fp.export_gds(library, file=gds_file)
    # fp.plot(library)

Set the path and filename where the script will be saved to generate the GDS file:

gds_file = Path(__file__).parent / "local" /Path(__file__).with_suffix(".gds").name

Call fp.Library() for the layouts of the device created by the class:

library = fp.Library()

Call get_technology() to obtain process information for the layout file:

TECH = get_technology()

Create three examples(devices r0, r1, r2) using classes RingResonator and RingResonator2, set the type of ring and upper and lower waveguides, and control the relative coordinates of the devices by means of the translated(x, y) method:

r0 = RingResonator(ring_radius=60, ring_type=TECH.WG.FWG.C.WIRE, top_type=TECH.WG.FWG.C.WIRE, bottom_type=TECH.WG.MWG.C.WIRE)
library += r0
 r1 = RingResonator(ring_type=TECH.WG.FWG.C.WIRE, top_type=TECH.WG.FWG.C.WIRE).translated(20, 0)
library += r1
r2 = RingResonator2(ring_type=TECH.WG.FWG.C.WIRE, top_type=TECH.WG.FWG.C.WIRE).translated(50, 0)
library += r2

Export the layout cells as GDS files:

fp.export_gds(library, file=gds_file)

GDS Layout

../_images/fnpcell_write_pcell1.png