Source code for qpdk.cells.junction_array

"""Josephson junction array (junction chain) components."""

from __future__ import annotations

import itertools
import math
from typing import Literal

import gdsfactory as gf
import scipy.constants
from gdsfactory.component import Component
from gdsfactory.typings import LayerSpec
from klayout import db as kdb

from qpdk.cells._schematic import josephson_junction_array_schematic
from qpdk.tech import LAYER

__all__ = ["josephson_junction_array", "josephson_junction_array_compact"]

LayoutStyle = Literal["dolan", "manhattan"]

_DBU = gf.kcl.dbu
"""Database unit in µm."""
# Not qpdk.models.constants: importing qpdk.models pulls in jax, which the
# cells must not need.
_Φ_0 = scipy.constants.physical_constants["mag. flux quantum"][0]


def _manhattan_island(
    index: int,
    pitch: float,
    island_length: float,
    island_width: float,
    tip_width: float,
    *,
    first: bool,
    last: bool,
) -> list[kdb.DPolygon]:
    """Island of a Manhattan chain, centred at the origin, chain along +x.

    The island is a bar at +45° (even ``index``) or -45° (odd ``index``) to the
    chain axis. Its wide body has length ``island_length`` and width
    ``island_width``. Narrow fingers of width ``tip_width`` run from the body to
    the neighbouring junction and overshoot it by ``tip_width / 2``, so the two
    perpendicular fingers of adjacent islands overlap in a
    ``tip_width × tip_width`` square at the junction position.
    End islands omit the finger that would point away from the chain.

    Returns:
        The island polygons, rotated into the chain frame.
    """
    half_axis = pitch / math.sqrt(2)  # centre to junction, along the bar axis
    polygons = [
        kdb.DPolygon(
            kdb.DBox(
                -island_length / 2,
                -island_width / 2,
                island_length / 2,
                island_width / 2,
            )
        )
    ]
    # In the bar frame the +u finger points towards the next junction for even
    # islands (+45°: towards (+p/2, +p/2)) and the -u finger towards the previous.
    # For odd islands (-45°) the same holds after the rotation.
    if not last:
        polygons.append(
            kdb.DPolygon(
                kdb.DBox(
                    island_length / 2,
                    -tip_width / 2,
                    half_axis + tip_width / 2,
                    tip_width / 2,
                )
            )
        )
    if not first:
        polygons.append(
            kdb.DPolygon(
                kdb.DBox(
                    -half_axis - tip_width / 2,
                    -tip_width / 2,
                    -island_length / 2,
                    tip_width / 2,
                )
            )
        )
    angle = 45.0 if index % 2 == 0 else -45.0
    rotation = kdb.DCplxTrans(1.0, angle, False, 0.0, 0.0)
    return [rotation * polygon for polygon in polygons]


def _merged(polygons: list[kdb.DPolygon]) -> kdb.Region:
    """Merge polygons in µm into a database-unit region.

    Returns:
        The merged region.
    """
    region = kdb.Region()
    for polygon in polygons:
        region.insert(polygon.to_itype(_DBU))
    return region.merged()


def _check_island_contacts(islands: list[kdb.Region], *, junction_area: float) -> None:
    """Check that islands only touch their chain neighbours, as intended.

    Neighbouring islands must overlap by exactly ``junction_area`` (µm², 0 for
    a Dolan chain, where they must not touch at all), and islands further apart
    in the chain must not overlap. A row of the chain can only touch the rows
    next to it, so the non-neighbour check compares the sum of the island areas,
    less the neighbour overlaps, with the area of their union.

    Raises:
        ValueError: If islands short each other or a junction is malformed.
    """
    expected = junction_area / _DBU**2
    tolerance = 0.05 * expected + 4
    overlap_total = 0.0
    for index, (left, right) in enumerate(itertools.pairwise(islands)):
        overlap = (left & right).area()
        touching = not left.merged().interacting(right).is_empty()
        if (expected == 0 and touching) or abs(overlap - expected) > tolerance:
            raise ValueError(
                f"Junction {index} is malformed: islands {index} and {index + 1} "
                f"overlap by {overlap * _DBU**2:g} µm² instead of {junction_area:g} µm²"
            )
        overlap_total += overlap
    union = kdb.Region()
    for island in islands:
        union += island
    union_area = union.merged().area()
    disjoint_area = sum(island.area() for island in islands) - overlap_total
    if union_area < disjoint_area - tolerance:
        raise ValueError(
            "Islands that are not neighbours in the chain overlap: increase "
            "pitch or fold_spacing, or reduce island_length or island_width"
        )


