Coverage for qpdk / cells / resonator.py: 97%
122 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-02 17:50 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-02 17:50 +0000
1"""Resonator components."""
3from __future__ import annotations
5from functools import partial
7import gdsfactory as gf
8import numpy as np
9from gdsfactory.component import Component
10from gdsfactory.typings import ComponentSpec, CrossSectionSpec
12from qpdk.cells.waveguides import bend_circular, straight
13from qpdk.helper import show_components
14from qpdk.tech import get_etch_section
17@gf.cell(tags=("resonators",))
18def resonator(
19 length: float = 4000.0,
20 meanders: int = 6,
21 bend_spec: ComponentSpec = bend_circular,
22 cross_section: CrossSectionSpec = "cpw",
23 *,
24 start_with_bend: bool = False,
25 end_with_bend: bool = False,
26 open_start: bool = True,
27 open_end: bool = False,
28) -> Component:
29 """Creates a meandering coplanar waveguide resonator.
31 Changing `open_start` and `open_end` appropriately allows creating
32 a shorted quarter-wave resonator or an open half-wave resonator.
34 .. svgbob::
36 o1 ─────┐
37 │
38 ┌───────┘
39 │
40 └───────┐
41 │
42 ┌───────┘
43 │
44 └────── o2
46 See :cite:`m.pozarMicrowaveEngineering2012` for details
48 Args:
49 length: Length of the resonator in μm.
50 meanders: Number of meander sections to fit the resonator in a compact area.
51 bend_spec: Specification for the bend component used in meanders.
52 cross_section: Cross-section specification for the resonator.
53 start_with_bend: If True, starts the resonator with a bend.
54 end_with_bend: If True, ends the resonator with a bend.
55 open_start: If True, adds an etch section at the start of the resonator.
56 open_end: If True, adds an etch section at the end of the resonator.
58 Returns:
59 Component: A gdsfactory component with meandering resonator geometry.
61 Raises:
62 ValueError: If length is too short for the requested meanders.
63 """
64 c = Component()
65 cross_section = gf.get_cross_section(cross_section)
66 bend = gf.get_component(
67 bend_spec, cross_section=cross_section, angle=180, angular_step=4
68 )
70 num_straights = meanders + 1
71 if start_with_bend:
72 num_straights -= 1
73 if end_with_bend:
74 num_straights -= 1
76 if num_straights < 0:
77 raise ValueError(
78 "Cannot have fewer than 0 straight sections. Reduce meanders or adjust bend start/end settings."
79 )
81 straight_comp = None
82 if num_straights > 0:
83 length_per_one_straight = (
84 length - meanders * bend.info["length"]
85 ) / num_straights
87 if length_per_one_straight <= 0:
88 raise ValueError(
89 f"Resonator length {length} is too short for {meanders} meanders with current bend spec {bend}. "
90 f"Increase length, reduce meanders, or change the bend spec."
91 )
93 straight_comp = straight(
94 length=length_per_one_straight,
95 cross_section=cross_section,
96 )
98 # Route meandering quarter-wave resonator
99 previous_port = None
100 first_ref = None
101 last_ref = None
103 for i in range(meanders):
104 # Determine if we should add a straight before this bend
105 if i == 0 and start_with_bend:
106 # First element is a bend
107 bend_ref = c.add_ref(bend)
108 if i % 2 == 0:
109 bend_ref.mirror()
110 bend_ref.rotate(90)
111 first_ref = bend_ref
112 previous_port = bend_ref.ports["o2"]
113 else:
114 if straight_comp is None:
115 raise ValueError("straight_comp is required but not initialized.")
116 straight_ref = c.add_ref(straight_comp)
117 if i == 0:
118 first_ref = straight_ref
119 else:
120 straight_ref.connect("o1", previous_port)
122 bend_ref = c.add_ref(bend)
123 if i % 2 == 0:
124 bend_ref.mirror()
125 bend_ref.rotate(90)
127 bend_ref.connect("o1", straight_ref.ports["o2"])
128 previous_port = bend_ref.ports["o2"]
130 last_ref = bend_ref
132 # Final section
133 if not end_with_bend:
134 if straight_comp is None:
135 raise ValueError("straight_comp is required but not initialized.")
136 final_straight_ref = c.add_ref(straight_comp)
137 if previous_port:
138 final_straight_ref.connect("o1", previous_port)
139 last_ref = final_straight_ref
140 if first_ref is None:
141 first_ref = final_straight_ref
143 if first_ref is None or last_ref is None:
144 raise ValueError("Resonator could not be generated correctly.")
146 actual_length = meanders * bend.info["length"]
147 if num_straights > 0:
148 if straight_comp is None:
149 raise ValueError("straight_comp is required but not initialized.")
150 actual_length += num_straights * straight_comp.info["length"]
152 # Etch at the open end
153 if open_end or open_start:
154 cross_section_etch_section = get_etch_section(cross_section)
156 open_etch_comp = gf.c.rectangle(
157 size=(
158 cross_section_etch_section.width,
159 2 * cross_section_etch_section.width + cross_section.width,
160 ),
161 layer=cross_section_etch_section.layer,
162 centered=True,
163 port_type="optical",
164 port_orientations=(0, 180),
165 )
167 def _add_etch_at_port(port_name, ref_port, output_port):
168 """Helper function to add etch at a specific port."""
169 open_etch = c.add_ref(open_etch_comp)
170 open_etch.connect(
171 port_name,
172 ref_port,
173 allow_width_mismatch=True,
174 allow_layer_mismatch=True,
175 )
176 c.add_port(
177 output_port, port=open_etch.ports[output_port], port_type="placement"
178 )
180 if open_end:
181 _add_etch_at_port("o1", last_ref.ports["o2"], "o2")
182 if open_start:
183 _add_etch_at_port("o2", first_ref.ports["o1"], "o1")
185 if not open_end:
186 c.add_port("o2", port=last_ref.ports["o2"])
188 if not open_start:
189 c.add_port("o1", port=first_ref.ports["o1"])
191 # Add metadata
192 c.info["length"] = actual_length
193 c.info["resonator_type"] = "quarter_wave"
194 c.info["cross_section"] = cross_section.name
195 # c.info["frequency_estimate"] = (
196 # 3e8 / (4 * length * 1e-6) / 1e9
197 # ) # GHz, rough estimate
199 return c
202# A quarter-wave resonator is shorted at one end and has maximum electric field
203# at the open end, making it suitable for capacitive coupling.
204resonator_quarter_wave = partial(resonator, open_start=False, open_end=True)
205# A half-wave resonator is open at both ends
206resonator_half_wave = partial(resonator, open_start=True, open_end=True)
208# Quarter-wave resonator starting with a bend
209resonator_quarter_wave_bend_start = partial(
210 resonator_quarter_wave, start_with_bend=True
211)
212# Half-wave resonator starting with a bend
213resonator_half_wave_bend_start = partial(resonator_half_wave, start_with_bend=True)
215# Resonator ending with a bend
216resonator_quarter_wave_bend_end = partial(resonator_quarter_wave, end_with_bend=True)
217resonator_half_wave_bend_end = partial(resonator_half_wave, end_with_bend=True)
219# Both
220resonator_quarter_wave_bend_both = partial(
221 resonator_quarter_wave, start_with_bend=True, end_with_bend=True
222)
223resonator_half_wave_bend_both = partial(
224 resonator_half_wave, start_with_bend=True, end_with_bend=True
225)
228@gf.cell(tags=("resonators", "couplers"))
229def resonator_coupled(
230 length: float = 4000.0,
231 meanders: int = 6,
232 bend_spec: ComponentSpec = bend_circular,
233 cross_section: CrossSectionSpec = "cpw",
234 *,
235 start_with_bend: bool = False,
236 end_with_bend: bool = False,
237 open_start: bool = True,
238 open_end: bool = False,
239 cross_section_non_resonator: CrossSectionSpec = "cpw",
240 coupling_straight_length: float = 200.0,
241 coupling_gap: float = 20.0,
242) -> Component:
243 """Creates a meandering coplanar waveguide resonator with a coupling waveguide.
245 This component combines a resonator with a parallel coupling waveguide placed
246 at a specified gap for proximity coupling. Similar to the design described in
247 :cite:`besedinQualityFactorTransmission2018a`.
249 .. svgbob::
251 coupling_o1 ─────────────── coupling_o2
252 coupling_gap
253 resonator_o1 ───────┐
254 │
255 ┌───────────────────┘
256 │
257 └───────────────────┐
258 │
259 ┌───────────────────┘
260 │
261 └──── resonator_o2
263 Args:
264 length: Length of the resonator in μm.
265 meanders: Number of meander sections to fit the resonator in a compact area.
266 bend_spec: Specification for the bend component used in meanders.
267 cross_section: Cross-section specification for the resonator.
268 start_with_bend: If True, starts the resonator with a bend.
269 end_with_bend: If True, ends the resonator with a bend.
270 open_start: If True, adds an etch section at the start of the resonator.
271 open_end: If True, adds an etch section at the end of the resonator.
272 cross_section_non_resonator: Cross-section specification for the coupling waveguide.
273 coupling_straight_length: Length of the coupling waveguide section in μm.
274 coupling_gap: Gap between the resonator and coupling waveguide in μm.
275 Measured from edges of the center conductors.
277 Returns:
278 Component: A gdsfactory component with meandering resonator and coupling waveguide.
279 """
280 c = Component()
282 resonator_ref = c.add_ref(
283 resonator(
284 length=length,
285 meanders=meanders,
286 bend_spec=bend_spec,
287 cross_section=cross_section,
288 start_with_bend=start_with_bend,
289 end_with_bend=end_with_bend,
290 open_start=open_start,
291 open_end=open_end,
292 )
293 )
295 cross_section_obj = gf.get_cross_section(cross_section_non_resonator)
297 coupling_wg = straight(
298 length=coupling_straight_length,
299 cross_section=cross_section_obj,
300 )
301 coupling_ref = c.add_ref(coupling_wg)
303 # Position coupling waveguide parallel to resonator with specified gap
304 coupling_ref.movey(coupling_gap + cross_section_obj.width)
306 coupling_ref.xmin = resonator_ref["o1"].x # Align left edges
308 for port in resonator_ref.ports:
309 port_type = (
310 "placement"
311 if ((port.name == "o1" and open_start) or (port.name == "o2" and open_end))
312 else "optical"
313 )
314 c.add_port(f"resonator_{port.name}", port=port, port_type=port_type)
316 for port in coupling_ref.ports:
317 c.add_port(f"coupling_{port.name}", port=port)
319 c.info += resonator_ref.cell.info
320 c.info["coupling_length"] = coupling_straight_length
321 c.info["coupling_gap"] = coupling_gap
323 return c
326@gf.cell(tags=("resonators", "couplers"))
327def quarter_wave_resonator_coupled(
328 length: float = 4000.0,
329 meanders: int = 6,
330 bend_spec: ComponentSpec = bend_circular,
331 cross_section: CrossSectionSpec = "cpw",
332 *,
333 start_with_bend: bool = False,
334 end_with_bend: bool = False,
335 open_start: bool = True,
336 open_end: bool = False,
337 cross_section_non_resonator: CrossSectionSpec = "cpw",
338 coupling_straight_length: float = 200.0,
339 coupling_gap: float = 20.0,
340) -> Component:
341 """Creates a quarter-wave resonator with a coupling waveguide.
343 Uses :func:`~qpdk.cells.resonator.resonator_coupled` as the basis but
344 removes the shorted end port from the output ports.
346 .. svgbob::
348 coupling_o1 ─────────────── coupling_o2
349 coupling_gap
350 resonator_o1 ───────┐
351 │
352 ┌───────────────────┘
353 │
354 └───────────────────┐
355 │
356 ┌───────────────────┘
357 │
358 └──── (shorted, no port)
360 Args:
361 length: Length of the resonator in μm.
362 meanders: Number of meander sections to fit the resonator in a compact area.
363 bend_spec: Specification for the bend component used in meanders.
364 cross_section: Cross-section specification for the resonator.
365 start_with_bend: If True, starts the resonator with a bend.
366 end_with_bend: If True, ends the resonator with a bend.
367 open_start: If True, adds an etch section at the start of the resonator.
368 open_end: If True, adds an etch section at the end of the resonator.
369 cross_section_non_resonator: Cross-section specification for the coupling waveguide.
370 coupling_straight_length: Length of the coupling waveguide section in μm.
371 coupling_gap: Gap between the resonator and coupling waveguide in μm.
373 Returns:
374 The coupled quarter-wave resonator component.
375 """
376 c = Component()
378 res_ref = c << resonator_coupled(
379 length=length,
380 meanders=meanders,
381 bend_spec=bend_spec,
382 cross_section=cross_section,
383 start_with_bend=start_with_bend,
384 end_with_bend=end_with_bend,
385 open_start=open_start,
386 open_end=open_end,
387 cross_section_non_resonator=cross_section_non_resonator,
388 coupling_straight_length=coupling_straight_length,
389 coupling_gap=coupling_gap,
390 )
391 movement = np.array(res_ref.ports["coupling_o1"].center)
392 res_ref.move(tuple(-movement))
394 for port in res_ref.ports:
395 if port.name != "resonator_o2": # Skip the shorted end port
396 c.add_port(port=port)
398 return c
401if __name__ == "__main__":
402 show_components(
403 resonator,
404 resonator_quarter_wave,
405 resonator_half_wave,
406 resonator_quarter_wave_bend_start,
407 resonator_quarter_wave_bend_both,
408 resonator_coupled,
409 partial(
410 resonator_coupled,
411 length=2000,
412 meanders=4,
413 open_start=False,
414 open_end=True,
415 ),
416 )