Source code for qpdk.cells.snspd

"""Superconducting nanowire single-photon detector (SNSPD)."""

from __future__ import annotations

from typing import Literal

import gdsfactory as gf
import numpy as np
from gdsfactory.component import Component
from gdsfactory.typings import LayerSpec, Port, Size

from qpdk.tech import LAYER


[docs] @gf.cell(tags=("detectors",)) def snspd( wire_width: float = 0.2, wire_pitch: float = 0.6, size: Size = (10, 8), num_squares: int | None = None, turn_ratio: float = 4, terminals_same_side: bool = False, layer: LayerSpec = LAYER.NbTiN, port_type: Literal["electrical", "optical"] = "electrical", ) -> Component: """Creates an optimally-rounded SNSPD. .. svgbob:: e1 ─────────────────────╮ ╭───────────────────────╯ ╰───────────────────────╮ ╭───────────────────────╯ ╰───────────────────────╮ ╭───────────────────────╯ ╰───────────────────────╮ e2 ──────────────────╯ Args: wire_width: Width of the wire. wire_pitch: Distance between two adjacent wires. Must be greater than `width`. size: Float2 (width, height) of the rectangle formed by the outer boundary of the SNSPD. num_squares: int | None = None Total number of squares inside the SNSPD length. If given, overrides `size` with an approximately square SNSPD. The meander count is quantized to whole wire pitches, so the achieved square count (reported in `info["num_squares"]`) is close to but not exactly the requested one. turn_ratio: float Specifies how much of the SNSPD width is dedicated to the 180 degree turn. A `turn_ratio` of 10 will result in 20% of the width being comprised of the turn. terminals_same_side: If True, both ports will be located on the same side of the SNSPD. layer: layer spec to put polygon geometry on. port_type: type of port to add to the component (`"electrical"` or `"optical"`). Returns: A Component containing the SNSPD geometry. Raises: ValueError: If parameters are invalid or the SNSPD is too small for at least 3 meanders. """ if num_squares is not None: if num_squares <= 0: raise ValueError(f"num_squares={num_squares} must be a positive integer.") # num_squares overrides size: build a square SNSPD with the requested # total number of squares. The meander count below is quantized to # whole wire pitches, so the achieved square count (reported in # `info["num_squares"]`) is close to but not exactly the request. xy = np.sqrt(num_squares * wire_pitch * wire_width) size = (xy, xy) xsize, ysize = size if xsize <= 0 or ysize <= 0: raise ValueError(f"size={size} dimensions must be positive.") if wire_pitch <= wire_width: raise ValueError( f"wire_pitch={wire_pitch} must be greater than wire_width={wire_width}." ) num_meanders = int(np.ceil(ysize / wire_pitch)) if (not terminals_same_side and (num_meanders % 2) == 0) or ( terminals_same_side and (num_meanders % 2) == 1 ): num_meanders += 1 if num_meanders < 3: raise ValueError( f"num_meanders={num_meanders} is too small; the SNSPD needs at least " "3 meanders. Increase `size` or `num_squares`, or decrease `wire_pitch`." ) D = Component() hairpin = gf.c.optimal_hairpin( width=wire_width, pitch=wire_pitch, turn_ratio=turn_ratio, length=xsize / 2, num_pts=20, layer=layer, ) start_nw = D.add_ref(gf.c.compass(size=(xsize / 2, wire_width), layer=layer)) hp_prev = D.add_ref(hairpin) hp_prev.connect("e1", start_nw.ports["e3"]) alternate = True last_port: Port | None = None for _n in range(2, num_meanders): hp = D.add_ref(hairpin) if alternate: hp.connect("e2", hp_prev.ports["e2"]) else: hp.connect("e1", hp_prev.ports["e1"]) last_port = hp.ports["e2"] if terminals_same_side else hp.ports["e1"] hp_prev = hp alternate = not alternate finish_se = D.add_ref(gf.c.compass(size=(xsize / 2, wire_width), layer=layer)) if last_port is not None: finish_se.connect("e3", last_port) # The nanowire geometry itself only makes electrical connections # (`optimal_hairpin` has electrical ports), so honor `port_type` on the # exposed terminal ports. port_prefix = "e" if port_type == "electrical" else "o" D.add_port(port=start_nw.ports["e1"], name=f"{port_prefix}1", port_type=port_type) D.add_port(port=finish_se.ports["e1"], name=f"{port_prefix}2", port_type=port_type) D.info["num_squares"] = num_meanders * (xsize / wire_width) D.info["area"] = xsize * ysize D.info["xsize"] = xsize D.info["ysize"] = ysize D.flatten() return D