Source code for qpdk.cells.inductor

"""Inductor and lumped-element resonator components."""

from __future__ import annotations

from math import ceil, floor, sqrt

import gdsfactory as gf
from gdsfactory.component import Component
from gdsfactory.typings import CrossSectionSpec, LayerSpec

from qpdk.cells._schematic import (
    lumped_element_resonator_schematic,
    meander_inductor_schematic,
)
from qpdk.cells.waveguides import straight
from qpdk.tech import (
    MEANDER_SQUARES_CONVENTIONS,
    MeanderCornerStyle,
    get_etch_section,
    get_meander_wire_gap,
    meander_inductor_cross_section,
    meander_n_squares_drawn,
    meander_round_radius,
    optimal_hairpin_arm_length,
)


[docs] @gf.cell(tags=["inductors"], schematic_function=meander_inductor_schematic) def meander_inductor( n_turns: int = 5, turn_length: float = 200.0, cross_section: CrossSectionSpec = meander_inductor_cross_section, wire_gap: float | None = None, etch_bbox_margin: float = 2.0, add_etch: bool = True, corner_style: MeanderCornerStyle = "sharp", radius: float | None = None, ) -> Component: r"""Creates a meander inductor with Manhattan routing using a narrow wire. The inductor consists of multiple horizontal runs connected by short vertical segments at alternating ends, forming a serpentine (meander) path. The total inductance is dominated by kinetic inductance for superconducting thin films. .. svgbob:: o1 ─────────────────────┐ │ ┌───────────────────────┘ │ └───────────────────────┐ │ ┌───────────────────────┘ │ └────────────────────── o2 Similar structures are described in :cite:`kimThinfilmSuperconductingResonator2011,chenCompactInductorcapacitorResonators2023`. Args: n_turns: Number of horizontal meander runs (must be >= 1). turn_length: Length of each horizontal run in µm. cross_section: Cross-section specification for the meander wire. The center conductor width and etch gap are derived from this specification. The meander's vertical pitch is the wire width plus the run-to-run gap, which defaults to twice the etch gap so that the etched regions of adjacent runs do not overlap, maintaining the characteristic impedance of each run. wire_gap: Optional explicit gap between adjacent inductor runs in µm. If None (default), it's inferred as 2x the etch gap from the cross-section, or the wire width if it has no etch section. etch_bbox_margin: Extra margin around the inductor for the etch bounding box in µm. This margin is added in addition to the etch region defined in the cross-section. add_etch: Whether to add the etch bounding box. Defaults to True. corner_style: Shape of the U-turn corners, all within the same bounding box: * ``"sharp"``: Manhattan corners (the original geometry). * ``"chamfer"``: the outer corner of each 90° bend is cut at 45° so the wire keeps its width :math:`w` along the diagonal. * ``"round"``: circular arcs of centreline radius ``radius``. * ``"optimal"``: the current-crowding-free hairpin contour of :cite:`clemGeometrydependentCriticalCurrents2011`, drawn with :func:`gdsfactory.components.optimal_hairpin`. Each hairpin spans half a run. radius: Centreline radius of ``"round"`` corners in µm, with :math:`w/2 < R \\le p/2` for pitch :math:`p`. Defaults to :math:`p/2`, which makes each U-turn a semicircle. Ignored for other styles. Returns: Component: A gdsfactory component with the meander inductor geometry and two ports ('o1' and 'o2'). ``info`` holds * ``total_wire_length``: centreline length of the sharp meander in µm. * ``n_squares``: ``total_wire_length / w``, which counts every corner square as one square. The SAX model uses this value. * ``n_squares_drawn``: squares of the drawn polygon for the chosen ``corner_style``, from :func:`qpdk.tech.meander_n_squares_drawn`. A sharp 90° corner counts 0.559 squares :cite:`hagedornRightAngleBendsThin1963`. * ``n_squares_convention``: the corner rule used for ``n_squares_drawn``. Raises: ValueError: If `n_turns` < 1, `turn_length` <= 0, `wire_gap` is not positive, `radius` is out of range, the runs are too short for the chosen corners, or `corner_style` is unknown. """ if n_turns < 1: raise ValueError("Must have at least 1 turn") if turn_length <= 0: raise ValueError(f"turn_length must be positive, got {turn_length}") xs = gf.get_cross_section(cross_section) wire_width = xs.width layer = xs.layer # Infer etch parameters and spacing from cross section try: etch_section = get_etch_section(xs) except ValueError: etch_section = None etch_layer = etch_section.layer if etch_section is not None else None # Same rule as the SAX model: explicit gap wins, else 2x etch width, # else the wire width when there is no etch section wire_gap = get_meander_wire_gap(xs, wire_gap) c = Component() pitch = wire_width + wire_gap total_height = n_turns * wire_width + max(0, n_turns - 1) * wire_gap match corner_style: case "sharp": _draw_sharp_meander(c, n_turns, turn_length, wire_width, pitch, layer) case "chamfer": cut = (2 - sqrt(2)) * wire_width if n_turns > 1 and turn_length < cut: raise ValueError( f"turn_length={turn_length} is too short for chamfered corners; " f"need at least {cut:.4g}" ) _draw_chamfer_meander(c, n_turns, turn_length, wire_width, pitch, layer) case "round": r = meander_round_radius(wire_width, wire_gap, radius) _draw_round_meander(c, n_turns, turn_length, wire_width, pitch, r, layer) case "optimal": _draw_optimal_meander(c, n_turns, turn_length, wire_width, pitch, layer) case _: raise ValueError(f"Unknown corner_style {corner_style!r}") if corner_style != "sharp": c.flatten() if add_etch and etch_section is not None: # Extra margin on top of the implicit etch margin from the cross-section margin = etch_section.width + etch_bbox_margin c.add_polygon( [ (-margin, -margin), (turn_length + margin, -margin), (turn_length + margin, total_height + margin), (-margin, total_height + margin), ], layer=etch_layer, ) c_metal = gf.boolean( A=c, B=c, operation="or", layer=layer, layer1=layer, layer2=layer ) c_etch = gf.boolean( A=c, B=c_metal, operation="A-B", layer=etch_layer, layer1=etch_layer, layer2=layer, ) c = gf.Component() c.absorb(c << c_metal) c.absorb(c << c_etch) c.add_port( name="o1", center=(0, wire_width / 2), width=wire_width, orientation=180, layer=layer, cross_section=xs, ) last_run_center_y = (n_turns - 1) * pitch + wire_width / 2 if n_turns % 2 == 1: c.add_port( name="o2", center=(turn_length, last_run_center_y), width=wire_width, orientation=0, layer=layer, cross_section=xs, ) else: c.add_port( name="o2", center=(0, last_run_center_y), width=wire_width, orientation=180, layer=layer, cross_section=xs, ) c.move((-turn_length / 2, -total_height / 2)) total_wire_length = n_turns * turn_length + max(0, n_turns - 1) * wire_gap c.info["total_wire_length"] = total_wire_length c.info["n_squares"] = total_wire_length / wire_width c.info["n_squares_drawn"] = meander_n_squares_drawn( n_turns, turn_length, wire_width, wire_gap, corner_style, radius ) c.info["n_squares_convention"] = MEANDER_SQUARES_CONVENTIONS[corner_style] c.info["cross_section_name"] = xs.name return c
def _draw_sharp_meander( c: Component, n_turns: int, turn_length: float, wire_width: float, pitch: float, layer: LayerSpec, ) -> None: """Draws Manhattan runs and vertical connectors, run 0 at y = 0.""" for i in range(n_turns): y0 = i * pitch c.add_polygon( [ (0, y0), (turn_length, y0), (turn_length, y0 + wire_width), (0, y0 + wire_width), ], layer=layer, ) for i in range(n_turns - 1): y0 = i * pitch + wire_width y1 = (i + 1) * pitch if i % 2 == 0: c.add_polygon( [ (turn_length - wire_width, y0), (turn_length, y0), (turn_length, y1), (turn_length - wire_width, y1), ], layer=layer, ) else: c.add_polygon( [(0, y0), (wire_width, y0), (wire_width, y1), (0, y1)], layer=layer, ) def _draw_chamfer_meander( c: Component, n_turns: int, turn_length: float, wire_width: float, pitch: float, layer: LayerSpec, ) -> None: r"""Draws the sharp meander with each outer bend corner cut at 45°. The cut triangle has legs :math:`(2 - \sqrt{2})\,w`, so the wire width across the diagonal stays :math:`w`. """ cut = (2 - sqrt(2)) * wire_width length, w = turn_length, wire_width for i in range(n_turns): y0 = i * pitch y1 = y0 + w bend_right_below = i % 2 == 0 and i < n_turns - 1 bend_right_above = i % 2 == 1 bend_left_below = i % 2 == 1 and i < n_turns - 1 bend_left_above = i % 2 == 0 and i >= 2 pts = [(0.0, y0)] if not bend_left_below else [(cut, y0)] if bend_right_below: pts += [(length - cut, y0), (length, y0 + cut)] else: pts += [(length, y0)] if bend_right_above: pts += [(length, y1 - cut), (length - cut, y1)] else: pts += [(length, y1)] if bend_left_above: pts += [(cut, y1), (0.0, y1 - cut)] else: pts += [(0.0, y1)] if bend_left_below: pts += [(0.0, y0 + cut)] c.add_polygon(pts, layer=layer) for i in range(n_turns - 1): y0 = i * pitch + w y1 = (i + 1) * pitch x0 = length - w if i % 2 == 0 else 0.0 c.add_polygon([(x0, y0), (x0 + w, y0), (x0 + w, y1), (x0, y1)], layer=layer) def _draw_round_meander( c: Component, n_turns: int, turn_length: float, wire_width: float, pitch: float, radius: float, layer: LayerSpec, ) -> None: """Draws the meander as one extruded path with circular U-turns. The arcs are placed so that the outer wire edges stay on the bounding box of the sharp meander. Raises: ValueError: If the runs are too short for the radius. """ w, r = wire_width, radius first_last = turn_length - w / 2 - r middle = turn_length - w - 2 * r vertical = pitch - 2 * r if n_turns > 1 and (first_last < 0 or (n_turns > 2 and middle < 0)): minimum = w + 2 * r if n_turns > 2 else w / 2 + r raise ValueError( f"turn_length={turn_length} is too short for round corners of " f"radius {r}; need at least {minimum}" ) def _straight(path: gf.Path, length: float) -> gf.Path: return path + gf.path.straight(length=length) if length > 0 else path path = gf.Path([(0, 0)]) if n_turns == 1: path = _straight(path, turn_length) else: for i in range(n_turns): run = first_last if i in {0, n_turns - 1} else middle path = _straight(path, run) if i == n_turns - 1: break angle = 90 if i % 2 == 0 else -90 path += gf.path.arc(radius=r, angle=angle) path = _straight(path, vertical) path += gf.path.arc(radius=r, angle=angle) wire = gf.path.extrude(path, layer=layer, width=w) ref = c << wire ref.move((0, w / 2)) def _draw_optimal_meander( c: Component, n_turns: int, turn_length: float, wire_width: float, pitch: float, layer: LayerSpec, ) -> None: """Draws the meander from chained Clem-Berggren hairpins. Each U-turn is one :func:`gdsfactory.components.optimal_hairpin` spanning half a run. Adjacent hairpins meet at the middle of the shared run. """ w = wire_width if n_turns == 1: c.add_polygon([(0, 0), (turn_length, 0), (turn_length, w), (0, w)], layer=layer) return # Validates that the hairpin fits; raises ValueError otherwise. optimal_hairpin_arm_length(turn_length, w, pitch) hairpin = gf.c.optimal_hairpin( width=w, pitch=pitch, length=turn_length / 2, turn_ratio=(pitch + w) / w, layer=layer, ) refs = [] for i in range(n_turns - 1): ref = c << hairpin if i == 0: ref.move(ref.ports["e2"].center, (turn_length / 2, w / 2)) elif i % 2 == 1: ref.connect("e1", refs[-1].ports["e1"]) else: ref.connect("e2", refs[-1].ports["e2"]) refs.append(ref) half = turn_length / 2 c.add_polygon([(0, 0), (half, 0), (half, w), (0, w)], layer=layer) y0 = (n_turns - 1) * pitch x0 = 0.0 if n_turns % 2 == 0 else half c.add_polygon( [(x0, y0), (x0 + half, y0), (x0 + half, y0 + w), (x0, y0 + w)], layer=layer )
[docs] @gf.cell( tags=["resonators", "inductors", "capacitors"], schematic_function=lumped_element_resonator_schematic, ) def lumped_element_resonator( fingers: int = 20, finger_length: float = 20.0, finger_gap: float = 2.0, finger_thickness: float = 5.0, n_turns: int = 15, bus_bar_spacing: float = 4.0, cross_section: CrossSectionSpec = meander_inductor_cross_section, etch_bbox_margin: float = 2.0, ) -> Component: r"""Creates a lumped-element resonator combining an interdigital capacitor and a meander inductor. The resonator consists of an interdigital capacitor section (providing capacitance) connected in parallel with a meander inductor section (providing inductance) via shared bus bars. The resonance frequency is: .. math:: f_\text{r} = \frac{1}{2\pi\sqrt{LC}} .. svgbob:: +-----------+ | Capacitor | o1 --+ (IDC) +-- o2 | | | Inductor | | (Meander) | +-----------+ Similar structures are described in :cite:`kimThinfilmSuperconductingResonator2011,chenCompactInductorcapacitorResonators2023`. Args: fingers: Number of interdigital capacitor fingers. finger_length: Length of each capacitor finger in µm. finger_gap: Gap between adjacent capacitor fingers in µm. finger_thickness: Width of each capacitor finger and bus bar in µm. n_turns: Number of horizontal meander inductor runs. bus_bar_spacing: Vertical spacing between the capacitor and inductor sections in µm. cross_section: Cross-section specification for the inductor and ports. etch_bbox_margin: Margin around the structure for the etch region in µm. Returns: Component: A gdsfactory component with the lumped-element resonator geometry and two ports ('o1' and 'o2'). Raises: ValueError: If `n_turns` is even, `bus_bar_spacing` <= 0, or if the resultant meander run length is non-positive. """ if n_turns % 2 == 0: raise ValueError( "n_turns must be odd so that the meander path spans from the " "left bus bar to the right bus bar" ) if bus_bar_spacing <= 0: raise ValueError( "bus_bar_spacing must be positive to electrically isolate the " "last inductor run from the full-width bus bar sections" ) xs = gf.get_cross_section(cross_section) wire_width = xs.width etch_section = get_etch_section(xs) wire_gap = 2 * etch_section.width layer = xs.layer etch_layer = etch_section.layer etch_width = etch_section.width cap_width = 2 * finger_thickness + finger_length + finger_gap short_length = cap_width - 4 * wire_width if short_length <= 0: raise ValueError( f"Meander run length would be non-positive ({short_length} µm). " "Increase finger_length/finger_gap/finger_thickness or decrease wire_width." ) c = Component() # 1. Inductor part ind = c << meander_inductor( n_turns=n_turns, turn_length=short_length, cross_section=cross_section, etch_bbox_margin=0, ) cap_height = fingers * finger_thickness + (fingers - 1) * finger_gap ind_height = ind.size_info.height total_internal_height = cap_height + bus_bar_spacing + ind_height # Center inductor at the bottom of the internal area ind.dcenter = (0, -total_internal_height / 2 + ind_height / 2) # 2. Capacitor part (fingers and bus bars) cap_y0 = -total_internal_height / 2 + ind_height + bus_bar_spacing x_left_inner = -cap_width / 2 + finger_thickness x_right_inner = cap_width / 2 - finger_thickness _draw_interdigital_fingers_left( c, layer, x_inner=x_left_inner, y_offset=cap_y0, fingers=fingers, finger_length=finger_length, finger_gap=finger_gap, thickness=finger_thickness, ) _draw_interdigital_fingers_right( c, layer, x_inner=x_right_inner, y_offset=cap_y0, fingers=fingers, finger_length=finger_length, finger_gap=finger_gap, thickness=finger_thickness, ) # 3. Bus bars connecting everything # Small overlap to ensure solid connectivity overlap = 0.1 # Left bus bar: connects to turn 0 (bottom) # Use the metal bottom edge of the inductor, not the component bbox bottom (which includes etch) left_bb_ymin = ind.ports["o1"].center[1] - wire_width / 2 c.add_polygon( [ (-cap_width / 2, left_bb_ymin), (-cap_width / 2 + wire_width, left_bb_ymin), (-cap_width / 2 + wire_width, cap_y0 + overlap), (-cap_width / 2, cap_y0 + overlap), ], layer=layer, ) # Top wide part c.add_polygon( [ (-cap_width / 2, cap_y0), (-cap_width / 2 + finger_thickness, cap_y0), (-cap_width / 2 + finger_thickness, total_internal_height / 2), (-cap_width / 2, total_internal_height / 2), ], layer=layer, ) # Right bus bar: connects to turn n_turns-1 (top) # Redundant section below top run is removed right_bb_ymin = ind.ports["o2"].center[1] - wire_width / 2 c.add_polygon( [ (cap_width / 2 - wire_width, right_bb_ymin), (cap_width / 2, right_bb_ymin), (cap_width / 2, cap_y0 + overlap), (cap_width / 2 - wire_width, cap_y0 + overlap), ], layer=layer, ) # Top wide part c.add_polygon( [ (cap_width / 2 - finger_thickness, cap_y0), (cap_width / 2, cap_y0), (cap_width / 2, total_internal_height / 2), (cap_width / 2 - finger_thickness, total_internal_height / 2), ], layer=layer, ) # Tabs to inductor # Left tab connects o1 to the left bus bar c.add_polygon( [ ( -cap_width / 2 + wire_width - overlap, ind.ports["o1"].center[1] - wire_width / 2, ), ( ind.ports["o1"].center[0] + overlap, ind.ports["o1"].center[1] - wire_width / 2, ), ( ind.ports["o1"].center[0] + overlap, ind.ports["o1"].center[1] + wire_width / 2, ), ( -cap_width / 2 + wire_width - overlap, ind.ports["o1"].center[1] + wire_width / 2, ), ], layer=layer, ) # Right tab connects o2 to the right bus bar c.add_polygon( [ ( ind.ports["o2"].center[0] - overlap, ind.ports["o2"].center[1] - wire_width / 2, ), ( cap_width / 2 - wire_width + overlap, ind.ports["o2"].center[1] - wire_width / 2, ), ( cap_width / 2 - wire_width + overlap, ind.ports["o2"].center[1] + wire_width / 2, ), ( ind.ports["o2"].center[0] - overlap, ind.ports["o2"].center[1] + wire_width / 2, ), ], layer=layer, ) # 4. Etch bounding box margin = etch_width + etch_bbox_margin c.add_polygon( [ (-cap_width / 2 - margin, -total_internal_height / 2 - margin), (cap_width / 2 + margin, -total_internal_height / 2 - margin), (cap_width / 2 + margin, total_internal_height / 2 + margin), (-cap_width / 2 - margin, total_internal_height / 2 + margin), ], layer=etch_layer, ) # 5. Ports straight_out = straight(length=margin, cross_section=cross_section) center_y = 0 straight_left = c.add_ref(straight_out).move((-cap_width / 2 - margin, center_y)) straight_right = c.add_ref(straight_out).move((cap_width / 2, center_y)) c_metal = gf.boolean( A=c, B=c, operation="or", layer=layer, layer1=layer, layer2=xs.layer ) c_etch = gf.boolean( A=c, B=c_metal, operation="A-B", layer=etch_layer, layer1=etch_layer, layer2=layer, ) c = gf.Component() c.absorb(c << c_metal) c.absorb(c << c_etch) c.add_port( name="o1", port=straight_left.ports["o1"], layer=layer, port_type="optical", cross_section=xs, ) c.add_port( name="o2", port=straight_right.ports["o2"], layer=layer, port_type="optical", cross_section=xs, ) c.info["total_wire_length"] = ( 2 * wire_width + n_turns * short_length + max(0, n_turns - 1) * wire_gap ) c.info["inductor_n_squares"] = c.info["total_wire_length"] / wire_width c.info["capacitor_fingers"] = fingers c.info["capacitor_finger_length"] = finger_length return c
def _draw_interdigital_fingers_left( c: Component, layer: LayerSpec, x_inner: float, y_offset: float, fingers: int, finger_length: float, finger_gap: float, thickness: float, ) -> None: """Draw left-side interdigital capacitor fingers (even-indexed, extending right).""" for i in range(ceil(fingers / 2)): finger_idx = 2 * i y0 = y_offset + finger_idx * (thickness + finger_gap) c.add_polygon( [ (x_inner, y0), (x_inner + finger_length, y0), (x_inner + finger_length, y0 + thickness), (x_inner, y0 + thickness), ], layer=layer, ) def _draw_interdigital_fingers_right( c: Component, layer: LayerSpec, x_inner: float, y_offset: float, fingers: int, finger_length: float, finger_gap: float, thickness: float, ) -> None: """Draw right-side interdigital capacitor fingers (odd-indexed, extending left).""" for i in range(floor(fingers / 2)): finger_idx = 1 + 2 * i y0 = y_offset + finger_idx * (thickness + finger_gap) c.add_polygon( [ (x_inner - finger_length, y0), (x_inner, y0), (x_inner, y0 + thickness), (x_inner - finger_length, y0 + thickness), ], layer=layer, )