Tutorial 3: Making Resonator Networks¶
The first two tutorials modelled a single resonator, but real chip designs rarely stop there. Most involve several resonators coupled in a chain. For more complicated designs such as this, the process shown in Tutorial 1 can be repeated for multiple resonators in a network. This tutorial shows how to
build a network of many capacitors and multiple CPW resonators and look at its response,
draw that network with the schematics feature so you can see what you built, and
Fit a lumped oscillator model (LOM) to each resonator in the chain, reassemble the pieces, and check the lumped network against the CPW ground truth.
The device design we will use to test this is a chain of three capacitively-coupled CPW resonators with staggered lengths, so each rings at its own frequency:
[P1] -- Cc1 -- Ctog -- CPW_1 -- Ctog -- Cc2 -- Ctog -- CPW_2 -- Ctog -- Cc3 -- Ctog -- CPW_3 -- Ctog -- Cc4 -- [P2]
A natural “subgroup” of this larger design is one resonator with the two couplers and two ground caps that flank it: Cc -- Ctog -- CPW -- Ctog -- Cc. That subgroup is exactly the topology fit_lom already knows how to fit.
More complicated designs which include hanger resonators will be covered in Tutorials 8 and 9.
[2]:
import warnings
warnings.filterwarnings("ignore")
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import skrf as rf
from simpleLOMs import CPWParams, fit_lom
from simpleLOMs.networks.cpw import cpw_resonator_network_2port
from simpleLOMs.networks.chain import resonator_chain_network_2port
from simpleLOMs.schematics import resonator_chain_schematic_2port
from simpleLOMs.analysis import circle_fit_f0_kappa, resonances_from_s_max
from simpleLOMs import (
plot_transmission, plot_all_models, plot_re_im,
apply_style, axis_label,
COLOR_PRIMARY, COLOR_SECONDARY, COLOR_HIGHLIGHT,
FIGURE_WIDTH_1COL, FIGURE_WIDTH_2COL,
FIGURE_HEIGHT_1COL_GR, FIGURE_HEIGHT_2COL_GR,
)
apply_style()
cpw = CPWParams(ep_r=11.45) # ultracold-silicon substrate
freq = rf.Frequency(4e9, 12e9, 8_001, unit="Hz")
Z0 = 50.0
# Three staggered CPW resonators
lengths = [8.0e-3, 7.0e-3, 6.0e-3] # metres
# Four coupling capacitors
Ccs = [8e-15, 6e-15, 6e-15, 8e-15]
Ctog = 1e-14
Ctogs = [Ctog] * (2 * len(lengths))
segments_cpw = [{"kind": "cpw", "d": d} for d in lengths]
print(f"{len(lengths)} CPW resonators, lengths (mm): {[d*1e3 for d in lengths]}")
3 CPW resonators, lengths (mm): [8.0, 7.0, 6.0]
1. Build the CPW network and look at it¶
Run resonator_chain_network_2port to wire the ladder together. Each entry in segments is a resonator. Here every one is a CPW line ({"kind": "cpw", "d": ...}), but in section 3 we will swap some for lumped LC tanks. Ccs holds the coupling capacitors (one more than the number of resonators) and Ctogs holds the ground capacitors (two per resonator).
[3]:
chain_cpw = resonator_chain_network_2port(
freq, segments_cpw, Ccs, Ctogs, cpw_params=cpw, Z0=Z0,
)
We then look at the network’s transmission \(|S_{21}|\) and reflection \(|S_{11}|\).
[4]:
plot_transmission(
{"CPW chain": chain_cpw},
m=1, n=0,
)
[4]:
(<Figure size 2117.1x1308.44 with 1 Axes>,
<Axes: title={'center': '$|S_{21}|$'}, xlabel='Frequency, $f$ (GHz)', ylabel='$|S_{21}|$ (dB)'>)
The three modes can be read off from the S21 transmission peaks.
[5]:
mode_freqs = resonances_from_s_max(chain_cpw, m=1, n=0)
mode_freqs = np.sort(mode_freqs)
print("Chain modes (GHz):", np.round(mode_freqs / 1e9, 4))
Chain modes (GHz): [7.335 8.37 9.709]
We have now constructed a full two-port network with the three distributed elements, which produce three separate but coupled modes.
2. Draw the network with the schematics feature¶
schematics feature, using resonator_chain_schematic_2port, takes the same[6]:
schematic_cpw = resonator_chain_schematic_2port(
segments_cpw, Ccs, Ctogs,
cpw_params=cpw, freq=freq, Z0=Z0,
annotations={"resonators": len(lengths), "modes_GHz": list(np.round(mode_freqs / 1e9, 3))},
)
You can choose to save portable copies next to the notebook, and render them inline.
[7]:
from pathlib import Path
figdir = Path("figures")
figdir.mkdir(exist_ok=True)
schematic_cpw.save_svg(figdir / "chain_cpw.svg")
schematic_cpw.save_html(figdir / "chain_cpw.html")
schematic_cpw
[7]:
3. Fit a LOM to each resonator individually¶
In order to use fit_lom to fit LC models, we can break the larger network into Cc -- Ctog -- CPW -- Ctog -- Cc subgroups. Then , we fit each subgroup in isolation with the Optimized model.
The circuit parameters we will use are:
the two coupling capacitors of subgroup \(i\) which are the shared couplers \(C_{c,i}\) and \(C_{c,i+1}\),
the two ground capacitors, which are the ones flanking that resonator.
fit_lom builds its own isolated CPW reference for each subgroup and returns the effective \((L, C)\).
Note that fitting a piece in isolation ignores that its neighbours are other resonators rather than \(50\,\Omega\) ports. In future versions, this will be generalized so that more accurate \(Z_0\) values can be used.
[8]:
segments_lc = []
rows = []
for i, d in enumerate(lengths):
Cc_left, Cc_right = Ccs[i], Ccs[i + 1]
Ctog_left, Ctog_right = Ctogs[2 * i], Ctogs[2 * i + 1]
# Locate this subgroup's resonance on its own isolated CPW reference
sub_cpw = cpw_resonator_network_2port(
freq, d, Cc_left, Cc_right, Ctog_left, Ctog_right, cpw_params=cpw, Z0=Z0,
)
f0_sub, _ = circle_fit_f0_kappa(sub_cpw, 0, 0)
fit_freq = rf.Frequency(f0_sub - 0.4e9, f0_sub + 0.4e9, 20_001, unit="Hz")
m = fit_lom(
d, model="optimized",
Cc1=Cc_left, Cc2=Cc_right, Ctog1=Ctog_left, Ctog2=Ctog_right,
cpw_params=cpw, freq=fit_freq, return_model=True,
)
segments_lc.append({"kind": "lc", "L": m.L, "C": m.C})
rows.append({
"resonator": i + 1,
"length_mm": d * 1e3,
"f0_sub_GHz": f0_sub / 1e9,
"L_nH": m.L * 1e9,
"C_fF": m.C * 1e15,
})
print(f" resonator {i+1}: d={d*1e3:.1f} mm f0~{f0_sub/1e9:.3f} GHz "
f"L={m.L*1e9:.3f} nH C={m.C*1e15:.2f} fF")
fits = pd.DataFrame(rows)
fits
resonator 1: d=8.0 mm f0~7.336 GHz L=0.640 nH C=721.10 fF
resonator 2: d=7.0 mm f0~8.370 GHz L=0.560 nH C=633.29 fF
resonator 3: d=6.0 mm f0~9.707 GHz L=0.484 nH C=541.99 fF
[8]:
| resonator | length_mm | f0_sub_GHz | L_nH | C_fF | |
|---|---|---|---|---|---|
| 0 | 1 | 8.0 | 7.336277 | 0.640245 | 721.097308 |
| 1 | 2 | 7.0 | 8.369674 | 0.560362 | 633.292138 |
| 2 | 3 | 6.0 | 9.706938 | 0.483517 | 541.994094 |
3a. Build the LC chain¶
With the three \((L, C)\) pairs extracted, we can now rebuild the chain with the same coupling capacitors but each CPW line replaced by its fitted LC tank.
Note that because the Optimized model folds the ground capacitance into its effective \((L, C)\) (the topology rule from Tutorial 2), the lumped chain no longer carries
Ctogcaps.
[9]:
chain_lc = resonator_chain_network_2port(
freq, segments_lc, Ccs, Ctogs=None, cpw_params=cpw, Z0=Z0,
)
nets = {"CPW": chain_cpw, "Lumped (optimized)": chain_lc}
# The comparison the reduction is judged on: Re/Im of S11, lumped vs CPW.
plot_all_models(nets, m=0, n=0, quantity="re", title=r"$\mathrm{Re}\,S_{11}$")
plot_all_models(nets, m=0, n=0, quantity="im", title=r"$\mathrm{Im}\,S_{11}$")
plot_all_models(nets, m=1, n=0, quantity="db", title=r"$|S_{21}|$")
[9]:
(<Figure size 1012.5x625.759 with 1 Axes>,
<Axes: title={'center': '$|S_{21}|$'}, xlabel='Frequency, $f$ (GHz)', ylabel='$|S_{21}|$ (dB)'>)
3b. Per-mode frequency agreement¶
between the lumped chain and the CPW ground truth. Now that they are both built, we can find agreement (or lack thereof) between the lumped and CPW chains.
To do so, we locate each mode on the coarse grid, then circle-fit both chains on a dense window around it for sub-MHz resolution.
[10]:
mode_guess = np.sort(resonances_from_s_max(chain_cpw, m=1, n=0))
def mode_f0(segments, Ctogs, f_guess):
win = rf.Frequency(f_guess - 0.15e9, f_guess + 0.15e9, 40_001, unit="Hz")
net = resonator_chain_network_2port(win, segments, Ccs, Ctogs=Ctogs,
cpw_params=cpw, Z0=Z0)
f0, _ = circle_fit_f0_kappa(net, 0, 0)
return f0
rows_cmp = []
for j, fg in enumerate(mode_guess, start=1):
f_cpw = mode_f0(segments_cpw, Ctogs, fg)
f_lc = mode_f0(segments_lc, None, fg)
rows_cmp.append({
"mode": j,
"CPW_GHz": f_cpw / 1e9,
"Lumped_GHz": f_lc / 1e9,
"error_MHz": (f_lc - f_cpw) / 1e6,
"error_%": 100.0 * (f_lc - f_cpw) / f_cpw,
})
compare = pd.DataFrame(rows_cmp)
compare
[10]:
| mode | CPW_GHz | Lumped_GHz | error_MHz | error_% | |
|---|---|---|---|---|---|
| 0 | 1 | 7.335346 | 7.335350 | 0.003762 | 0.000051 |
| 1 | 2 | 8.369481 | 8.369993 | 0.511327 | 0.006109 |
| 2 | 3 | 9.705619 | 9.705852 | 0.233740 | 0.002408 |
For this network, fitting each resonator in isolation with the Optimized LOM and stitching the pieces back together reproduces the full CPW chain closely. The mode frequencies line up and the \(S_{11}\) lineshape overlays the ground truth.
The main approximation we made was that each subgroup was fit against \(50\,\Omega\) ports, not against its neighbouring resonators.
3c. Draw the final LC chain schematic¶
Finally, redraw the network in its lumped form — the same schematic builder, now fed LC segments, so the CPW lines are replaced by parallel-LC tanks showing the fitted \(L\) and \(C\).
[11]:
schematic_lc = resonator_chain_schematic_2port(
segments_lc, Ccs, Ctogs=None,
cpw_params=cpw, freq=freq, Z0=Z0,
annotations={"model": "optimized", "resonators": len(lengths)},
)
schematic_lc.save_svg(figdir / "chain_lc.svg")
schematic_lc.save_html(figdir / "chain_lc.html")
schematic_lc
[11]: