Source code for qpdk.cells.inductor

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

from __future__ import annotations

from math import ceil, floor

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

from qpdk.cells.waveguides import straight
from qpdk.tech import (
    get_etch_section,
    meander_inductor_cross_section,
)


[docs] @gf.cell(tags=("inductors",)) 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, ) -> 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 set to ensure that the etched regions of adjacent runs do not overlap, maintaining the characteristic impedance of each run. Specifically, the pitch is calculated as :math:`w + 2g`, where :math:`w` is the wire width and :math:`g` is the etch gap. 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. 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. Returns: Component: A gdsfactory component with the meander inductor geometry and two ports ('o1' and 'o2'). Raises: ValueError: If `n_turns` < 1 or `turn_length` <= 0. """ 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) etch_layer = etch_section.layer except ValueError: etch_section = None etch_layer = None # For CPW-like structures, we assume a pitch that allows for non-overlapping etches # i.e. pitch = width + 2 * gap, which means wire_gap = 2 * etch_width # If no etch section is found, we use a default gap equal to the wire width if wire_gap is None: wire_gap = 2 * etch_section.width if etch_section is not None else wire_width c = Component() pitch = wire_width + wire_gap total_height = n_turns * wire_width + max(0, n_turns - 1) * wire_gap 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, ) 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["cross_section"] = xs.name return c
[docs] @gf.cell(tags=("resonators", "inductors", "capacitors")) 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_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="electrical", cross_section=xs, ) c.add_port( name="o2", port=straight_right.ports["o2"], layer=layer, port_type="electrical", 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, ) if __name__ == "__main__": from qpdk.helper import show_components show_components( meander_inductor, lumped_element_resonator, )