Source code for qnngds.test_structures

"""Teststructures contains lithographic and electrical test structures."""

# can be removed in python 3.14, see https://peps.python.org/pep-0749/
from __future__ import annotations

import qnngds as qg
import phidl.geometry as pg

from typing import List, Optional, Tuple

import numpy as np

from functools import partial

from qnngds.typing import LayerSpec, LayerSpecs, DeviceSpec
from qnngds import Device


[docs]@qg.device def vernier_comb( pitch1: int | float = 0.5, pitch2: int | float = 0.1, layer1: LayerSpec = (1, 0), layer2: LayerSpec = (10, 0), text_angle: int | float = 0, ) -> Device: """Creates vernier caliper comb. Helper method for alignment_mark. Args: pitch1 (int or float): pitch of top comb pitch2 (int or float): pitch of bottom comb layer1 (LayerSpec): center comb GDS layer specification layer2 (LayerSpec): top/bottom comb GDS layer specification text_angle (int or float): angle to rotate text labels Returns: (Device): alignment vernier calipers """ COMB = Device("comb") # middle comb (made of layer1), pitch = 10 rect1 = pg.rectangle(size=(5, 30), layer=qg.get_layer(layer1)) middle_comb = COMB.add_array(rect1, columns=21, rows=1, spacing=(10, 0)) middle_comb.move(COMB.center, (0, 0)) # top and bottom combs (made of layer2), pitchs = 10+pitch1, 10+pitch2 rect2 = pg.rectangle(size=(5, 30), layer=qg.get_layer(layer2)) top_comb = COMB.add_array(rect2, columns=21, rows=1, spacing=(10 + pitch1, 0)) top_comb.move(top_comb.center, (middle_comb.center[0], middle_comb.center[1] + 30)) top_text = COMB.add_ref( pg.text(f"{round(pitch1 * 1e3)}NM", size=10, layer=qg.get_layer(layer2)) ) top_text.rotate(-text_angle) top_text.move(top_text.center, (140, 30)) bottom_comb = COMB.add_array(rect2, columns=21, rows=1, spacing=(10 + pitch2, 0)) bottom_comb.move( bottom_comb.center, (middle_comb.center[0], middle_comb.center[1] - 30) ) bottom_text = COMB.add_ref( pg.text(f"{round(pitch2 * 1e3)}NM", size=10, layer=qg.get_layer(layer2)) ) bottom_text.rotate(-text_angle) bottom_text.move(bottom_text.center, (140, -30)) # additional markers (made of layer1), for clarity rect1a = pg.rectangle(size=(5, 20), layer=qg.get_layer(layer1)) marksa = COMB.add_array(rect1a, columns=3, rows=2, spacing=(100, 110)) marksa.move(marksa.center, middle_comb.center) rect1b = pg.rectangle(size=(5, 10), layer=qg.get_layer(layer1)) marksb = COMB.add_array(rect1b, columns=2, rows=2, spacing=(100, 100)) marksb.move(marksb.center, middle_comb.center) return COMB
[docs]@qg.device def alignment_mark(layer1: LayerSpec = (1, 0), layer2: LayerSpec = (10, 0)) -> Device: """Creates vernier caliper comb between two layers. Helper method for alignment_mark. Args: layer1 (LayerSpec): center comb GDS layer specification layer2 (LayerSpec): top/bottom comb GDS layer specification Returns: (Device): alignment cross with vernier calipers """ MARK = Device("interlayer_align") # central part with cross CROSS = Device() cross = CROSS << pg.cross(length=200, width=2, layer=qg.get_layer(layer1)) rect = pg.rectangle(size=(45, 45), layer=qg.get_layer(layer2)) window = CROSS.add_array(rect, rows=2, columns=2, spacing=(155, 155)) window.move(window.center, cross.center) CROSS.flatten() MARK << CROSS VERNIER = Device() for n, pitches in enumerate(((0.5, 0.1), (0.2, 0.05))): p1, p2 = pitches for i in range(2): index = n * 2 + i comb = vernier_comb( pitch1=p1, pitch2=p2, layer1=layer1, layer2=layer2, text_angle=index * 90, ) v = VERNIER.add_ref(comb) v.rotate(index * 90) if index == 0: v.move((0, 0), (0, 200)) elif index == 1: v.move((0, 0), (-200, 0)) elif index == 2: v.move((0, 0), (0, -200)) elif index == 3: v.move((0, 0), (200, 0)) VERNIER.flatten() MARK << VERNIER MARK.move(MARK.center, (0, 0)) # text TEXT = Device() layer1_str = qg.get_layer(layer1).name.split("_")[0] layer2_str = qg.get_layer(layer2).name.split("_")[0] bg_label = ( layer2_str[:3] if len(layer2_str) < 4 else layer2_str[:2] + layer2_str[-1] ) sm_label = "" if len(layer2_str) < 5: sm_label += layer2_str else: sm_label += f"{layer2_str[:4]}{layer2_str[-1]}" sm_label += " ON " if len(layer1_str) < 5: sm_label += layer1_str else: sm_label += f"{layer1_str[:4]}{layer1_str[-1]}" for layer in (layer1, layer2): text1 = TEXT << pg.text(bg_label, size=50, layer=qg.get_layer(layer)) text1.move(text1.center, (-200, 190)) text2 = TEXT << pg.text(sm_label, size=10, layer=qg.get_layer(layer2)) text2.move(text2.center, (-200, 250)) if isinstance(layer1, tuple): layer1_numeric = f"{layer1[0]}/{layer1[1]}" else: layer1_enum = qg.get_layer(layer1).tuple layer1_numeric = f"{layer1_enum[0]}/{layer1_enum[1]}" if isinstance(layer2, tuple): layer2_numeric = f"{layer2[0]}/{layer2[1]}" else: layer2_enum = qg.get_layer(layer2).tuple layer2_numeric = f"{layer2_enum[0]}/{layer2_enum[1]}" text3 = TEXT << pg.text( layer2_numeric + " ON " + layer1_numeric, size=10, layer=qg.get_layer(layer2), ) text3.move(text3.center, (-200, 235)) TEXT.flatten() MARK << TEXT return MARK
[docs]@qg.device def multilayer_alignment( layers: LayerSpecs = ["PHOTO1", "PHOTO2", "PHOTO3"], ) -> Device: """Creates an alignment mark for each lithography layer. Args: layers (LayerSpecs): A list of GDS layer specifications Returns: (Device): alignment marks between each layer pair """ ALIGN = Device("alignment_marks") markers_pitch = 600 for i, layer1 in enumerate(layers): n = len(layers) - i - 1 if n != 0: for j, layer2 in enumerate(layers[-n:]): mark = ALIGN << alignment_mark(layer1, layer2) mark.move((j * markers_pitch, i * markers_pitch)) num_layers = len(layers) offset = -(num_layers - 2) * markers_pitch / 2 ALIGN.move((0, 0), (offset, offset)) return ALIGN
[docs]@qg.device def resolution_waffle(res: float | int = 1, layer: LayerSpec = (1, 0)) -> Device: """Creates resolution_waffle test structures for determining process resolution. Helper method for resolution_test. Args: res (float or int): Resolution (in µm) to be tested. layer (LayerSpec): GDS layer specification Returns: (Device): the resolution test structure """ WAFFLE = Device("resolution_waffle") W = pg.rectangle(size=(res * 80, res * 80), layer=qg.get_layer(layer)) pattern = [(res * x, res * 80) for x in [2, 1, 1, 2, 3, 5, 8, 13, 21, 15]] DUMMY = Device() WOut = DUMMY << pg.gridsweep( function=pg.rectangle, param_x={"size": pattern}, param_y={}, spacing=res ) WOut.move(WOut.center, W.center) WAFFLE << pg.kl_boolean(A=W, B=WOut, operation="A-B", layer=qg.get_layer(layer)) WOut.rotate(90, center=WOut.center) WAFFLE << pg.kl_boolean(A=W, B=WOut, operation="A-B", layer=qg.get_layer(layer)) text = WAFFLE << pg.text(str(res), size=20, layer=qg.get_layer(layer)) text.move((text.xmin, text.ymax), (W.xmin, W.ymin - min(10, 10 * res))) WAFFLEu = Device() WAFFLEu << pg.union(WAFFLE, by_layer=True) WAFFLEu.flatten() return WAFFLEu
[docs]@qg.device def resolution_L(res: float | int = 1, layer: LayerSpec = (1, 0)) -> Device: """Creates L-shaped test structures for determining process resolution. Helper method for resolution_test. Args: res (float or int): Resolution (in µm) to be tested. layer (LayerSpec): GDS layer specification Returns: (Device): the resolution test structure """ LLL = Device("LLL") grid_spacing = (15 * res, 15 * res) deviation = [0.8, 1, 1.2] for i, percent in enumerate(deviation): bars = Device() w = percent * res spacing = 2 * res bar = pg.rectangle(size=(min(100 * res, 100), w), layer=qg.get_layer(layer)) h_bars = bars.add_array(bar, columns=1, rows=5, spacing=(0, spacing)) v_bars = bars.add_array(bar, columns=1, rows=5, spacing=(0, spacing)) h_bars.rotate(90) h_bars.move((h_bars.xmin, h_bars.ymin), (0, 0)) v_bars.move((v_bars.xmin, v_bars.ymin), (0, 0)) lll = LLL << bars lll.move([i * offset for offset in grid_spacing]) text = LLL << pg.text(str(res), size=20, layer=qg.get_layer(layer)) start = (text.xmin, text.ymin) text.move(start, [(len(deviation) + 0.5) * offset for offset in grid_spacing]) LLLu = Device() LLLu << pg.union(LLL, by_layer=True) LLLu.flatten() return LLLu
[docs]@qg.device def resolution_test( resolutions: List[float] = [0.6, 0.8, 1.0], outline: Optional[float] = None, layer: LayerSpec = (1, 0), ) -> Device: """Creates L and waffle structures for determining process resolution. Args: resolutions (List[float]): List of resolutions (in µm) to be tested. outline (Optional[float]): If none, do not invert. If zero, invert the device, otherwise outline the device by this width. layer (LayerSpec): GDS layer specification Returns: (Device): the resolution test structures """ RES_TEST = Device("resolution_test") for test_fn in [resolution_L, resolution_waffle]: for res in resolutions: RES_TEST << test_fn(res, layer) RES_TEST.distribute(direction="y", spacing=0, separation=False, edge="ymin") RES_TEST.distribute(direction="x", spacing=10, edge="ymin") RES_TEST.move(RES_TEST.center, (0, 0)) if outline is not None: if outline > 0: RES_TEST = qg.utilities.outline(RES_TEST, {layer: outline}) else: RES_TEST = qg.utilities.invert(RES_TEST, {layer: 5}) RES_TESTu = Device() RES_TESTu << pg.union(RES_TEST, by_layer=True) RES_TESTu.flatten() return RES_TESTu
[docs]@qg.device def resolution_steps( resolutions: List[float] = [0.3, 0.4, 0.5, 0.6, 0.7, 0.8], width: float = 5, spacing: float = 5, layer: LayerSpec = (1, 0), ) -> Device: """Creates step pattern for lithographic resolution test. Adapted from PHIDL. Args: resolutions (List[float]): List of resolutions (in µm) to be tested. width (float): width of stripes spacing (float): spacing between stripes layer (LayerSpec): GDS layer specification Returns: (Device): the test structure """ D = Device() R1 = pg.rectangle(size=(width, spacing), layer=qg.get_layer(layer)) r = D << R1 r.xmin = -width r.ymin = 0 offset = 0.0 for resolution in reversed(resolutions): offset += spacing + resolution R2 = pg.rectangle(size=(width, resolution), layer=qg.get_layer(layer)) r = D << R1 r.xmin = 0 r.ymin = 0 r.movey(offset) r.movex(-width) r = D << R2 r.xmin = 0 r.ymin = 0 r.movey(offset - resolution) return D
[docs]@qg.device def resolution_checkerboard( resolutions: List[float] = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], layer: LayerSpec = (1, 0), label_interval: int = 5, label_size: float = 10, ) -> Device: """Creates crossed lith_steps pattern for lithographic resolution test. Args: resolutions (List[float]): List of resolutions (in µm) to be tested. layer (LayerSpec): GDS layer specification label_interval (bool): how often to label (set to 0 to disable all labels) label_size (float): size of text label Returns: (Device): the litho test structure """ D = Device("resolution_checkerboard") max_res = np.max(resolutions) widths = list(resolutions) + list(max_res * np.linspace(2, 10, 9)) + [100 * max_res] xmax = 0 spacing = 2 * max_res for width in widths: steps = D << resolution_steps( resolutions=resolutions, spacing=spacing, width=width, layer=qg.get_layer(layer), ) steps.movex(xmax - steps.xmax) xmax = steps.xmin # add labels offset = 0 for n, resolution in enumerate(reversed(resolutions)): offset += spacing + resolution if label_interval == 0: continue if n % label_interval == 0: label = D << pg.text( f"{round(resolution * 10) / 10}UM", size=label_size, layer=qg.get_layer(layer), ) label.move((label.xmin, label.y), (2 * label_size, offset - resolution / 2)) # add wider rectangle tick = D << pg.rectangle( size=(label_size, max(spacing / 2, 1.5)), layer=qg.get_layer(layer) ) else: # add narrower rectangle tick = D << pg.rectangle( size=(label_size / 2, max(spacing / 2, 1.5)), layer=qg.get_layer(layer) ) tick.move((tick.xmin, tick.y), (label_size / 2, offset - resolution / 2)) return D
[docs]@qg.device def vdp( diagonal: float = 400, contact_width: float = 40, layer: LayerSpec = (1, 0), ) -> Device: """Creates a Van der Pauw (VDP) device with specified dimensions. Args: diagonal (float): Length of the VDP device, overall maximum dimension, in µm. contact_width (float): Width of the contact points (width of the ports), in µm. layer (LayerSpec): GDS layer specification Returns: (Device): Van der Pauw cell """ VDP = Device("vdp") xpts = [ -contact_width / 2, contact_width / 2, diagonal / 2, diagonal / 2, contact_width / 2, -contact_width / 2, -diagonal / 2, -diagonal / 2, ] ypts = [ diagonal / 2, diagonal / 2, contact_width / 2, -contact_width / 2, -diagonal / 2, -diagonal / 2, -contact_width / 2, contact_width / 2, ] polygon = pg.polygon_ports(xpts=xpts, ypts=ypts, layer=qg.get_layer(layer)) VDP << polygon VDP.flatten() VDP.add_port(port=polygon.ports["1"], name="N1", layer=qg.get_layer(layer)) VDP.add_port(port=polygon.ports["3"], name="E1", layer=qg.get_layer(layer)) VDP.add_port(port=polygon.ports["5"], name="S1", layer=qg.get_layer(layer)) VDP.add_port(port=polygon.ports["7"], name="W1", layer=qg.get_layer(layer)) return VDP
[docs]@qg.device def rect_tlm( contact_l: float = 10, spacings: List[float] = [10, 10, 20, 50, 80, 100, 200], contact_w: float = 100, via_layer: LayerSpec | None = (1, 0), finger_layer: LayerSpec = (10, 0), pad_layer: LayerSpec | None = (10, 0), mesa_layer: LayerSpec = (20, 0), pad_size: Tuple[float, float] = (80, 80), ) -> Device: """Creates rectangular transfer-length-method test structures. Args: contact_l (float): length of metal contact on semiconductor spacings (List[float]): list of spacings between contacts contact_w (float): width of contact/semiconductor via_layer (LayerSpec | None): layer specification for via between mesa and fingers. If None, don't include via. finger_layer (LayerSpec): layer for fingers that periodically contact mesa pad_layer (LayerSpec | None): layer for probable pads. If None, don't include. mesa_layer (LayerSpec): layer for semiconductor pad_size (tuple(float,float)): width, height of pad Returns: (Device): TLM structure """ TLM = Device("rect_tlm") xoff = 0 for n, space in enumerate(spacings): fp_w = space + 2 * contact_l w = contact_w * 1.2 + 10 for i in range(2): fp = TLM << pg.flagpole( size=(fp_w, pad_size[1]), stub_size=(contact_l, w), shape=("d" if i % 2 else "p"), taper_type=None, layer=qg.get_layer(finger_layer), ) if i % 2: fp.movey(-fp.ymax + contact_w / 2 + 5) fp.movex(xoff - fp.xmax) else: fp.movey(-fp.ymin - contact_w / 2 - 5) fp.movex(xoff - fp.xmin + 50) xoff = fp.xmax if via_layer is not None: via = TLM << pg.rectangle( size=(contact_l, contact_w + 10), layer=qg.get_layer(via_layer) ) if i % 2: via.move((fp.xmax - contact_l / 2 - via.x, -via.y)) else: via.move((fp.xmin + contact_l / 2 - via.x, -via.y)) # add vias to lower metal pads pad_via = TLM << pg.rectangle( size=(fp_w, pad_size[1]), layer=qg.get_layer(via_layer) ) pad_via.movex(fp.xmax - pad_via.xmax) if i % 2: pad_via.movey(fp.ymin - pad_via.ymin) else: pad_via.movey(fp.ymax - pad_via.ymax) top_pad = TLM << pg.rectangle( size=(fp_w + 2, pad_size[1] + 2), layer=qg.get_layer(pad_layer) ) top_pad.move(top_pad.center, pad_via.center) text = TLM << pg.text(str(space), layer=qg.get_layer(finger_layer)) text.move((xoff - text.xmin + 5, -w / 2 - pad_size[1] + 10 - text.ymin)) # add mesa center = (TLM.x, 0) mesa = TLM << pg.rectangle( size=(TLM.xsize + 50, contact_w), layer=qg.get_layer(mesa_layer) ) mesa.move(mesa.center, center) return TLM
[docs]@qg.device def circ_tlm( ext_radius: float = 100, int_radius: List[float] = [50, 70, 80, 90, 95, 98, 99], pad_layer: LayerSpec = (20, 0), mesa_layers: LayerSpecs = [(1, 0), (10, 0)], text_size: float = 10, ) -> Device: """Creates rectangular transfer-length-method test structures. Args: ext_radius (float): external radius of hole that defines outer pad int_radius (List[float]): list of internal radii. The gap is d = ext_radius - int_radius. pad_layer (LayerSpec): layer specification for probable pads. mesa_layers (LayerSpecs): layer(s) for bottom metal/semiconductor and/or vias text_size (float): size of text label Returns: (Device): TLM structure """ TLM = Device("circ_tlm") cuts = [] for r_i in int_radius: d = ext_radius - r_i r = (ext_radius + r_i) / 2 CUT = Device() r = CUT << pg.ring( radius=r, width=d, angle_resolution=2.5, layer=qg.get_layer(pad_layer) ) t = CUT << pg.text( text=f"{ext_radius}/{r_i}", size=text_size, justify="right", layer=qg.get_layer(pad_layer), ) t.move((t.xmax, t.ymax), (r.xmax, r.ymax)) cuts.append(CUT) c = pg.grid( cuts, spacing=(10, 10), shape=(len(cuts), 1), align_x="x", align_y="y", ) # make the mesa for layer in mesa_layers: m = TLM << pg.rectangle( size=(c.xsize + 10, c.ysize + 10), layer=qg.get_layer(layer) ) m.move(m.center, c.center) DUMMY = Device() p = DUMMY << pg.rectangle( size=(c.xsize + 10, c.ysize + 10), layer=qg.get_layer(pad_layer) ) p.move(p.center, c.center) TLM << pg.kl_boolean( A=p, B=c, operation="A-B", layer=qg.get_layer(pad_layer), ) return TLM
[docs]@qg.device def via_chain( via_spec: DeviceSpec | Device = qg.geometries.via, num_vias: int = 5, spacing: float = 10, tap_period: int = 1, ) -> Device: """Makes a chain of vias, with optional taps along the length of the chain. Args: via_spec (DeviceSpec | Device): function, component name, or component for the via num_vias (int): number of vias to include in chain spacing (float): spacing between vias tap_period (int): number of vias between each tap. If zero, doesn't place any taps. Returns: (Device): the via chain """ if tap_period < 0: raise ValueError(f"{tap_period=} must be positive") if tap_period > 1: raise ValueError("tap_period > 1 has not been implemented yet") VC = Device("via_chain") via = qg.get_device(via_spec) # get layers port_dict = qg.utilities.get_device_port_direction(via) east_layers = set(port.layer for port in port_dict["E"]) west_layers = set(port.layer for port in port_dict["W"]) if len(east_layers) == 1 and len(west_layers) == 1: if east_layers == west_layers: raise ValueError("bad via_spec, did not receive ports on different layers") east_layer = east_layers.pop() west_layer = west_layers.pop() else: if east_layers != west_layers: raise ValueError( f"got multiple layers on east/west side of via, but they are not identical. please check via spec: {port_dict=}" ) east_layer = east_layers.pop() west_layer = (west_layers - set([east_layer])).pop() east_port = [port for port in port_dict["E"] if port.layer == east_layer] west_port = [port for port in port_dict["W"] if port.layer == west_layer] if len(east_port) > 1 or len(west_port) > 1: raise ValueError(f"got too many ports, please check via spec: {port_dict=}") east_port = east_port[0] west_port = west_port[0] if east_port.width != west_port.width: raise ValueError(f"width mismatch between ports {east_port=} and {west_port=}") width = east_port.width if tap_period == 0: connector = partial(pg.straight, size=(spacing, width)) else: connector = partial( qg.geometries.tee, size=(spacing, width), stub_size=(width, width), taper_type="fillet", taper_radius=width / 2, ) vias = VC.add_array( via, columns=num_vias, rows=1, spacing=(via.xsize + spacing, 0), ) east_end_port_layer = west_layer if num_vias % 2 == 0 else east_layer east_end_port_name = [ port for port in port_dict["E"] if port.layer == east_end_port_layer ][0].name end_ports = [ vias.ports[0, 0][west_port.name], vias.ports[0, num_vias - 1][east_end_port_name], ] conn_ports = [] for i in range(2): layer = east_layer if i == 0 else west_layer port = east_port if i == 0 else west_port odd = i n_conn = (num_vias - odd) // 2 if n_conn > 0: conn = VC.add_array( connector(layer=qg.get_layer(layer)), columns=n_conn, rows=1, spacing=(2 * (via.xsize + spacing), 1), ) conn.movey(-width / 2) if odd: conn.rotate(180) conn.movex(vias.xmin + 2 * via.xsize + spacing - conn.xmin) else: conn.movex(vias.xmin + via.xsize - conn.xmin) if tap_period > 0: conn_ports.append([conn.ports[0, n][3] for n in range(n_conn)]) else: conn_ports.append([]) ports = [end_ports[0]] if len(conn_ports) > 0: ports += conn_ports[0] ports += [end_ports[1]] if len(conn_ports) > 1: ports += conn_ports[1] for n, port in enumerate(ports): VC.add_port(name=f"{n + 1}", port=port) return VC
[docs]@qg.device def etch_test( layer: LayerSpec = (1, 0), pad_size: tuple[float, float] = (2000, 2000), trench_width: float = 20, ) -> Device: """Construct side-by-side pads for performing electrical etch tests. Args: layer (LayerSpec): GDS layer specification pad_size (tuple[float, float]): width, height of each pad trench_width (float): width of trench around each pad Returns: (Device): etch test structure """ TRENCHES = Device("etch_trench") # create trench rect = pg.rectangle(size=pad_size, layer=qg.get_layer(layer)) qg.utilities.create_layered_ports(rect, layer) pad_outlined = qg.utilities.outline( device=rect, outline_layers={layer: trench_width}, kl_tile_size=max(pad_size[0], pad_size[1]), ) t = TRENCHES.add_array( pad_outlined, columns=2, rows=1, spacing=(pad_size[0] + 5 * trench_width, 0) ) t.move(t.center, (0, 0)) return TRENCHES
[docs]@qg.device def cross_bridge_kelvin_resistor( size: float = 50, lead_length: float = 50, layer_top: LayerSpec = "PHOTO1", layer_bot: LayerSpec = "EBEAM_COARSE", layer_via: LayerSpec | None = "PHOTO2", ) -> Device: """Generate a cross-bridge Kelvin resistor. See `this paper <http://ieeexplore.ieee.org/document/913141/>`_. Args: size (float): side length of square junction lead_length (float): length of leads to junction layer_top (LayerSpec): layer specification of top conductor layer_bot (LayerSpec): layer specification of bottom conductor layer_via (LayerSpec | None): if not None, create via on specified layer Returns: (Device): cross-bridge Kelvin resistor """ CBKR = Device("cbkr") if layer_via is not None: center = qg.geometries.via( size=(size, size), via_undersize=1, layer_bottom=layer_bot, layer_via=layer_via, layer_top=layer_top, ) else: center = Device() cbot = center << pg.compass(size=(size, size), layer=qg.get_layer(layer_bot)) ctop = center << pg.compass(size=(size, size), layer=qg.get_layer(layer_top)) for port_name in cbot.ports: center.add_port(name=f"2{port_name}", port=cbot.ports[port_name]) for port_name in ctop.ports: center.add_port(name=f"1{port_name}", port=ctop.ports[port_name]) top_ext = pg.compass(size=(lead_length, size), layer=qg.get_layer(layer_top)) bot_ext = pg.compass(size=(lead_length, size), layer=qg.get_layer(layer_bot)) center_i = CBKR << center ports = [] dir_lut = {1: "W", 2: "N", 3: "E", 4: "S"} for layer_i in range(2): for lead_i in range(2): lead = CBKR << (bot_ext if layer_i == 0 else top_ext) qg.utilities.create_layered_ports( lead, layer_bot if layer_i == 0 else layer_top ) con_port = f"{2 - layer_i}{dir_lut[lead_i + 1 + 2 * layer_i]}" lead.connect(port=lead.ports["W"], destination=center_i.ports[con_port]) ports.append(lead.ports["E"]) for n, port in enumerate(ports): CBKR.add_port(name=n + 1, port=port) return CBKR
[docs]@qg.device def dose_defocus( resolutions: tuple[float] = (0.7, 0.8, 0.9, 1.0), layer: LayerSpec = "PHOTO1", ) -> Device: """Generate a test structure for doing dose/defocus tests Contains a lithographic checkerboard, stars, crossed lines, and waffles. Args: layer (LayerSpec): layer to put pattern on resolutions (tuple[float]): resolutions to use for star, crossed lines and waffles. Resolution for litho checkerboard is determined automatically from maximum element. Returns: (Device): dose defocus test structure. """ D = Device("dose_defocus") res_test = Device("res_test") for i in range(2): rt = res_test << qg.test_structures.resolution_test( resolutions=resolutions, layer=layer, outline=None if i == 0 else 0 ) rt.movey((rt.ysize + 10) * i) D << res_test # checkerboards maxres = 10 ** np.round(np.log10(np.max(resolutions))) minres = 10 ** np.round(np.log10(np.min(resolutions))) minres = min(minres, 0.1 * maxres) maxres = min(maxres, 10 * minres) line_widths = tuple(np.linspace(minres, maxres, 10)) checkerboard = qg.test_structures.resolution_checkerboard( resolutions=line_widths, layer=layer, label_interval=5, label_size=10, ) y = D.ymin ch_horz = D << checkerboard ch_horz.move((ch_horz.xmin, ch_horz.ymax), (res_test.xmin, y - 50)) ch_vert = D << checkerboard ch_vert.rotate(90) ch_vert.move((ch_vert.xmax, ch_vert.ymax), (res_test.xmin - 30, res_test.ymax)) # litho stars x = ch_horz.xmax + 20 y = ch_horz.y for i in range(2): for width in np.linspace(0.4 * maxres, maxres, 4): star = pg.litho_star( num_lines=20, line_width=width, diameter=40, layer=qg.get_layer(layer), ) if i == 1: star = qg.utilities.invert(device=star, ext_bbox_distance={layer: 2}) star_i = D << star star_i.move((star_i.xmin, star_i.y), (x, y)) x = star_i.xmax + 10 text = D << pg.text(str(width), size=15, layer=qg.get_layer(layer)) text.move((text.x, text.ymax), (star_i.x, star_i.ymin - 5)) D.flatten() return D