"""Resonator components."""
from __future__ import annotations
from functools import partial
import gdsfactory as gf
import numpy as np
from gdsfactory.component import Component
from gdsfactory.typings import ComponentSpec, CrossSectionSpec
from qpdk.cells._schematic import (
attach_schematic,
quarter_wave_resonator_coupled_schematic,
resonator_coupled_schematic,
resonator_half_wave_schematic,
resonator_quarter_wave_schematic,
resonator_schematic,
)
from qpdk.cells.resonator_sections import resonator_meander
from qpdk.cells.waveguides import (
bend_circular,
coupler_straight,
open_2_port,
short,
straight,
)
from qpdk.tech import get_etch_section
def _resonator_straight(
length: float,
meanders: int,
bend: Component,
cross_section: CrossSectionSpec,
*,
start_with_bend: bool,
end_with_bend: bool,
) -> tuple[int, Component | None, float]:
"""Return the snapped straight section and realized resonator length."""
straight_count = meanders + 1 - int(start_with_bend) - int(end_with_bend)
if straight_count < 0:
raise ValueError(
"Cannot have fewer than 0 straight sections. Reduce meanders or adjust bend start/end settings."
)
bend_length = meanders * bend.info["length"]
if straight_count == 0:
return straight_count, None, bend_length
straight_length = (length - bend_length) / straight_count
if straight_length <= 0:
raise ValueError(
f"Resonator length {length} is too short for {meanders} meanders with current bend spec {bend}. "
"Increase length, reduce meanders, or change the bend spec."
)
straight_comp = straight(length=straight_length, cross_section=cross_section)
actual_length = bend_length + straight_count * straight_comp.info["length"]
return straight_count, straight_comp, actual_length
[docs]
@gf.cell(tags=["resonators"], schematic_function=resonator_schematic)
def resonator(
length: float = 4000.0,
meanders: int = 6,
bend_spec: ComponentSpec = bend_circular,
cross_section: CrossSectionSpec = "cpw",
*,
start_with_bend: bool = False,
end_with_bend: bool = False,
open_start: bool = True,
open_end: bool = False,
) -> Component:
"""Creates a meandering coplanar waveguide resonator.
Changing `open_start` and `open_end` appropriately allows creating
a shorted quarter-wave resonator or an open half-wave resonator.
.. svgbob::
o1 ─────┐
│
┌───────┘
│
└───────┐
│
┌───────┘
│
└────── o2
See :cite:`m.pozarMicrowaveEngineering2012` for details
Args:
length: Length of the resonator in μm.
meanders: Number of meander sections to fit the resonator in a compact area.
bend_spec: Specification for the bend component used in meanders.
cross_section: Cross-section specification for the resonator.
start_with_bend: If True, starts the resonator with a bend.
end_with_bend: If True, ends the resonator with a bend.
open_start: If True, adds an etch section at the start of the resonator.
open_end: If True, adds an etch section at the end of the resonator.
Returns:
Component: A gdsfactory component with meandering resonator geometry.
Raises:
ValueError: If length is too short for the requested meanders.
"""
c = Component()
cross_section = gf.get_cross_section(cross_section)
bend = gf.get_component(
bend_spec, cross_section=cross_section, angle=180, angular_step=4
)
_, straight_comp, actual_length = _resonator_straight(
length,
meanders,
bend,
cross_section,
start_with_bend=start_with_bend,
end_with_bend=end_with_bend,
)
# Route meandering quarter-wave resonator
previous_port = None
first_ref = None
last_ref = None
for i in range(meanders):
# Determine if we should add a straight before this bend
if i == 0 and start_with_bend:
# First element is a bend
bend_ref = c.add_ref(bend)
if i % 2 == 0:
bend_ref.mirror()
bend_ref.rotate(90)
first_ref = bend_ref
previous_port = bend_ref.ports["o2"]
else:
if straight_comp is None:
raise ValueError("straight_comp is required but not initialized.")
straight_ref = c.add_ref(straight_comp)
if i == 0:
first_ref = straight_ref
else:
if previous_port is None:
raise ValueError("previous_port is required but not initialized.")
straight_ref.connect("o1", previous_port)
bend_ref = c.add_ref(bend)
if i % 2 == 0:
bend_ref.mirror()
bend_ref.rotate(90)
bend_ref.connect("o1", straight_ref.ports["o2"])
previous_port = bend_ref.ports["o2"]
last_ref = bend_ref
# Final section
if not end_with_bend:
if straight_comp is None:
raise ValueError("straight_comp is required but not initialized.")
final_straight_ref = c.add_ref(straight_comp)
if previous_port:
final_straight_ref.connect("o1", previous_port)
last_ref = final_straight_ref
if first_ref is None:
first_ref = final_straight_ref
if first_ref is None or last_ref is None:
raise ValueError("Resonator could not be generated correctly.")
# Etch at the open end
if open_end or open_start:
cross_section_etch_section = get_etch_section(cross_section)
open_etch_comp = gf.c.rectangle(
size=(
cross_section_etch_section.width,
2 * cross_section_etch_section.width + cross_section.width,
),
layer=cross_section_etch_section.layer,
centered=True,
port_type="optical",
port_orientations=(0, 180),
)
def _add_etch_at_port(port_name, ref_port, output_port):
"""Helper function to add etch at a specific port."""
open_etch = c.add_ref(open_etch_comp)
open_etch.connect(
port_name,
ref_port,
allow_width_mismatch=True,
allow_layer_mismatch=True,
)
# Keep open-end ports optical so recursive circuit netlists retain them.
c.add_port(output_port, port=open_etch.ports[output_port])
if open_end:
_add_etch_at_port("o1", last_ref.ports["o2"], "o2")
if open_start:
_add_etch_at_port("o2", first_ref.ports["o1"], "o1")
if not open_end:
c.add_port("o2", port=last_ref.ports["o2"])
if not open_start:
c.add_port("o1", port=first_ref.ports["o1"])
# Add metadata
c.info["length"] = actual_length
c.info["resonator_type"] = (
"half_wave" if open_start and open_end else "quarter_wave"
)
c.info["cross_section_name"] = cross_section.name
# c.info["frequency_estimate"] = (
# 3e8 / (4 * length * 1e-6) / 1e9
# ) # GHz, rough estimate
return c
attach_schematic(resonator, resonator_schematic)
# A quarter-wave resonator is shorted at one end and has maximum electric field
# at the open end, making it suitable for capacitive coupling.
resonator_quarter_wave = partial(resonator, open_start=False, open_end=True)
# A half-wave resonator is open at both ends
resonator_half_wave = partial(resonator, open_start=True, open_end=True)
# Quarter-wave resonator starting with a bend
resonator_quarter_wave_bend_start = partial(
resonator_quarter_wave, start_with_bend=True
)
# Half-wave resonator starting with a bend
resonator_half_wave_bend_start = partial(resonator_half_wave, start_with_bend=True)
# Resonator ending with a bend
resonator_quarter_wave_bend_end = partial(resonator_quarter_wave, end_with_bend=True)
resonator_half_wave_bend_end = partial(resonator_half_wave, end_with_bend=True)
# Both
resonator_quarter_wave_bend_both = partial(
resonator_quarter_wave, start_with_bend=True, end_with_bend=True
)
resonator_half_wave_bend_both = partial(
resonator_half_wave, start_with_bend=True, end_with_bend=True
)
for _resonator_factory in (
resonator_quarter_wave,
resonator_quarter_wave_bend_start,
resonator_quarter_wave_bend_end,
resonator_quarter_wave_bend_both,
):
attach_schematic(_resonator_factory, resonator_quarter_wave_schematic)
for _resonator_factory in (
resonator_half_wave,
resonator_half_wave_bend_start,
resonator_half_wave_bend_end,
resonator_half_wave_bend_both,
):
attach_schematic(_resonator_factory, resonator_half_wave_schematic)
[docs]
@gf.cell(
tags=["resonators", "couplers"],
schematic_function=resonator_coupled_schematic,
)
def resonator_coupled(
length: float = 4000.0,
meanders: int = 6,
bend_spec: ComponentSpec = bend_circular,
cross_section: CrossSectionSpec = "cpw",
*,
start_with_bend: bool = False,
end_with_bend: bool = False,
open_start: bool = True,
open_end: bool = False,
cross_section_non_resonator: CrossSectionSpec = "cpw",
coupling_straight_length: float = 200.0,
coupling_gap: float = 20.0,
) -> Component:
"""Creates a meandering coplanar waveguide resonator with a coupling waveguide.
This component combines a resonator with a parallel coupling waveguide placed
at a specified gap for proximity coupling. Similar to the design described in
:cite:`besedinQualityFactorTransmission2018a`.
.. svgbob::
coupling_o1 ─────────────── coupling_o2
coupling_gap
resonator_o1 ───────┐
│
┌───────────────────┘
│
└───────────────────┐
│
┌───────────────────┘
│
└──── resonator_o2
Args:
length: Length of the resonator in μm.
meanders: Number of meander sections to fit the resonator in a compact area.
bend_spec: Specification for the bend component used in meanders.
cross_section: Cross-section specification for the resonator.
start_with_bend: If True, starts the resonator with a bend.
end_with_bend: If True, ends the resonator with a bend.
open_start: If True, adds an etch section at the start of the resonator.
open_end: If True, adds an etch section at the end of the resonator.
cross_section_non_resonator: Cross-section specification for the coupling waveguide.
coupling_straight_length: Length of the coupling waveguide section in μm.
coupling_gap: Gap between the resonator and coupling waveguide in μm.
Measured from edges of the center conductors.
Returns:
Component: A gdsfactory component with meandering resonator and coupling waveguide.
"""
c = Component()
resonator_ref = c.add_ref(
resonator(
length=length,
meanders=meanders,
bend_spec=bend_spec,
cross_section=cross_section,
start_with_bend=start_with_bend,
end_with_bend=end_with_bend,
open_start=open_start,
open_end=open_end,
)
)
cross_section_obj = gf.get_cross_section(cross_section_non_resonator)
coupling_wg = straight(
length=coupling_straight_length,
cross_section=cross_section_obj,
)
coupling_ref = c.add_ref(coupling_wg)
# Position coupling waveguide parallel to resonator with specified gap
coupling_ref.movey(coupling_gap + cross_section_obj.width)
coupling_ref.xmin = resonator_ref["o1"].x # Align left edges
for port in resonator_ref.ports:
c.add_port(f"resonator_{port.name}", port=port)
for port in coupling_ref.ports:
c.add_port(f"coupling_{port.name}", port=port)
c.info += resonator_ref.cell.info
c.info["coupling_length"] = coupling_straight_length
c.info["coupling_gap"] = coupling_gap
return c
attach_schematic(resonator_coupled, resonator_coupled_schematic)
[docs]
@gf.cell(
tags=["resonators", "couplers"],
schematic_function=quarter_wave_resonator_coupled_schematic,
)
def quarter_wave_resonator_coupled(
length: float = 4000.0,
meanders: int = 6,
bend_spec: ComponentSpec = bend_circular,
cross_section: CrossSectionSpec = "cpw",
*,
start_with_bend: bool = False,
end_with_bend: bool = False,
cross_section_non_resonator: CrossSectionSpec = "cpw",
coupling_straight_length: float = 200.0,
coupling_gap: float = 20.0,
) -> Component:
"""Creates a quarter-wave resonator with a coupling waveguide.
Composes a straight coupler, the meander, and explicit open and short
terminations. The meander starts at the coupler output so that the trace
leaves the coupling gap at ``coupling_straight_length``. Layouts with no
usable first straight section retain the unsplit coupled-resonator cell.
The terminations are fixed: the coupled end is open and the far end is
shorted. Use :func:`~qpdk.cells.resonator.resonator_coupled` if other
terminations are needed.
.. svgbob::
coupling_o1 ─────────────── coupling_o2
coupling_gap
resonator_o1 ───────┐
│
┌───────────────────┘
│
└───────────────────┐
│
┌───────────────────┘
│
└──── (shorted, no port)
Args:
length: Length of the resonator in μm.
meanders: Number of meander sections to fit the resonator in a compact area.
bend_spec: Specification for the bend component used in meanders.
cross_section: Cross-section specification for the resonator.
start_with_bend: If True, starts the resonator with a bend.
end_with_bend: If True, ends the resonator with a bend.
cross_section_non_resonator: Cross-section specification for the coupling waveguide.
coupling_straight_length: Length of the coupling waveguide section in μm.
coupling_gap: Gap between the resonator and coupling waveguide in μm.
Returns:
The coupled quarter-wave resonator component.
Raises:
ValueError: If the requested length is too short for its meanders.
"""
c = Component()
cross_section_obj = gf.get_cross_section(cross_section)
bend = gf.get_component(
bend_spec,
cross_section=cross_section_obj,
angle=180,
angular_step=4,
)
first_straight_length = 0.0
actual_length = length
if not start_with_bend and meanders > 0:
_, first_straight, actual_length = _resonator_straight(
length,
meanders,
bend,
cross_section_obj,
start_with_bend=False,
end_with_bend=end_with_bend,
)
if first_straight is None:
raise ValueError("A coupling section requires a first straight section.")
first_straight_length = gf.snap.snap_to_grid(first_straight.info["length"])
if (
start_with_bend
or meanders == 0
or coupling_straight_length >= first_straight_length
):
res_ref = c << resonator_coupled(
length=length,
meanders=meanders,
bend_spec=bend_spec,
cross_section=cross_section,
start_with_bend=start_with_bend,
end_with_bend=end_with_bend,
open_start=True,
open_end=False,
cross_section_non_resonator=cross_section_non_resonator,
coupling_straight_length=coupling_straight_length,
coupling_gap=coupling_gap,
)
res_ref.move(tuple(-np.array(res_ref.ports["coupling_o1"].center)))
short_ref = c << short()
short_ref.connect("o1", res_ref.ports["resonator_o2"])
for port in res_ref.ports:
if port.name != "resonator_o2":
c.add_port(port=port)
c.info += res_ref.cell.info
return c
coupling_ref = c << coupler_straight(
length=coupling_straight_length,
gap=coupling_gap,
cross_section=cross_section,
cross_section_non_resonator=cross_section_non_resonator,
resonator_offset=get_etch_section(cross_section_obj).width,
)
coupling_ref.move(tuple(-np.array(coupling_ref.ports["o2"].center)))
open_ref = c << open_2_port(cross_section=cross_section)
open_ref.connect("o1", coupling_ref.ports["o1"])
# A straight lead-out here would run parallel to the probeline and make
# the coupled length longer than ``coupling_straight_length``.
meander_ref = c << resonator_meander(
length=actual_length - coupling_straight_length,
meanders=meanders,
bend_spec=bend_spec,
cross_section=cross_section,
end_with_bend=end_with_bend,
)
meander_ref.connect("o1", coupling_ref.ports["o4"])
short_ref = c << short()
short_ref.connect("o1", meander_ref.ports["o2"])
c.add_port("coupling_o1", port=coupling_ref.ports["o2"])
c.add_port("coupling_o2", port=coupling_ref.ports["o3"])
c.add_port("resonator_o1", port=open_ref.ports["o2"])
c.info["length"] = coupling_straight_length + meander_ref.cell.info["length"]
c.info["resonator_type"] = "quarter_wave"
c.info["cross_section_name"] = gf.get_cross_section(cross_section).name
c.info["coupling_length"] = coupling_straight_length
c.info["coupling_gap"] = coupling_gap
return c
attach_schematic(
quarter_wave_resonator_coupled, quarter_wave_resonator_coupled_schematic
)