Source code for qpdk.cells.capacitor

"""Capacitive coupler components."""

from __future__ import annotations

from itertools import chain
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 add_etch_gap, bend_circular, straight
from qpdk.tech import LAYER, get_etch_section, get_etch_sections
from qpdk.utils import merge_layers_with_etch as _merge_layers_with_etch


[docs] @gf.cell(tags=("capacitors", "couplers")) def half_circle_coupler( radius: float = 50.0, angle: float = 180.0, extension_length: float = 10.0, cross_section: CrossSectionSpec = "cpw", extra_straight_length: float = 20.0, open_end: bool = True, ) -> Component: """Creates a half-circle coupler for readout. This coupler consists of a circular bend (typically 180 degrees) that wraps around a resonator arm for capacitive coupling. Args: radius: Inner radius of the half-circle in μm. angle: Angle of the circular arc in degrees. extension_length: Length of the straight sections extending from the ends of the half-circle in μm. cross_section: Cross-section specification for the coupler. extra_straight_length: Length of the straight section extending from the bottom of the half-circle in μm. open_end: If True, adds an etched gap at the ends of the extensions. Returns: Component: A gdsfactory component with the half-circle coupler geometry. """ c = Component() bend = c.add_ref( bend_circular( radius=radius, angle=angle, cross_section=cross_section, ) ) # Position bend such that it's centered and opening upwards bend.rotate(-angle / 2) bend.move((-bend.dcenter[0], -bend.dcenter[1])) # Add extensions to the ends of the bend if extension_length > 0: ext1 = c.add_ref(straight(length=extension_length, cross_section=cross_section)) ext1.connect("o1", bend.ports["o1"]) ext2 = c.add_ref(straight(length=extension_length, cross_section=cross_section)) ext2.connect("o1", bend.ports["o2"]) where_to_add_gaps = [ext1.ports["o2"], ext2.ports["o2"]] else: where_to_add_gaps = [bend.ports["o1"], bend.ports["o2"]] if open_end: for port in where_to_add_gaps: add_etch_gap(c, port, cross_section=cross_section) # Get cross section details to calculate overlap xs = gf.get_cross_section(cross_section) cross_section_etch_section = get_etch_section(xs) # Ensure significant overlap by moving stem into the bend metal # and considering the bend radius overlap = xs.width / 2 + cross_section_etch_section.width # Add a stem/lead straight from the center of the arc. # The stem uses the CPW cross-section but does NOT extend into the bend, # to avoid M1_ETCH overlapping with the bend's M1_DRAW. stem = c.add_ref( straight( length=extra_straight_length, cross_section=cross_section, ) ) stem.rotate(-90) stem.movey(bend.dbbox().bottom) stem.movex(bend.dcenter[0]) # Center it # Bridge the stem into the bend with M1_DRAW center conductor and # M1_ETCH gap sections, matching the CPW cross-section pattern. # Unlike using a straight ref, these polygons avoid hierarchical # M1_ETCH-over-M1_DRAW conflicts with the bend. bridge_x = bend.dcenter[0] bridge_y_bottom = bend.dbbox().bottom bridge_y_top = bridge_y_bottom + overlap c.add_polygon( [ (bridge_x - xs.width / 2, bridge_y_bottom), (bridge_x + xs.width / 2, bridge_y_bottom), (bridge_x + xs.width / 2, bridge_y_top), (bridge_x - xs.width / 2, bridge_y_top), ], layer=LAYER.M1_DRAW, ) etch_height = overlap / 3 for etch_s in get_etch_sections(xs): etch_x_center = bridge_x + etch_s.offset c.add_polygon( [ (etch_x_center - etch_s.width / 2, bridge_y_bottom), (etch_x_center + etch_s.width / 2, bridge_y_bottom), (etch_x_center + etch_s.width / 2, bridge_y_bottom + etch_height), (etch_x_center - etch_s.width / 2, bridge_y_bottom + etch_height), ], layer=LAYER.M1_ETCH, ) c.add_port("o3", port=stem.ports["o2"]) # Place anchor at the arc center, computed as the midpoint of the # bend ports for a circular arc, for concentric alignment with an # inner resonator bend. arc_center_x = (bend.ports["o1"].dx + bend.ports["o2"].dx) / 2 arc_center_y = (bend.ports["o1"].dy + bend.ports["o2"].dy) / 2 c.add_port( name="anchor", center=(arc_center_x, arc_center_y), width=xs.width, orientation=90, layer=LAYER.M1_DRAW, port_type="placement", ) return c
[docs] @gf.cell(tags=("capacitors",)) def interdigital_capacitor( fingers: int = 4, finger_length: float = 20.0, finger_gap: float = 2.0, thickness: float = 5.0, layer_metal: LayerSpec = LAYER.M1_DRAW, etch_layer: LayerSpec | None = "M1_ETCH", etch_bbox_margin: float = 2.0, cross_section: CrossSectionSpec = "cpw", half: bool = False, ) -> Component: """Generate an interdigital capacitor component with ports on both ends. An interdigital capacitor consists of interleaved metal fingers that create a distributed capacitance. This component creates a planar capacitor with two sets of interleaved fingers extending from opposite ends. .. svgbob:: ┌─┐───────┐┌─┐ │ │───────┘│ │ │ │ ┌──────│ │ ┌│ │ └──────│ │┐ o1└│ │──────┐ │ │┘o2 │ │──────┘ │ │ │ │ ┌──────│ │ └─┘ └──────└─┘ See for example :cite:`leizhuAccurateCircuitModel2000`. Note: ``finger_length=0`` effectively provides a parallel plate capacitor. The capacitance scales approximately linearly with the number of fingers and finger length. Args: fingers: Total number of fingers of the capacitor (must be >= 1). finger_length: Length of each finger in μm. finger_gap: Gap between adjacent fingers in μm. thickness: Thickness of fingers and the base section in μm. layer_metal: Layer for the metal fingers. etch_layer: Optional layer for etching around the capacitor. etch_bbox_margin: Margin around the capacitor for the etch layer in μm. cross_section: Cross-section for the short straight from the etch box capacitor. half: If True, creates a single-sided capacitor (half of the interdigital capacitor). Returns: Component: A gdsfactory component with the interdigital capacitor geometry and two ports ('o1' and 'o2') on opposing sides. Raises: ValueError: If fingers is less than 1. """ c = Component() if fingers < 1: raise ValueError("Must have at least 1 finger") width = ( 2 * thickness + finger_length + finger_gap if not half else thickness + finger_length ) # total length height = fingers * thickness + (fingers - 1) * finger_gap # total height points_1 = [ (0.0, 0.0), (0, height), (thickness + finger_length, height), (thickness + finger_length, height - thickness), (thickness, height - thickness), *chain.from_iterable( ( (thickness, height - (2 * i) * (thickness + finger_gap)), ( thickness + finger_length, height - (2 * i) * (thickness + finger_gap), ), ( thickness + finger_length, height - (2 * i) * (thickness + finger_gap) - thickness, ), (thickness, height - (2 * i) * (thickness + finger_gap) - thickness), ) for i in range(ceil(fingers / 2)) ), (thickness, 0), (0.0, 0.0), ] c.add_polygon(points_1, layer=layer_metal) if not half: points_2 = [ (width, 0), (width, height), (width - thickness, height), *chain.from_iterable( ( ( width - thickness, height - (1 + 2 * i) * thickness - (1 + 2 * i) * finger_gap, ), ( width - (thickness + finger_length), height - (1 + 2 * i) * thickness - (1 + 2 * i) * finger_gap, ), ( width - (thickness + finger_length), height - (2 + 2 * i) * thickness - (1 + 2 * i) * finger_gap, ), ( width - thickness, height - (2 + 2 * i) * thickness - (1 + 2 * i) * finger_gap, ), ) for i in range(floor(fingers / 2)) ), (width - thickness, 0), (width, 0), ] c.add_polygon(points_2, layer=layer_metal) # Add etch layer bbox if specified if etch_layer is not None: etch_bbox = [ (-etch_bbox_margin, -etch_bbox_margin), (width + etch_bbox_margin, -etch_bbox_margin), (width + etch_bbox_margin, height + etch_bbox_margin), (-etch_bbox_margin, height + etch_bbox_margin), ] c.add_polygon(etch_bbox, layer=etch_layer) # Add small straights on the left and right sides of the capacitor straight_cross_section = gf.get_cross_section(cross_section) straight_out_of_etch = straight( length=etch_bbox_margin, cross_section=straight_cross_section ) straight_left = c.add_ref(straight_out_of_etch).move(( -etch_bbox_margin, height / 2, )) straight_right = None if not half: straight_right = c.add_ref(straight_out_of_etch).move((width, height / 2)) # Merge WG marker layer with draw metal and create etch negative c = _merge_layers_with_etch( component=c, draw_layer=layer_metal, wg_layer=straight_cross_section.layer, etch_layer=etch_layer, ) ports_config: list[tuple[str, gf.Port] | None] = [ ("o1", straight_left["o1"]), ] if not half and straight_right is not None: ports_config.append(("o2", straight_right["o2"])) for port_name, port_ref in filter(None, ports_config): c.add_port( name=port_name, width=port_ref.width, center=port_ref.center, orientation=port_ref.orientation, layer=LAYER.M1_DRAW, ) # Center at (0,0) c.move((-width / 2, -height / 2)) return c
[docs] @gf.cell(tags=("capacitors",)) def plate_capacitor( length: float = 26.0, width: float = 5.0, gap: float = 7.0, etch_layer: LayerSpec | None = "M1_ETCH", etch_bbox_margin: float = 2.0, cross_section: CrossSectionSpec = "cpw", ) -> Component: """Creates a plate capacitor. A capacitive coupler consists of two metal pads separated by a small gap, providing capacitive coupling between circuit elements like qubits and resonators. .. svgbob:: ______ ______ _________| | | |________ | | | | | o1 pad1 | ====gap==== | pad2 o2 | | | | | |_________ | | _________| |______| |______| Args: length: Length (vertical extent) of the capacitor pad in μm. width: Width (horizontal extent) of the capacitor pad in μm. gap: Gap between plates in μm. etch_layer: Optional layer for etching around the capacitor. etch_bbox_margin: Margin around the capacitor for the etch layer in μm. cross_section: Cross-section for the short straight from the etch box capacitor. Returns: A gdsfactory component with the plate capacitor geometry and two ports ('o1' and 'o2') on opposing sides. Raises: ValueError: If width or length is not positive. """ if width <= 0: raise ValueError(f"width must be positive, got {width}") if length <= 0: raise ValueError(f"length must be positive, got {length}") c = Component() single_capacitor = plate_capacitor_single( length=length, width=width, etch_layer=etch_layer, etch_bbox_margin=etch_bbox_margin, cross_section=cross_section, ) pad1 = c.add_ref(single_capacitor) pad2 = c.add_ref(single_capacitor) pad2.rotate(180) pad2.move((width + gap, 0)) c.center = (0.0, 0.0) # Add ports c.add_port(name="o1", port=pad1.ports["o1"]) c.add_port(name="o2", port=pad2.ports["o1"]) # Ensure etch box between pads if etch_layer is not None: missing_width = gap - 2 * etch_bbox_margin if missing_width > 0: etch_bbox = [ (-missing_width / 2, -length / 2 - etch_bbox_margin), (missing_width / 2, -length / 2 - etch_bbox_margin), (missing_width / 2, length / 2 + etch_bbox_margin), (-missing_width / 2, length / 2 + etch_bbox_margin), ] c.add_polygon(etch_bbox, layer=etch_layer) return c
[docs] @gf.cell(tags=("capacitors", "couplers")) def plate_capacitor_single( length: float = 26.0, width: float = 5.0, layer_metal: LayerSpec = LAYER.M1_DRAW, etch_layer: LayerSpec | None = "M1_ETCH", etch_bbox_margin: float = 2.0, cross_section: CrossSectionSpec = "cpw", ) -> Component: """Creates a single plate capacitor for coupling. This is essentially half of a :func:`~plate_capacitor`. .. svgbob:: ______ _________| | | | | o1 pad1 | | | |_________ | |______| Args: length: Length (vertical extent) of the capacitor pad in μm. width: Width (horizontal extent) of the capacitor pad in μm. layer_metal: Layer for the metal pad. etch_layer: Optional layer for etching around the capacitor. etch_bbox_margin: Margin around the capacitor for the etch layer in μm. cross_section: Cross-section for the short straight from the etch box capacitor. Returns: A gdsfactory component with the plate capacitor geometry. Raises: ValueError: If width or length is not positive. """ if width <= 0: raise ValueError(f"width must be positive, got {width}") if length <= 0: raise ValueError(f"length must be positive, got {length}") c = Component() points = [ (0.0, 0.0), (0, length), (width, length), (width, 0), ] c.add_polygon(points, layer=layer_metal) # Add etch layer bbox if specified if etch_layer is not None: etch_bbox = [ (-etch_bbox_margin, -etch_bbox_margin), (width + etch_bbox_margin, -etch_bbox_margin), (width + etch_bbox_margin, length + etch_bbox_margin), (-etch_bbox_margin, length + etch_bbox_margin), ] c.add_polygon(etch_bbox, layer=etch_layer) # Add small straight on the left side of the capacitor straight_cross_section = gf.get_cross_section(cross_section) straight_out_of_etch = straight( length=etch_bbox_margin, cross_section=straight_cross_section ) straight_left = c.add_ref(straight_out_of_etch).move(( -etch_bbox_margin, length / 2, )) # Merge WG marker layer with draw metal and create etch negative c = _merge_layers_with_etch( component=c, draw_layer=layer_metal, wg_layer=straight_cross_section.layer, etch_layer=etch_layer, ) c.add_port( name="o1", width=straight_left["o1"].width, center=straight_left["o1"].center, orientation=straight_left["o1"].orientation, layer=LAYER.M1_DRAW, ) # Center at (0,0) c.move((-width / 2, -length / 2)) return c