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._schematic import (
    lumped_element_resonator_schematic,
    meander_inductor_schematic,
)
from qpdk.cells.waveguides import straight
from qpdk.tech import (
    get_etch_section,
    get_meander_wire_gap,
    meander_inductor_cross_section,
)


[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, ) -> 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. Returns: Component: A gdsfactory component with the meander inductor geometry and two ports ('o1' and 'o2'). Raises: ValueError: If `n_turns` < 1, `turn_length` <= 0, or `wire_gap` is not positive. """ 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 # 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 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_name"] = xs.name return c
meander_inductor.schematic_function = meander_inductor_schematic
[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
lumped_element_resonator.schematic_function = lumped_element_resonator_schematic 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, )