Reticle assembly#

With gdsfactory you can easily go from a Component, to a Component sweep, to a top level Component full of subcomponents.

Test protocols#

To measure your reticle / die after fabrication you need to decide your test configurations. This includes things like:

  • Individual input and output fibers versus fiber array. I recommend fiber array for easier testing and higher throughtput, but also understand the flexibility of single fibers for some cases.

  • Fiber array pitch (127um or 250um) if using a fiber array.

  • Pad pitch for DC and RF high speed probes (100, 125, 150, 200um). Probe configuration (GSG, GS …)

  • Test layout for DC, RF and optical fibers.

To enable automatic testing you need to add labels to all the devices that you want to test. GDS labels are not fabricated and are only visible in the GDS file.

Lets show some different automatic labeling schemas:

  1. SiEPIC ubc Ebeam PDK schema, labels one of the grating couplers from the fiber array.

  2. add_label_yaml includes a YAML based Label with all testing information.

  3. EHVA automatic testers, include a Label component declaration as described in this doc

1. SiEPIC labels#

[1]:
import gdsfactory as gf
from gdsfactory.dft import add_label_yaml, add_label_ehva

gf.config.set_plot_options(show_subports=False)
2022-06-28 17:01:44.118 | INFO     | gdsfactory.config:<module>:52 - Load '/home/runner/work/gdsfactory/gdsfactory/gdsfactory' 5.11.4
[2]:
mmi = gf.components.mmi2x2()
mmi_te_siepic = gf.dft.add_fiber_array_siepic(component=mmi)
mmi_te_siepic
../_images/notebooks_07_mask_3_0.png
[2]:
mmi2x2_add_fiber_array__31af28aa: uid 4, ports ['vertical_te_00', 'vertical_te_01', 'vertical_te_02', 'vertical_te_03'], aliases [], 0 polygons, 1 references
[3]:
mmi_te_siepic.get_labels(depth=1)
[3]:
[Label("opt_in_TE_1530_device_YourUserName_(mmi2x2)-1-o1", (-23.69999999999999, 60.699999999999996), None, None, False, 10, 0)]

2. YAML labels#

