"""Layout for various superconducting resonators."""
# can be removed in python 3.14, see https://peps.python.org/pep-0749/
from __future__ import annotations
import qnngds as qg
import numpy as np
import scipy.constants
from functools import partial
from qnngds.typing import (
LayerSpec,
LayerSpecs,
DeviceSpec,
DeviceFactory,
CrossSectionSpec,
)
from qnngds import Device, CrossSection
import phidl.path as pp
import phidl.geometry as pg
from phidl import Path
[docs]def compute_veff(n_eff: float) -> float:
"""Computes effective light speed from effective index
Args:
n_eff: effective index of refraction
Returns:
(float): effective phase velocity in m/s
"""
return scipy.constants.c / n_eff
[docs]def compute_res_wavelength(n_eff: float, res_freq: float) -> float:
"""Computes resonant wavelength from effective index, resonant
frequency.
Args:
n_eff: effective index of refraction
res_freq: resonant frequency in Hz
Returns:
(float): resonant wavelength in microns
"""
v = compute_veff(n_eff)
return v / res_freq * 1e6
[docs]def cpw(
width: float = 10,
gap: float = 5,
radius: float = 50,
layer: LayerSpec = "PHOTO1",
) -> CrossSection:
"""Creates a coplanar waveguide (CPW) cross section.
NB resulting cross section is inverted: gaps will be filled and
conductor will be empty.
Args:
width (float): width of center conductor
gap (float): width of gaps on either side of conductor
radius (float): bend radius
layer (LayerSpec): GDS layer specification
Returns:
(CrossSection): CPW cross section
"""
CPW = CrossSection(radius=radius)
CPW.add(
width=width,
offset=0,
layer=qg.get_layer(layer),
hidden=True,
ports=(1, 2),
name="center",
)
for i in range(2):
CPW.add(
width=gap,
layer=qg.get_layer(layer),
offset=(-1) ** i * (width + gap) / 2,
name="gap_" + ("u" if i == 0 else "l"),
)
return CPW
[docs]def microstrip(
width: float = 5,
radius: float = 50,
layer: LayerSpec = "PHOTO1",
) -> CrossSection:
"""Creates a microstrip cross section
NB unlike :py:func:`cpw`, conductor is filled
Args:
width (float): width of center conductor
radius (float): bend radius
layer (LayerSpec): GDS layer specification
Returns:
(CrossSection): microstrip cross section
"""
USTRIP = CrossSection(radius=radius)
USTRIP.add(
width=width,
offset=0,
layer=qg.get_layer(layer),
ports=(1, 2),
name="center",
)
return USTRIP
[docs]@qg.device
def transmission_line(
cross_section: CrossSectionSpec = cpw,
length: float = 100,
) -> Device:
"""Construct a straight transmission line by extruding a cross section
Args:
length (float): length of transmission line
cross_section (CrossSectionSpec): cross section to extrude
Returns:
(Device): straight transmission line
"""
xc = qg.get_cross_section(cross_section)
return xc.extrude(pp.straight(length=length))
[docs]@qg.device
def meandered(
cross_section: CrossSectionSpec = cpw,
n_eff: float = 10,
resonant_freq: float = 5e9,
meander_width: float = 500,
) -> Device:
"""Construct meandered half-wave resonator
Args:
n_eff (float): effective index of refraction
resonant_freq (float): resonant frequeny in Hz
meander_width (float): width of meander structure
cross_section (CrossSectionSpec): cross section to use (e.g. CPW)
Returns:
(Device): meandered half-wave resonator
"""
xc = qg.get_cross_section(cross_section)
desired_length = compute_res_wavelength(n_eff=n_eff, res_freq=resonant_freq) / 2
left_bend = pp.euler(radius=xc.radius, angle=90, use_eff=False, p=1)
right_bend = pp.euler(radius=xc.radius, angle=-90, use_eff=False, p=1)
L_bend = left_bend.length()
x_bend = left_bend.xsize
# compute number of lines
L_straight = meander_width - 2 * x_bend
L_half = L_straight / 2 - x_bend
n_rows = int(
np.ceil(
(desired_length - 2 * (L_half + 2 * L_bend)) / (L_straight + 2 * L_bend)
)
)
# correct straight length
L_straight = (desired_length - 2 * L_bend * n_rows - 4 * L_bend + 2 * x_bend) / (
1 + n_rows
)
L_half = L_straight / 2 - x_bend
half = pp.straight(length=L_half)
straight = pp.straight(length=L_straight)
# start, left, right, and end segments
start = [left_bend, half, right_bend]
right = [right_bend, straight, left_bend]
left = [left_bend, straight, right_bend]
if n_rows % 2 == 0:
end = [right_bend, half, left_bend]
else:
end = [left_bend, half, right_bend]
# construct the path
P = Path()
P.append(start)
rows = 0
for _ in range(int(np.ceil(n_rows / 2))):
P.append(right)
rows += 1
if rows < n_rows:
P.append(left)
rows += 1
P.append(end)
return xc.extrude(P)
[docs]@qg.device
def straight(
cross_section: CrossSectionSpec = cpw,
n_eff: float = 100,
resonant_freq: float = 1e9,
) -> Device:
"""Construct straight half-wave resonator
Args:
cross_section (CrossSectionSpec): cross section to use (e.g. CPW)
n_eff (float): effective index of refraction
resonant_freq (float): resonant frequeny in Hz
Returns:
(Device): meandered half-wave resonator
"""
desired_length = compute_res_wavelength(n_eff=n_eff, res_freq=resonant_freq) / 2
xc = qg.get_cross_section(cross_section)
return xc.extrude(pp.straight(length=desired_length))
[docs]@qg.device
def pad(
width: float = 100,
length: float = 200,
edge_exclusion: float = 10,
sc_layer: LayerSpec = "PHOTO1",
metal_layers: LayerSpecs = ("PHOTO2",),
) -> Device:
"""Construct a pad for resonator with a metal layer for bonding on top of superconductor.
Args:
width (float): Desired width of superconductor layer
edge_exclusion (float): Amount on each side to decrease width of top metal bonding pad.
sc_layer (LayerSpec): layer specification for superconductor
metal_layers (LayerSpecs): layer(s) for metal
Returns:
(Device): pad
"""
PAD = Device()
sc = PAD << pg.straight(size=(width, length), layer=qg.get_layer(sc_layer))
sc.move(sc.center, (0, 0))
for metal_layer in metal_layers:
metal = PAD << pg.rectangle(
size=(width - 2 * edge_exclusion, length - 2 * edge_exclusion),
layer=qg.get_layer(metal_layer),
)
metal.move(metal.center, (0, 0))
PAD.add_port(name=1, port=sc.ports[2], layer=sc_layer)
return PAD
[docs]@qg.device
def transmission_line_resonator(
transmission_line_specs: tuple[DeviceSpec | None, DeviceSpec | None] = (
transmission_line,
None,
),
resonator_spec: DeviceSpec = meandered,
tl_cross_section: CrossSectionSpec = partial(cpw, width=75, gap=24),
res_cross_section: CrossSectionSpec = cpw,
taper: DeviceFactory = qg.geometries.hyper_taper,
pads: tuple[DeviceSpec | None, DeviceSpec | None] = (pad, None),
bbox_extension: float = 500,
) -> Device:
"""Construct a resonator embedded between two transmission lines
Inverts final design based on layer choice and PDK Layer class's outline function
Args:
transmission_line_specs (tuple[DeviceSpec | None, DeviceSpec | None]): Desired DeviceSpec for
transmission line on either end of resonator. If DeviceSpec, take a single argument
``cross_section``. If None, no transmission line will be created (although the resonator will
taper out to the width of the ``tl_cross_section``.
resonator_spec (DeviceSpec): Desired DeviceSpec for embedded resonator.
Must take a single argument ``cross_section``.
tl_cross_section (CrossSectionSpec): CPW or microstrip cross section for transmission line.
res_cross_section (CrossSectionSpec): CPW or microstrip cross section for resonator.
taper (DeviceFactory): Callable which produces a Device, used to generate a filled taper
between resonator and transmission line. Port widths must match. Should be solid (i.e. not outlined);
will be automatically outlined for CPW.
pads (tuple[DeviceSpec | None, DeviceSpec | None]): DeviceSpec or None for each pad.
If DeviceSpec, must take a single argument ``width``. If None, no pad will be created
bbox_extension (float): amount to extend ground plane for negative tone layout CPW, or positive
tone microstrip
Returns:
(Device): resonator embedded between transmission lines
Example:
>>> from functools import partial
>>> tl_spec = partial(
>>> qg.devices.resonator.transmission_line,
>>> length=100,
>>> )
>>> res_spec = partial(
>>> qg.devices.resonator.meandered,
>>> n_eff=100,
>>> resonant_freq=1e9,
>>> meander_width=300,
>>> )
>>> tl_xc_spec = partial(
>>> qg.devices.resonator.cpw,
>>> width=20,
>>> gap=4,
>>> layer="PHOTO1",
>>> )
>>> res_xc_spec = partial(
>>> qg.devices.resonator.cpw,
>>> width=10,
>>> radius=30,
>>> gap=2,
>>> layer="PHOTO1",
>>> )
>>> c = qg.devices.resonator.transmission_line_resonator(
>>> transmission_line_specs=(tl_spec, None),
>>> resonator_spec=res_spec,
>>> tl_cross_section=tl_xc_spec,
>>> res_cross_section=res_xc_spec,
>>> taper=qg.geometries.hyper_taper,
>>> pads=(qg.devices.resonator.pad, None),
>>> bbox_extension=200,
>>> )
"""
res_xc = qg.get_cross_section(res_cross_section)
tl_xc = qg.get_cross_section(tl_cross_section)
# if section[0] (the main section) is hidden, then assume we're using a CPW
res_cpw = res_xc.sections[0]["hidden"]
tl_cpw = tl_xc.sections[0]["hidden"]
res_layers = set(section["layer"] for section in res_xc.sections)
tl_layers = set(section["layer"] for section in tl_xc.sections)
# check inputs
if res_cpw ^ tl_cpw:
raise ValueError(
"Detected mismatch between resonator and transmission "
f"line cross-section types. {res_cpw=} and {tl_cpw=}."
)
if res_layers != tl_layers:
raise ValueError(
"Detected mismatch between resonator and transmission "
f"line layers. {res_layers=} and {tl_layers=}."
)
if len(res_layers) > 1:
raise Warning(
"WARNING: detected more than 1 layer in cross section "
"spec, tapers may not work correctly"
)
is_cpw = res_cpw
layers = res_layers
R = Device()
res = resonator_spec(res_xc)
R << res
R.add_ports(res.ports)
# create new cross-section for taper using transitions
T = Device()
trans_length = sum(section["width"] for section in tl_xc.sections)
trans_layer = res_xc.sections[0]["layer"]
transition = taper(
start_width=res_xc.sections[0]["width"],
end_width=tl_xc.sections[0]["width"],
length=trans_length,
layer=trans_layer,
)
if is_cpw:
# outline transition
start_w = sum(section["width"] for section in res_xc.sections)
end_w = sum(section["width"] for section in tl_xc.sections)
wide_transition = taper(
start_width=start_w, end_width=end_w, length=trans_length, layer=trans_layer
)
T << pg.kl_boolean(
A=wide_transition,
B=transition,
operation="A-B",
layer=res_xc.sections[1]["layer"],
)
else:
T << transition
T.add_ports(transition.ports)
# add tapers
R = qg.utilities.extend_ports(
device=R,
port_names=[1, 2],
extension=T,
)
for n, tl_spec in enumerate(transmission_line_specs):
if tl_spec is None:
continue
# attach tl to device
R = qg.utilities.extend_ports(
device=R,
port_names=[n + 1],
extension=tl_spec(tl_xc),
)
for n, pad in enumerate(pads):
if pad is None:
continue
# create the pad
pad_i = pad(tl_xc.sections[0]["width"])
if is_cpw:
# outline the pad
pad_i = qg.utilities.outline(
device=pad_i,
outline_layers={tl_xc.sections[1]["layer"]: tl_xc.sections[1]["width"]},
)
# attach pad to device
# new_ports is False, since we're not really extending the ports, just capping them with pads.
# there is not a second port on the pad to propagate to the newly-created device.
R = qg.utilities.extend_ports(
device=R, port_names=[n + 1], extension=pad_i, new_ports=False
)
# invert if needed
outline_layers = qg.utilities.get_outline_layers(qg.get_active_pdk().layers)
# if not CPW and layer is positive-tone, invert
ext_bbox_distance = {
layer: bbox_extension for layer in outline_layers if not is_cpw
}
# if CPW and layer is negative-tone, invert
ext_bbox_distance |= {
layer: bbox_extension
for layer in layers
if is_cpw and layer not in outline_layers
}
inverted = qg.utilities.invert(R, ext_bbox_distance=ext_bbox_distance)
R = Device("resonator")
R << inverted
R.add_ports(inverted.ports)
return R