[docs] @gf.cell( tags=["junctions", "inductors"], schematic_function=josephson_junction_array_schematic, ) def josephson_junction_array( n_junctions: int = 43, junction_area: float = 0.5, island_length: float = 0.3, island_width: float = 2.0, pitch: float = 0.465, layout_style: LayoutStyle = "dolan", n_folds: int = 0, fold_spacing: float = 4.0, lead_length: float = 1.0, ground_plane_distance: float | None = None, critical_current_density: float = 0.09, layer_junction: LayerSpec = LAYER.JJ_AREA, layer_patch: LayerSpec | None = LAYER.JJ_PATCH, layer_etch: LayerSpec = LAYER.M1_ETCH, ) -> Component: r"""Creates a chain of Josephson junctions in series between two ports. The array is used as a superinductance, for example in the fluxonium :cite:`manucharyan_fluxonium_2009,maslukMicrowaveCharacterizationJosephson2012`, or as a junction-chain transmission line. Island :math:`i` (:math:`i = 0, \dots, N`) sits at :math:`i p` along the unfolded chain and junction :math:`k` sits between islands :math:`k` and :math:`k + 1`. .. svgbob:: o1 ┌────┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌────┐ o2 ───┤lead├─┤ ├─┤ ├─┤ ├─ ... ─┤ ├─┤lead├─── └────┘ └─┘ └─┘ └─┘ └─┘ └────┘ │<p>│ island_length Layout styles: - ``"dolan"``: collinear islands of ``island_length`` × ``island_width`` separated by the bridge gap :math:`p - l`. This is the mask of a shadow-evaporated bridge chain. The junction overlap, ``junction_area / island_width``, comes from the evaporation angle and is not drawn. - ``"manhattan"``: overlap junctions on a straight chain. Islands are bars alternating at ±45° to the chain axis, i.e. along the two evaporation axes, so adjacent narrow tips of width :math:`\sqrt{A}` cross at right angles and draw the junction overlap :math:`A`. With ``n_folds > 0`` the chain zig-zags in ``n_folds + 1`` rows spaced by ``fold_spacing``. The junctions are split as evenly as possible over the rows, and the island at each row end is joined to the first island of the next row by a vertical connector (``island_length`` wide for Dolan, :math:`\sqrt{A}` for Manhattan). The drawn islands are checked for unintended contact: Dolan islands must not touch, and Manhattan islands may overlap only their chain neighbours, in exactly the junction area. The ``info`` dictionary reports the number of junctions, the total junction area :math:`N A`, the area and perimeter of one interior island (as drawn), the chain length :math:`N p`, the critical current :math:`I_\text{c} = J_\text{c} A` in nA, and the estimated inductances :math:`L_\text{J} = \Phi_0 / (2 \pi J_\text{c} A)` and :math:`L_\text{array} = N L_\text{J}` in nH, which neglect the kinetic and geometric inductance of the islands. Defaults reproduce the array of :cite:`manucharyan_fluxonium_2009`: :math:`N = 43` junctions of :math:`0.25 \times 2\,\mu\text{m}^2` in a :math:`20\,\mu\text{m}` chain, with :math:`J_\text{c}` set so that :math:`L_\text{array}` matches the reported :math:`E_L = 0.52\,\text{GHz}`. Args: n_junctions: Number of junctions :math:`N` in the chain. junction_area: Area :math:`A` of each junction in µm². island_length: Length :math:`l` of each island along the chain (Dolan) or of the wide body of each bar (Manhattan) in µm. island_width: Width of each island in µm. For Dolan this is also the junction width. pitch: Island-to-island pitch :math:`p` along the chain in µm. layout_style: ``"dolan"`` or ``"manhattan"``. n_folds: Number of folds. ``0`` draws a straight chain. fold_spacing: Centre-to-centre spacing between rows in µm. lead_length: Length of the contact lead at each end in µm. ground_plane_distance: If given, draw a ground-plane clearance on ``layer_etch`` that extends this far beyond the chain bounding box in µm. critical_current_density: Critical current density :math:`J_\text{c}` in µA/µm², used only for the ``info`` inductance estimate. layer_junction: Layer for the islands and leads. layer_patch: Layer for the contact patches on the leads, or ``None``. layer_etch: Layer for the optional ground-plane clearance. Returns: The junction array component with ports ``o1`` and ``o2``. Raises: ValueError: If the geometry is inconsistent. """ if n_junctions < 1: raise ValueError(f"n_junctions must be at least 1, got {n_junctions}") if n_folds < 0: raise ValueError(f"n_folds must be non-negative, got {n_folds}") n_rows = n_folds + 1 if n_junctions < n_rows: raise ValueError( f"n_junctions={n_junctions} is smaller than the number of rows {n_rows}" ) if min(junction_area, island_length, island_width, pitch, lead_length) <= 0: raise ValueError( "junction_area, island_length, island_width, pitch and lead_length " "must be positive" ) tip_width = math.sqrt(junction_area) match layout_style: case "dolan": if island_length >= pitch: raise ValueError( f"island_length={island_length} must be smaller than pitch={pitch} " "to leave a bridge gap" ) if junction_area / island_width > island_length: raise ValueError( f"junction overlap junction_area / island_width = " f"{junction_area / island_width:g} µm exceeds island_length" ) case "manhattan": clearance = (math.sqrt(2) * pitch - island_length) / 2 if clearance <= max(island_width, tip_width) / 2: raise ValueError( "Manhattan geometry overlaps: need " "(sqrt(2) * pitch - island_length) / 2 > " "max(island_width, sqrt(junction_area)) / 2, got " f"{clearance:g} µm <= {max(island_width, tip_width) / 2:g} µm" ) case _: raise ValueError(f"Unknown layout_style {layout_style!r}") def island(index: int, *, first: bool, last: bool) -> list[kdb.DPolygon]: if layout_style == "dolan": # End islands are drawn as interior islands; the leads are added below. return [ kdb.DPolygon( kdb.DBox( -island_length / 2, -island_width / 2, island_length / 2, island_width / 2, ) ) ] return _manhattan_island( index, pitch, island_length, island_width, tip_width, first=first, last=last ) # Lateral extent of an island along the chain axis, used to place the leads. interior = island(1, first=False, last=False) island_half_extent = max( abs(point.x) for polygon in island(0, first=False, last=False) for point in polygon.each_point_hull() ) row_height = ( max( _merged(island(index, first=False, last=False)).bbox().height() for index in (0, 1) ) * _DBU ) if n_folds and fold_spacing <= max(row_height, island_width): raise ValueError( f"fold_spacing={fold_spacing} must exceed the row height " f"{max(row_height, island_width):g} µm" ) patches: list[kdb.DPolygon] = [] island_index = 0 x_start = 0.0 port_specs: list[tuple[str, float, float, float]] = [] connector_width = island_length if layout_style == "dolan" else tip_width # Polygons of each island, for the contact check below. island_polygons: list[list[kdb.DPolygon]] = [[] for _ in range(n_junctions + 1)] # Split the junctions over the rows, earlier rows taking the remainder. base, extra = divmod(n_junctions, n_rows) junctions_per_row = [base + (1 if row < extra else 0) for row in range(n_rows)] for row, n_row in enumerate(junctions_per_row): direction = 1 if row % 2 == 0 else -1 y = -row * fold_spacing # Odd rows run in -x: mirror about the y axis (mirror about x, then 180°). mirror = kdb.DCplxTrans( 1.0, 0.0 if direction > 0 else 180.0, direction < 0, x_start, y ) for local in range(n_row + 1): global_index = island_index + local is_first = global_index == 0 is_last = global_index == n_junctions # Row-end islands are drawn in both rows. Only the chain ends omit # the outward finger; row turns keep only the finger inside the row. first = local == 0 last = local == n_row shift = kdb.DCplxTrans(local * pitch, 0.0) island_polygons[global_index].extend( mirror * shift * polygon for polygon in island(global_index, first=first, last=last) ) if is_first or is_last: outward = -1 if is_first else 1 x_island = local * pitch x_outer = x_island + outward * (island_half_extent + lead_length) lead = kdb.DPolygon( kdb.DBox(x_island, -island_width / 2, x_outer, island_width / 2) ) island_polygons[global_index].append(mirror * lead) patch = kdb.DPolygon( kdb.DBox( x_outer - outward * lead_length, -island_width / 2 - lead_length / 4, x_outer + outward * lead_length / 4, island_width / 2 + lead_length / 4, ) ) patches.append(mirror * patch) x_port = x_start + direction * x_outer orientation = 0.0 if direction * outward > 0 else 180.0 port_specs.append(("o1" if is_first else "o2", x_port, y, orientation)) x_end = x_start + direction * n_row * pitch if row < n_rows - 1: island_polygons[island_index + n_row].append( kdb.DPolygon( kdb.DBox( x_end - connector_width / 2, y - fold_spacing, x_end + connector_width / 2, y, ) ) ) island_index += n_row x_start = x_end islands = [_merged(polygons) for polygons in island_polygons] _check_island_contacts( islands, junction_area=junction_area if layout_style == "manhattan" else 0.0 ) c = Component() region = kdb.Region() for island_region in islands: region += island_region region.merge() c.add_polygon(region, layer=layer_junction) if layer_patch is not None: patch_region = kdb.Region() for polygon in patches: patch_region.insert(polygon.to_itype(_DBU)) patch_region.merge() c.add_polygon(patch_region, layer=layer_patch) for name, x, y, orientation in sorted(port_specs): c.add_port( name=name, center=(x, y), width=island_width, orientation=orientation, layer=layer_junction, ) if ground_plane_distance is not None: if ground_plane_distance <= 0: raise ValueError( f"ground_plane_distance must be positive, got {ground_plane_distance}" ) bbox = c.dbbox().enlarged(ground_plane_distance, ground_plane_distance) c.add_polygon(kdb.DPolygon(bbox), layer=layer_etch) interior_region = _merged(interior) island_area = interior_region.area() * _DBU**2 island_perimeter = interior_region.perimeter() * _DBU critical_current = critical_current_density * 1e-6 * junction_area # A junction_inductance = _Φ_0 / (2 * math.pi * critical_current) c.info["n_junctions"] = n_junctions c.info["layout_style"] = layout_style c.info["junction_area"] = junction_area c.info["total_junction_area"] = n_junctions * junction_area c.info["island_area"] = round(island_area, 6) c.info["island_perimeter"] = round(island_perimeter, 6) c.info["chain_length"] = round(n_junctions * pitch, 6) c.info["critical_current_nA"] = round(critical_current * 1e9, 6) c.info["junction_inductance_nH"] = round(junction_inductance * 1e9, 6) c.info["array_inductance_nH"] = round(n_junctions * junction_inductance * 1e9, 6) return c
[docs] @gf.cell( tags=["junctions", "inductors"], schematic_function=josephson_junction_array_schematic, ) def josephson_junction_array_compact( n_junctions: int = 43, junction_area: float = 0.5, island_length: float = 0.3, island_width: float = 2.0, pitch: float = 0.465, layout_style: LayoutStyle = "dolan", n_folds: int = 2, fold_spacing: float = 4.0, lead_length: float = 1.0, ground_plane_distance: float | None = None, critical_current_density: float = 0.09, layer_junction: LayerSpec = LAYER.JJ_AREA, layer_patch: LayerSpec | None = LAYER.JJ_PATCH, layer_etch: LayerSpec = LAYER.M1_ETCH, ) -> Component: r"""Creates a folded Josephson junction array that fits a compact bounding box. Same as :func:`josephson_junction_array` with ``n_folds`` defaulting to 2, so the chain zig-zags in three rows and both ports face away from each other. .. svgbob:: o1 ──┤├┤├┤├┤├┤├─┐ │ ┌─┤├┤├┤├┤├─┘ │ └─┤├┤├┤├┤├┤├── o2 See :cite:`manucharyan_fluxonium_2009,maslukMicrowaveCharacterizationJosephson2012`. Args: n_junctions: Number of junctions :math:`N` in the chain. junction_area: Area :math:`A` of each junction in µm². island_length: Length of each island along the chain in µm. island_width: Width of each island in µm. pitch: Island-to-island pitch along the chain in µm. layout_style: ``"dolan"`` or ``"manhattan"``. n_folds: Number of folds. fold_spacing: Centre-to-centre spacing between rows in µm. lead_length: Length of the contact lead at each end in µm. ground_plane_distance: Optional ground-plane clearance in µm. critical_current_density: Critical current density in µA/µm². layer_junction: Layer for the islands and leads. layer_patch: Layer for the contact patches on the leads, or ``None``. layer_etch: Layer for the optional ground-plane clearance. Returns: The folded junction array component. """ c = Component() array = josephson_junction_array( n_junctions=n_junctions, junction_area=junction_area, island_length=island_length, island_width=island_width, pitch=pitch, layout_style=layout_style, n_folds=n_folds, fold_spacing=fold_spacing, lead_length=lead_length, ground_plane_distance=ground_plane_distance, critical_current_density=critical_current_density, layer_junction=layer_junction, layer_patch=layer_patch, layer_etch=layer_etch, ) ref = c << array c.add_ports(ref.ports) c.info.update(array.info.model_dump()) c.flatten() return c