Source code for qpdk.cells.fluxonium

"""Fluxonium qubit components."""

from __future__ import annotations

import math
from functools import partial
from typing import TYPE_CHECKING, Literal

import gdsfactory as gf
from gdsfactory.component import Component
from gdsfactory.typings import ComponentSpec, CrossSectionSpec, LayerSpec
from kfactory.conf import CheckInstances
from klayout.db import DCplxTrans

from qpdk.cells._schematic import (
    fluxonium_schematic,
    fluxonium_with_bbox_schematic,
)
from qpdk.cells.inductor import meander_inductor
from qpdk.cells.junction import josephson_junction
from qpdk.cells.junction_array import josephson_junction_array
from qpdk.tech import (
    LAYER,
    get_etch_section,
    superinductor_cross_section,
)
from qpdk.utils import add_rect, transform_component

if TYPE_CHECKING:
    from qpdk.utils import PortConfig

__all__ = ["fluxonium", "fluxonium_with_bbox"]


[docs] @gf.cell( check_instances=CheckInstances.IGNORE, tags=["qubits", "inductors"], schematic_function=fluxonium_schematic, ) def fluxonium( pad_size: tuple[float, float] = (250.0, 400.0), pad_gap: float = 25.0, junction_spec: ComponentSpec = josephson_junction, junction_displacement: DCplxTrans | None = None, junction_margin: float = 1.0, inductor_n_turns: int = 155, inductor_margin_x: float = 1.0, inductor_cross_section: CrossSectionSpec = superinductor_cross_section, connection_wire_width: float = 2.0, layer_metal: LayerSpec = LAYER.M1_DRAW, inductor: Literal["meander", "array"] = "meander", inductor_array_spec: ComponentSpec = josephson_junction_array, ) -> Component: r"""Creates a fluxonium qubit with capacitor pads, Josephson junction, and superinductor. .. svgbob:: +---------+ +---------+ | | | | | | | | | pad1 | | pad2 | | | | | | | | | +----+----+ +----+----+ | | | JJ | +======XX=============+ | | | inductor | +---------------------+ See :cite:`manucharyan_fluxonium_2009` and :cite:`nguyen_blueprint_2019`. Args: pad_size: (width, height) of each capacitor pad in µm. pad_gap: Gap between the two capacitor pads in µm. junction_spec: Component specification for the Josephson junction. junction_displacement: Optional transformation applied to the junction. junction_margin: Vertical margin between the junction and capacitor pads in µm. inductor_n_turns: Number of horizontal meander runs. Must be odd. inductor_margin_x: Horizontal margin for the inductor in µm. inductor_cross_section: Cross-section for the meander inductor. connection_wire_width: Width of the connecting wires in µm. layer_metal: Layer for the metal pads and connection wires. inductor: Superinductor implementation. ``"meander"`` draws a kinetic-inductance meander. ``"array"`` draws a Josephson junction array from ``inductor_array_spec`` instead, as in :cite:`manucharyan_fluxonium_2009`. inductor_array_spec: Component specification for the junction array, used when ``inductor="array"``. Its port ``o1`` must face west and ``o2`` east, i.e. an even number of folds. The metal wires overlap half of the array's ``lead_length`` (0.5 µm if it has none), so they never reach the end islands. Returns: The fluxonium component. Raises: ValueError: If inductor_n_turns is even, if pad_gap is too small, or if the junction array does not fit between the pads. """ if inductor not in {"meander", "array"}: raise ValueError(f"Unknown inductor {inductor!r}") if inductor == "meander" and inductor_n_turns % 2 == 0: raise ValueError("inductor_n_turns must be odd") xs = gf.get_cross_section(inductor_cross_section) inductor_wire_width = xs.width etch_section = get_etch_section(xs) inductor_wire_gap = 2 * etch_section.width c = Component() pad_width, pad_height = pad_size junction_comp = gf.get_component(junction_spec) junction_rotated_height = junction_comp.size_info.width inductor_turn_length = pad_gap - 2 * inductor_margin_x inductor_total_height = ( inductor_n_turns * inductor_wire_width + max(0, inductor_n_turns - 1) * inductor_wire_gap ) if inductor_turn_length <= 0: raise ValueError(f"pad_gap={pad_gap} is too small") # Capacitor pads def create_capacitor_pad(x_offset: float) -> gf.ComponentReference: pad = gf.components.rectangle(size=pad_size, layer=layer_metal) pad_ref = c.add_ref(pad) pad_ref.move((x_offset, -pad_height / 2)) return pad_ref create_capacitor_pad(-pad_width - pad_gap / 2) create_capacitor_pad(pad_gap / 2) # Josephson junction junction_ref = c.add_ref(junction_comp) junction_ref.rotate(45) junction_y = -pad_height / 2 - junction_margin - junction_rotated_height / 2 junction_ref.dcenter = (0, junction_y) if junction_displacement: junction_ref.transform(junction_displacement) # Superinductor if inductor == "array": inductor_info = _add_array_inductor( c, junction_bottom=junction_ref.dymin, junction_center_y=junction_ref.dcenter[1], pad_height=pad_height, pad_gap=pad_gap, junction_margin=junction_margin, inductor_margin_x=inductor_margin_x, inductor_array_spec=inductor_array_spec, connection_wire_width=connection_wire_width, layer_metal=layer_metal, ) else: meander = meander_inductor( n_turns=inductor_n_turns, turn_length=inductor_turn_length, cross_section=inductor_cross_section, wire_gap=inductor_wire_gap, etch_bbox_margin=0, add_etch=False, ) inductor_ref = c.add_ref(meander) inductor_y = ( junction_ref.dcenter[1] - junction_rotated_height / 2 - junction_margin - inductor_total_height / 2 ) inductor_ref.dcenter = (0, inductor_y) # Connection wires ind_o1 = inductor_ref.ports["o1"] ind_o2 = inductor_ref.ports["o2"] bus_left_x = -pad_gap / 2 - connection_wire_width / 2 bus_right_x = pad_gap / 2 + connection_wire_width / 2 bus_top_y = -pad_height / 2 + 5.0 # 5 um overlap with pads jj_conn_y0 = junction_ref.dcenter[1] - connection_wire_width / 2 # Left bus bar add_rect( c, layer=layer_metal, x_center=bus_left_x, width=connection_wire_width, y0=jj_conn_y0, y1=bus_top_y, ) # Tapered vertical NbTiN lead c.add_polygon( [ (-pad_gap / 2, jj_conn_y0), (-pad_gap / 2 - connection_wire_width, jj_conn_y0), ( -pad_gap / 2 - inductor_wire_width, ind_o1.dcenter[1] - inductor_wire_width / 2, ), (-pad_gap / 2, ind_o1.dcenter[1] - inductor_wire_width / 2), ], layer=LAYER.NbTiN, ) # Right bus bar add_rect( c, layer=layer_metal, x_center=bus_right_x, width=connection_wire_width, y0=jj_conn_y0, y1=bus_top_y, ) # Tapered vertical NbTiN lead c.add_polygon( [ (pad_gap / 2, jj_conn_y0), (pad_gap / 2 + connection_wire_width, jj_conn_y0), ( pad_gap / 2 + inductor_wire_width, ind_o2.dcenter[1] - inductor_wire_width / 2, ), (pad_gap / 2, ind_o2.dcenter[1] - inductor_wire_width / 2), ], layer=LAYER.NbTiN, ) # Inductor stubs - fixed height to prevent shorting add_rect( c, layer=LAYER.NbTiN, x0=-pad_gap / 2, x1=ind_o1.dcenter[0], y_center=ind_o1.dcenter[1], height=inductor_wire_width, ) add_rect( c, layer=LAYER.NbTiN, x0=ind_o2.dcenter[0], x1=pad_gap / 2, y_center=ind_o2.dcenter[1], height=inductor_wire_width, ) inductor_info: dict[str, int | float | str] = { "inductor_n_turns": inductor_n_turns, "inductor_total_wire_length": meander.info["total_wire_length"], } # Junction leads jj_ports = sorted( [junction_ref.ports["left_wide"], junction_ref.ports["right_wide"]], key=lambda p: p.dcenter[0], ) jj_p_left = jj_ports[0].dcenter jj_p_right = jj_ports[1].dcenter # Junction wires add_rect( c, layer=layer_metal, x0=-pad_gap / 2, x1=jj_p_left[0] + 1.0, # 1 um overlap to ensure contact y_center=jj_p_left[1], height=connection_wire_width, ) add_rect( c, layer=layer_metal, x0=jj_p_right[0] - 1.0, # 1 um overlap x1=pad_gap / 2, y_center=jj_p_right[1], height=connection_wire_width, ) # Ports ports_config: list[PortConfig] = [ { "name": "left_pad", "center": (-pad_width - pad_gap / 2, 0), "width": pad_height, "orientation": 180, "layer": layer_metal, }, { "name": "left_pad_inner", "center": (-pad_gap / 2, 0), "width": pad_height, "orientation": 0, "layer": layer_metal, "port_type": "placement", }, { "name": "right_pad", "center": (pad_width + pad_gap / 2, 0), "width": pad_height, "orientation": 0, "layer": layer_metal, }, { "name": "right_pad_inner", "center": (pad_gap / 2, 0), "width": pad_height, "orientation": 180, "layer": layer_metal, "port_type": "placement", }, { "name": "junction", "center": junction_ref.dcenter, "width": _snap_to_grid(junction_ref.size_info.height), "orientation": 90, "layer": LAYER.JJ_AREA, "port_type": "placement", }, ] for port_config in ports_config: c.add_port(**port_config) c.info["qubit_type"] = "fluxonium" for key, value in inductor_info.items(): c.info[key] = value return c
def _add_array_inductor( c: Component, *, junction_bottom: float, junction_center_y: float, pad_height: float, pad_gap: float, junction_margin: float, inductor_margin_x: float, inductor_array_spec: ComponentSpec, connection_wire_width: float, layer_metal: LayerSpec, ) -> dict[str, int | float | str]: """Place a Josephson junction array below the junction and wire it to the pads. Returns: The ``info`` entries describing the array. Raises: ValueError: If the array ports do not face west and east, or if the array does not fit between the pads. """ array = gf.get_component(inductor_array_spec) if (array.ports["o1"].orientation, array.ports["o2"].orientation) != (180, 0): raise ValueError( "inductor_array_spec must have o1 facing west and o2 facing east " "(use an even number of folds)" ) array_ref = c.add_ref(array) array_ref.dymax = junction_bottom - junction_margin port_span = array.ports["o2"].dcenter[0] - array.ports["o1"].dcenter[0] if port_span > pad_gap - 2 * inductor_margin_x: raise ValueError( f"Junction array port span {port_span:g} µm does not fit in " f"pad_gap={pad_gap} with inductor_margin_x={inductor_margin_x}" ) array_ref.dmove(( -(array_ref.ports["o1"].dcenter[0] + array_ref.ports["o2"].dcenter[0]) / 2, 0, )) lead_overlap = 0.5 * float(array.settings.model_dump().get("lead_length", 1.0)) ind_o1 = array_ref.ports["o1"].dcenter ind_o2 = array_ref.ports["o2"].dcenter bus_top_y = -pad_height / 2 + 5.0 # 5 um overlap with pads for sign, port_center in ((-1, ind_o1), (1, ind_o2)): bus_x = sign * (pad_gap / 2 + connection_wire_width / 2) add_rect( c, layer=layer_metal, x_center=bus_x, width=connection_wire_width, y0=min(junction_center_y, port_center[1]) - connection_wire_width / 2, y1=bus_top_y, ) # Stub from the bus bar onto half of the array lead, so it cannot reach # past the end island and short the first or last junction. x_inner = port_center[0] - sign * lead_overlap add_rect( c, layer=layer_metal, x0=min(sign * pad_gap / 2, x_inner), x1=max(sign * pad_gap / 2, x_inner), y_center=port_center[1], height=connection_wire_width, ) return { "inductor": "array", "n_junctions": array.info["n_junctions"], "array_inductance_nH": array.info["array_inductance_nH"], } def _snap_to_grid(value: float, grid: float = 0.002) -> float: """Snap a value up to the next grid multiple.""" return math.ceil(value / grid) * grid
[docs] @gf.cell( tags=["qubits", "inductors"], schematic_function=fluxonium_with_bbox_schematic, ) def fluxonium_with_bbox( bbox_extension: float = 200.0, pad_size: tuple[float, float] = (250.0, 400.0), pad_gap: float = 25.0, junction_spec: ComponentSpec = josephson_junction, junction_displacement: DCplxTrans | None = None, junction_margin: float = 1.0, inductor_n_turns: int = 155, inductor_margin_x: float = 1.0, inductor_cross_section: CrossSectionSpec = superinductor_cross_section, connection_wire_width: float = 2.0, layer_metal: LayerSpec = LAYER.M1_DRAW, inductor: Literal["meander", "array"] = "meander", inductor_array_spec: ComponentSpec = josephson_junction_array, ) -> Component: """Fluxonium with an etched bounding box. Args: bbox_extension: Extension of the bounding box from the fluxonium edge in µm. pad_size: (width, height) of each capacitor pad in µm. pad_gap: Gap between the two capacitor pads in µm. junction_spec: Component specification for the Josephson junction. junction_displacement: Optional transformation applied to the junction. junction_margin: Vertical margin between the junction and capacitor pads in µm. inductor_n_turns: Number of horizontal meander runs. Must be odd. inductor_margin_x: Horizontal margin for the inductor in µm. inductor_cross_section: Cross-section for the meander inductor. connection_wire_width: Width of the connecting wires in µm. layer_metal: Layer for the metal pads and connection wires. inductor: Superinductor implementation. ``"meander"`` draws a kinetic-inductance meander. ``"array"`` draws a Josephson junction array from ``inductor_array_spec`` instead, as in :cite:`manucharyan_fluxonium_2009`. inductor_array_spec: Component specification for the junction array, used when ``inductor="array"``. Its port ``o1`` must face west and ``o2`` east, i.e. an even number of folds. Returns: The fluxonium component with a bounding box. """ c = gf.Component() flux_ref = c << fluxonium( pad_size=pad_size, pad_gap=pad_gap, junction_spec=junction_spec, junction_displacement=junction_displacement, junction_margin=junction_margin, inductor_n_turns=inductor_n_turns, inductor_margin_x=inductor_margin_x, inductor_cross_section=inductor_cross_section, connection_wire_width=connection_wire_width, layer_metal=layer_metal, inductor=inductor, inductor_array_spec=inductor_array_spec, ) flux_size = (flux_ref.size_info.width, flux_ref.size_info.height) bbox_size = ( flux_size[0] + 2 * bbox_extension, flux_size[1] + 2 * bbox_extension, ) bbox = gf.container( partial( gf.components.rectangle, size=bbox_size, layer=LAYER.M1_ETCH, ), partial( transform_component, transform=DCplxTrans(*(-e / 2 for e in bbox_size)) ), ) bbox = gf.boolean( A=bbox, B=c, operation="-", layer=LAYER.M1_ETCH, layer1=LAYER.M1_ETCH, layer2=LAYER.M1_DRAW, ) bbox_ref = c.add_ref(bbox) c.absorb(bbox_ref) c.add_ports(flux_ref.ports) return c