[4]:
mmi = gf.components.mmi2x2()
mmi_te_yaml = gf.routing.add_fiber_array(mmi, get_input_labels_function=None)
add_label_yaml(component=mmi_te_yaml)
mmi_te_yaml
../_images/notebooks_07_mask_6_0.png
[4]:
mmi2x2_add_fiber_array_8d6929d2: uid 23, ports ['vertical_te_00', 'vertical_te_01', 'vertical_te_02', 'vertical_te_03', 'vertical_te_10', 'vertical_te_20'], aliases [], 0 polygons, 50 references
[5]:
mmi_te_yaml.get_labels(depth=0)
[5]:
[Label("component_name: mmi2x2_add_fiber_array_8d6929d2
 polarization: None
 wavelength: None
 settings:
 ports:
   vertical_te_20:
     name: vertical_te_20
     width: 11.0
     midpoint:
     - 320.3
     - -80.96900000000001
     orientation: 270.0
     layer:
     - 203
     - 0
     port_type: vertical_te

   vertical_te_03:
     name: vertical_te_03
     width: 11.0
     midpoint:
     - 193.3
     - -80.96900000000001
     orientation: 270.0
     layer:
     - 203
     - 0
     port_type: vertical_te

   vertical_te_02:
     name: vertical_te_02
     width: 11.0
     midpoint:
     - 66.30000000000001
     - -80.96900000000001
     orientation: 270.0
     layer:
     - 203
     - 0
     port_type: vertical_te

   vertical_te_01:
     name: vertical_te_01
     width: 11.0
     midpoint:
     - -60.699999999999996
     - -80.96900000000001
     orientation: 270.0
     layer:
     - 203
     - 0
     port_type: vertical_te

   vertical_te_00:
     name: vertical_te_00
     width: 11.0
     midpoint:
     - -187.7
     - -80.96900000000001
     orientation: 270.0
     layer:
     - 203
     - 0
     port_type: vertical_te

   vertical_te_10:
     name: vertical_te_10
     width: 11.0
     midpoint:
     - -314.7
     - -80.96900000000001
     orientation: 270.0
     layer:
     - 203
     - 0
     port_type: vertical_te
     ", (0.0, 0.0), None, None, False, 66, 0)]

3. EHVA labels#

[6]:
mmi_te_ehva = gf.routing.add_fiber_array(mmi, get_input_labels_function=None)
mmi_te_ehva.remove_labels(lambda x: True)
add_label_ehva(component=mmi_te_ehva, die="mpw1")
mmi_te_ehva
../_images/notebooks_07_mask_9_0.png
[6]:
mmi2x2_add_fiber_array_8d6929d2: uid 23, ports ['vertical_te_00', 'vertical_te_01', 'vertical_te_02', 'vertical_te_03', 'vertical_te_10', 'vertical_te_20'], aliases [], 0 polygons, 50 references
[7]:
mmi_te_ehva.get_labels(depth=0)
[7]:
[Label("DIE NAME:mpw1
 CIRCUIT NAME:mmi2x2_add_fiber_array_8d6929d2

 OPTICALPORT NAME: vertical_te_20 TYPE: vertical_te, POSITION RELATIVE:(320.3, -80.969), ORIENTATION: 270
 OPTICALPORT NAME: vertical_te_03 TYPE: vertical_te, POSITION RELATIVE:(193.3, -80.969), ORIENTATION: 270
 OPTICALPORT NAME: vertical_te_02 TYPE: vertical_te, POSITION RELATIVE:(66.3, -80.969), ORIENTATION: 270
 OPTICALPORT NAME: vertical_te_01 TYPE: vertical_te, POSITION RELATIVE:(-60.7, -80.969), ORIENTATION: 270
 OPTICALPORT NAME: vertical_te_00 TYPE: vertical_te, POSITION RELATIVE:(-187.7, -80.969), ORIENTATION: 270
 OPTICALPORT NAME: vertical_te_10 TYPE: vertical_te, POSITION RELATIVE:(-314.7, -80.969), ORIENTATION: 270", (0.0, 0.0), None, None, False, 66, 0)]

One advantage of the YAML and EHVA formats is that you can track any changes on the components directly from the GDS label, as the label already stores any changes of the child device, as well as any settings that you specify.

Settings can have many levels of hierarchy, but you can still access any children setting with : notation.

grating_coupler:
    function: grating_coupler_elliptical_trenches
    settings:
        polarization: te
        taper_angle: 35
[8]:
mmi = gf.components.mmi2x2(length_mmi=10)
mmi_te_ehva = gf.routing.add_fiber_array(mmi, get_input_labels_function=None)
mmi_te_ehva.remove_labels(lambda x: True)
add_label_ehva(
    component=mmi_te_ehva,
    die="mpw1",
    metadata_include_parent=["grating_coupler:settings:polarization"],
)
mmi_te_ehva
../_images/notebooks_07_mask_12_0.png
[8]:
mmi2x2_length_mmi10_add_08a943c6: uid 60, ports ['vertical_te_00', 'vertical_te_01', 'vertical_te_02', 'vertical_te_03', 'vertical_te_10', 'vertical_te_20'], aliases [], 0 polygons, 50 references
[9]:
mmi_te_ehva.get_labels(depth=0)
[9]:
[Label("DIE NAME:mpw1
 CIRCUIT NAME:mmi2x2_length_mmi10_add_08a943c6
 CIRCUITINFO NAME: length_mmi, VALUE: 10
 CIRCUITINFO NAME: grating_coupler_settings_polarization, VALUE: te
 OPTICALPORT NAME: vertical_te_20 TYPE: vertical_te, POSITION RELATIVE:(322.5, -80.969), ORIENTATION: 270
 OPTICALPORT NAME: vertical_te_03 TYPE: vertical_te, POSITION RELATIVE:(195.5, -80.969), ORIENTATION: 270
 OPTICALPORT NAME: vertical_te_02 TYPE: vertical_te, POSITION RELATIVE:(68.5, -80.969), ORIENTATION: 270
 OPTICALPORT NAME: vertical_te_01 TYPE: vertical_te, POSITION RELATIVE:(-58.5, -80.969), ORIENTATION: 270
 OPTICALPORT NAME: vertical_te_00 TYPE: vertical_te, POSITION RELATIVE:(-185.5, -80.969), ORIENTATION: 270
 OPTICALPORT NAME: vertical_te_10 TYPE: vertical_te, POSITION RELATIVE:(-312.5, -80.969), ORIENTATION: 270", (0.0, 0.0), None, None, False, 66, 0)]

Pack#

Lets start with a resistance sweep, where you change the resistance width to measure sheet resistance.

[10]:
sweep = [gf.components.resistance_sheet(width=width) for width in [1, 10, 100]]
m = gf.pack(sweep)
c = m[0]
c
../_images/notebooks_07_mask_15_0.png
[10]:
pack_0_2797dbed: uid 79, ports [], aliases [], 0 polygons, 3 references

Then we add spirals with different lengths to measure waveguide propagation loss.

[11]:
spiral = gf.components.spiral_inner_io_fiber_single()
spiral
../_images/notebooks_07_mask_17_0.png
[11]:
spiral_inner_io_2a92e51_825ccbf1: uid 80, ports ['o2', 'o1'], aliases [], 0 polygons, 3 references
[12]:
spiral_te = gf.routing.add_fiber_single(
    gf.functions.rotate(gf.components.spiral_inner_io_fiber_single, 90)
)
spiral_te
../_images/notebooks_07_mask_18_0.png
[12]:
spiral_inner_io_2a92e51_08350dd7: uid 145, ports ['vertical_te_0', 'vertical_te_1', 'loopback1', 'loopback2'], aliases [], 0 polygons, 6 references
[13]:
# which is equivalent to
spiral_te = gf.compose(
    gf.routing.add_fiber_single,
    gf.functions.rotate90,
    gf.components.spiral_inner_io_fiber_single,
)
c = spiral_te(length=10e3)
c
../_images/notebooks_07_mask_19_0.png
[13]:
spiral_inner_io_fdc8380_9d882bc1: uid 226, ports ['vertical_te_0', 'vertical_te_1', 'loopback1', 'loopback2'], aliases [], 0 polygons, 6 references
[14]:
import gdsfactory as gf

add_label_ehva_mpw1 = gf.partial(gf.dft.add_label_ehva, die="mpw1")
add_fiber_single_no_labels = gf.partial(
    gf.routing.add_fiber_single, get_input_label_text_function=None
)

spiral_te = gf.compose(
    add_label_ehva_mpw1,
    add_fiber_single_no_labels,
    gf.functions.rotate90,
    gf.components.spiral_inner_io_fiber_single,
)
sweep = [spiral_te(length=length) for length in [10e3, 20e3, 30e3]]
m = gf.pack(sweep)
c = m[0]
c
../_images/notebooks_07_mask_20_0.png
[14]:
pack_0_efa3a79f: uid 292, ports [], aliases [], 0 polygons, 3 references

Together with GDS labels that are not fabricated, you can also add some physical labels that will be fabricated.

For example you can add prefix S at the north-center of each spiral using text_rectangular which is DRC clean and anchored on nc (north-center)

[15]:
text_metal3 = gf.partial(
    gf.components.text_rectangular_multi_layer, layers=(gf.LAYER.M3,)
)

m = gf.pack(sweep, text=text_metal3, text_anchors=("nc",), text_prefix="s")
c = m[0]
c
../_images/notebooks_07_mask_22_0.png
[15]:
pack_0_984f5ee9: uid 293, ports [], aliases [], 0 polygons, 6 references
[16]:
text_metal2 = gf.partial(gf.components.text, layer=gf.LAYER.M2)

m = gf.pack(sweep, text=text_metal2, text_anchors=("nc",), text_prefix="s")
c = m[0]
c
../_images/notebooks_07_mask_23_0.png
[16]:
pack_0_0adafae3: uid 304, ports [], aliases [], 0 polygons, 6 references

Grid#

You can also pack components with a constant spacing.

[17]:
g = gf.grid(sweep)
g
../_images/notebooks_07_mask_25_0.png
[17]:
grid_ce29931a: uid 311, ports [], aliases [(0, 0), (1, 0), (2, 0)], 0 polygons, 3 references
[18]:
gh = gf.grid(sweep, shape=(1, len(sweep)))
gh
../_images/notebooks_07_mask_26_0.png
[18]:
grid_32aadc5e: uid 313, ports [], aliases [(0, 0), (0, 1), (0, 2)], 0 polygons, 3 references
[19]:
gh_ymin = gf.grid(sweep, shape=(1, len(sweep)), align_y="ymin")
gh_ymin
../_images/notebooks_07_mask_27_0.png
[19]:
grid_70287b96: uid 315, ports [], aliases [(0, 0), (0, 1), (0, 2)], 0 polygons, 3 references

You can also add text labels to each element of the sweep

[20]:
gh_ymin = gf.grid_with_text(
    sweep, shape=(1, len(sweep)), align_y="ymin", text=text_metal3
)
gh_ymin
../_images/notebooks_07_mask_29_0.png
[20]:
grid_with_text_399d510d: uid 317, ports [], aliases [], 0 polygons, 4 references

You can modify the text by customizing the text_function that you pass to grid_with_text

[21]:
gh_ymin_m2 = gf.grid_with_text(
    sweep, shape=(1, len(sweep)), align_y="ymin", text=text_metal2
)
gh_ymin_m2
../_images/notebooks_07_mask_31_0.png
[21]:
grid_with_text_eaf0be1b: uid 324, ports [], aliases [], 0 polygons, 4 references

You have 2 ways of defining a mask:

  1. in python

  2. in YAML

Component in python#

You can define a Component top cell reticle or die using grid and pack python functions.

[22]:
import gdsfactory as gf

text_metal3 = gf.partial(
    gf.components.text_rectangular_multi_layer, layers=(gf.LAYER.M3,)
)
grid = gf.partial(gf.grid_with_text, text=text_metal3)
pack = gf.partial(gf.pack, text=text_metal3)

gratings_sweep = [
    gf.components.grating_coupler_elliptical(taper_angle=taper_angle)
    for taper_angle in [20, 30, 40]
]
gratings = grid(gratings_sweep, text=None)
gratings
../_images/notebooks_07_mask_33_0.png
[22]:
grid_with_text_dce87cf4: uid 334, ports [], aliases [], 0 polygons, 1 references
[23]:
gratings_sweep = [
    gf.components.grating_coupler_elliptical(taper_angle=taper_angle)
    for taper_angle in [20, 30, 40]
]
gratings_loss_sweep = [
    gf.components.grating_coupler_loss_fiber_single(grating_coupler=grating)
    for grating in gratings_sweep
]
gratings = grid(
    gratings_loss_sweep, shape=(1, len(gratings_loss_sweep)), spacing=(40, 0)
)
gratings
../_images/notebooks_07_mask_34_0.png
[23]:
grid_with_text_f7339787: uid 360, ports [], aliases [], 0 polygons, 4 references
[24]:
sweep_resistance = [
    gf.components.resistance_sheet(width=width) for width in [1, 10, 100]
]
resistance = gf.pack(sweep_resistance)[0]
resistance
../_images/notebooks_07_mask_35_0.png
[24]:
pack_0_3e1676a4: uid 363, ports [], aliases [], 0 polygons, 3 references
[25]:
spiral_te = gf.compose(
    gf.routing.add_fiber_single,
    gf.functions.rotate90,
    gf.components.spiral_inner_io_fiber_single,
)
sweep_spirals = [spiral_te(length=length) for length in [10e3, 20e3, 30e3]]
spirals = gf.pack(sweep_spirals)[0]
spirals
../_images/notebooks_07_mask_36_0.png
[25]:
pack_0_fa93fb03: uid 366, ports [], aliases [], 0 polygons, 3 references
[26]:
mask = gf.pack([spirals, resistance, gratings])[0]
mask
../_images/notebooks_07_mask_37_0.png
[26]:
pack_0_d130f021: uid 367, ports [], aliases [], 0 polygons, 3 references

As you can see you can define your mask in a single line.

For more complex mask, you can also create a new cell to build up more complexity

[27]:
@gf.cell
def mask():
    c = gf.Component()
    c << gf.pack([spirals, resistance, gratings])[0]
    c << gf.components.seal_ring(c.bbox)
    return c


c = mask(cache=False)
c
../_images/notebooks_07_mask_39_0.png
[27]:
mask: uid 368, ports [], aliases [], 0 polygons, 2 references

Metadata#

When saving GDS files is also convenient to store the metadata settings that you used to generate the GDS file.

[28]:
gdspath = c.write_gds_with_metadata(gdsdir="extra")
2022-06-28 17:01:55.990 | INFO     | gdsfactory.component:write_gds:1062 - Write GDS to 'extra/mask.gds'
2022-06-28 17:01:57.811 | INFO     | gdsfactory.component:write_gds_with_metadata:1070 - Write YAML metadata to 'extra/mask.yml'
[29]:
yaml_path = gdspath.with_suffix(".yml")
[30]:
labels_path = gf.mask.write_labels(gdspath=gdspath, layer_label=(201, 0))
2022-06-28 17:01:57.898 | INFO     | gdsfactory.mask.write_labels:write_labels:84 - Wrote 18 labels to CSV /home/runner/work/gdsfactory/gdsfactory/docs/notebooks/extra/mask.csv
[31]:
from omegaconf import OmegaConf

mask_metadata = OmegaConf.load(yaml_path)
[32]:
test_metadata = tm = gf.mask.merge_test_metadata(
    labels_path=labels_path, mask_metadata=mask_metadata
)
[33]:
tm.keys()
[33]:
dict_keys(['spiral_inner_io_d5fcbb42', 'spiral_inner_io_0f60acb9', 'spiral_inner_io_fdc8380c', 'grating_coupler_ellipti_396d882b', 'grating_coupler_ellipti_54408399', 'grating_coupler_ellipti_4b8de7b1'])
CSV labels  ------|
                  |--> merge_test_metadata dict
                  |
YAML metatada  ---
[34]:
spiral_names = [s for s in test_metadata.keys() if s.startswith("spiral")]
spiral_names
[34]:
['spiral_inner_io_d5fcbb42',
 'spiral_inner_io_0f60acb9',
 'spiral_inner_io_fdc8380c']
[35]:
spiral_lengths = [
    test_metadata[spiral_name].info.length for spiral_name in spiral_names
]
spiral_lengths
[35]:
[29999.994, 20000.004, 9999.992]
[36]:
gc_names = [s for s in test_metadata.keys() if s.startswith("grating")]
gc_names
[36]:
['grating_coupler_ellipti_396d882b',
 'grating_coupler_ellipti_54408399',
 'grating_coupler_ellipti_4b8de7b1']
[37]:
gc_taper_angles = [test_metadata[name].full.taper_angle for name in gc_names]
gc_taper_angles
[37]:
[20, 30, 40]

Component in YAML#

You can also define your component in YAML format thanks to gdsfactory.read.from_yaml

You need to define:

  • instances

  • placements

and you can leverage:

  1. pack_doe

  2. pack_doe_grid

1. pack_doe#

pack_doe places components as compact as possible

When running this tutorial make sure you UNCOMMENT this line %matplotlib widget so you can live update your changes in the YAML file

# %matplotlib widget -> %matplotlib widget

[38]:
# %matplotlib widget

import ipywidgets
from IPython.display import clear_output
import matplotlib.pyplot as plt
import gdsfactory as gf

x = ipywidgets.Textarea(rows=20, columns=480)

x.value = """
name: mask_grid

instances:
  rings:
    component: pack_doe
    settings:
      doe: ring_single
      settings:
        radius: [30, 50, 20, 40]
        length_x: [1, 2, 3]
      do_permutations: True
      function:
        function: add_fiber_array
        settings:
            fanout_length: 200

  mzis:
    component: pack_doe
    settings:
      doe: mzi
      settings:
        delta_length: [10, 100]
      function: add_fiber_array

placements:
  rings:
    xmin: 50

  mzis:
    xmin: rings,east
"""

out = ipywidgets.Output()
display(x, out)


def f(change, out=out):
    try:
        c = gf.read.from_yaml(change["new"])
        # clear_output()
        fig = c.plot()
        c.show(show_ports=True)
        out.clear_output()
    except Exception as e:
        out.clear_output()
        with out:
            display(e)


x.observe(f, "value")
f({"new": x.value})
../_images/notebooks_07_mask_55_2.png

2. pack_doe_grid#

pack_doe_grid places each component on a regular grid

[39]:
x.value = """
name: mask_compact

instances:
  rings:
    component: pack_doe
    settings:
      doe: ring_single
      settings:
        radius: [30, 50, 20, 40]
        length_x: [1, 2, 3]
      do_permutations: True
      function:
        function: add_fiber_array
        settings:
            fanout_length: 200


  mzis:
    component: pack_doe_grid
    settings:
      doe: mzi
      settings:
        delta_length: [10, 100]
      do_permutations: True
      spacing: [10, 10]
      function: add_fiber_array

placements:
  rings:
    xmin: 50

  mzis:
    xmin: rings,east
"""

display(x, out)
../_images/notebooks_07_mask_57_0.png
[40]:
import gdsfactory as gf


def mzi_te(**kwargs):
    gc = gf.c.grating_coupler_elliptical_tm()
    c = gf.c.mzi_phase_shifter_top_heater_metal(delta_length=40)
    c = gf.routing.add_fiber_single(
        c, get_input_label_text_function=None, grating_coupler=gc
    )
    add_label_yaml(c)
    c = c.rotate(-90)
    return c


# Lets write a mask
c = gf.grid(
    [
        mzi_te(),
        mzi_te(),
        gf.functions.rotate(mzi_te),
        add_label_yaml(
            gf.functions.mirror(gf.routing.add_fiber_single(gf.components.mmi1x2))
        ),
    ]
)
gdspath = c.write_gds("mask.gds")
csvpath = gf.mask.write_labels_gdspy(gdspath, prefix="component_name")
c
2022-06-28 17:02:03.108 | INFO     | gdsfactory.component:write_gds:1062 - Write GDS to 'mask.gds'
2022-06-28 17:02:03.136 | INFO     | gdsfactory.mask.write_labels:write_labels_gdspy:132 - Wrote 10 labels to /home/runner/work/gdsfactory/gdsfactory/docs/notebooks/mask.csv
../_images/notebooks_07_mask_58_1.png
[40]:
grid_254008cd: uid 539, ports [], aliases [(0, 0), (1, 0), (2, 0), (3, 0)], 0 polygons, 4 references
[41]:
from gdsfactory.read.labels import read_labels_yaml, add_port_markers

# You can make sure that all the ports will be tested by adding port markers
add_port_markers(gdspath=gdspath, csvpath=csvpath, marker_size=40)
../_images/notebooks_07_mask_59_0.png
[41]:
overlay: uid 581, ports [], aliases [], 0 polygons, 18 references