"""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,
)