Source code for ooragan.resonator_attribution
import numpy as np
from numpy.typing import ArrayLike
from dataclasses import dataclass
from typing import Optional
from scipy.special import ellipk
from scipy.constants import epsilon_0, mu_0
from tabulate import tabulate
[docs]
@dataclass
class Resonator:
"""
Container of a CPW resonator's parameter.
Parameters
----------
length : float
Resonator total lenght in microns.
conductor_width : float
Width of the central conductor of the CPW in microns.
gap_to_ground : float
Gap between the central conductor and the ground plane in microns.
thickness : float
Thickness of the metal in nanometers.
coupling_gap : float
Coupling distance measured between the gap of the feedline and the gap
of the resonator. Distance in microns.
substrate_dielectric_const : float, optional
Dielectric constant of the substrate. Defaults to 11.68 (silicon).
Qc_estimated : float, optional
Estimation of the coupling quality factor of the resonator.
type : str, optional
Type of resonator, between "lambda2" for half-wave or "lambda4" for quarter wave.
Defaults to "lambda4".
Attributes
----------
conductor_width : float
Width of the central conductor of the CPW in microns.
coupling_gap : float
Coupling distance measured between the gap of the feedline and the gap
of the resonator. Distance in microns.
freq_geo : float
Geometrical frequency of the resonator in Hz.
gap_to_ground : float
Gap between the central conductor and the ground plane in microns.
length : float
Resonator total lenght in microns.
Qc_estimated : float
Estimation of the coupling quality factor of the resonator.
substrate_dielectric_const : float
Dielectric constant of the substrate.
thickness : float
Thickness of the metal in nanometers.
type : str
Type of resonator, between "lambda2" for half-wave or "lambda4" for quarter wave.
"""
length: float
conductor_width: float
gap_to_ground: float
thickness: float
coupling_gap: float
london_eff: float
substrate_dielectric_const: float = 11.68
Qc_estimated: Optional[float] = None
type: str = "lambda4"
def __post_init__(self) -> None:
s = self.gap_to_ground * 1e-6
w = self.conductor_width * 1e-6
t = self.thickness * 1e-9
lamb = self.london_eff * 1e-9
lres = self.length * 1e-6
k_zero = w / (w + 2 * s)
k1_zero = np.sqrt(1 - k_zero ** 2)
k = self._u1(t, w, s) / self._u2(t, w, s)
k_half = self._u1(t / 2, w, s) / self._u2(t / 2, w, s)
k1 = np.sqrt(1 - k ** 2)
k1_half = np.sqrt(1 - k_half ** 2)
g = (-k * np.log(t / (4 * (w + 2 * s))) - np.log(t / (4 * w)) + (2 * (w + s) / (w + 2 * s)) * np.log(
s / (w + s))) / (2 * k ** 2 * ellipk(k ** 2) ** 2)
self.c_geo = 2 * epsilon_0 * ellipk(k ** 2) / ellipk(
k1 ** 2
) + 2 * self.substrate_dielectric_const * epsilon_0 * ellipk(
k_zero ** 2
) / ellipk(
k1_zero ** 2
)
self.l_geo = mu_0 * ellipk(k1_half ** 2) / (4 * ellipk(k_half ** 2))
self.l_kin = (mu_0 * g * lamb ** 2) / (w * t)
if self.type == "lambda4":
res_type = 4
elif self.type == "lambda2":
res_type = 2
else:
raise ValueError("Only types 'lambda2' and 'lambda4' are accepted")
self.freq_geo = (1 / (np.sqrt(self.l_geo * self.c_geo) * res_type * lres)) / 1e9
self.freq_kin = (1 / (np.sqrt((self.l_geo + self.l_kin) * self.c_geo) * res_type * lres)) / 1e9
def _u1(self, t: float, w: float, s: float) -> float:
"""
Definition of u-parameter for Gao's CPW inductance and capacitance calculation derivation.
Parameters
----------
t : float
Thickness of metal film in nanometers.
w : float
Width of resonator central conductor in microns.
s : float
Separation between resonator central conductor and ground plane in microns.
"""
tm = t * 1e-3
d = 2 * tm / np.pi
a = w
b = w + 2 * s
return (
a
+ (d / 2)
+ (3 * np.log(2) * d / 2)
- (d * np.log(d / a) / 2)
+ (d * np.log((b - a) / (b + a)) / 2)
)
def _u2(self, t: float, w: float, s: float) -> float:
"""
Definition of u-parameter for Gao's CPW inductance and capacitance calculation derivation.
Parameters
----------
t : float
Thickness of metal film in nanometers.
w : float
Width of resonator central conductor in microns.
s : float
Separation between resonator central conductor and ground plane in microns.
"""
tm = t * 1e-3
d = 2 * tm / np.pi
a = w
b = w + 2 * s
return (
b
- (d / 2)
- (3 * np.log(2) * d / 2)
+ (d * np.log(d / a) / 2)
+ (d * np.log((b - a) / (b + a)) / 2)
)
[docs]
class ResonatorAttribution:
"""
Resonator attribution takes the fit results and tries to attribute the resonance peaks
to physical resonators present on the feedline.
Parameters
----------
fit_results : ArrayLike
Results from the fit of the resonators given by the ResonatorFitter class.
dc_kinetic_induct : float
DC measured sheet kinetic inductance in nH/square.
resonators : Resonator
The physical resonators present on the feedline.
Attributes
----------
_fit_results : ArrayLike
Results from the fit of the resonators given by the ResonatorFitter class.
_dc_Lkin : float
DC measured sheet kinetic inductance in nH/square.
_res_on_line : Resonator
The physical resonators present on the feedline.
"""
def __init__(
self,
fit_results: ArrayLike,
dc_kinetic_induct: float,
resonators: list[Resonator],
):
"""
Resonator attribution takes the fit results and tries to attribute the resonance peaks
to physical resonators present on the feedline.
Parameters
----------
fit_results : ArrayLike
Results from the fit of the resonators given by the ResonatorFitter class.
dc_kinetic_induct : float
DC measured sheet kinetic inductance in nH/square.
resonators : Resonator
The physical resonators present on the feedline.
"""
self.result = None
self._fit_results = fit_results
self._res_on_line = resonators
self._dc_Lkin = dc_kinetic_induct * 1e-9
[docs]
def minimize_lambda_eff(self):
raise NotImplementedError
[docs]
def minimize_Lk(self, printer=True):
"""
Tries to attribute each resonance measured with a resonator object provided to the class by calculating a
kinetic inductance from the resonance shift with the geometrical resonance. Compares this kinetic inductance
with the one provided to the class (ideally from DC measurements).
Parameters
----------
printer : bool, optional
Choose whether to display the results dictionary (with fashion!) or not.
"""
# Initializing the results dictionary
result = {}
# Kinetic inductance calculation and minimization
for fitnb, fitdict in enumerate(self._fit_results):
lkarr = np.zeros(len(self._res_on_line))
fmes = fitdict["f_r"][0]
for resnb, res in enumerate(self._res_on_line):
lkarr[resnb] = ((1 / (16 * res.c_geo * (res.length * 1e-6) ** 2 * fmes ** 2))
- res.l_geo)
# Minimization
id_closest_res = (np.abs(lkarr - self._dc_Lkin)).argmin()
# Resonator allocation and variable storage
vals = [self._res_on_line[id_closest_res].length, self._res_on_line[id_closest_res].freq_geo,
self._res_on_line[id_closest_res].freq_kin, fmes, lkarr[id_closest_res], self._dc_Lkin * 1e9]
result[f"res {fitnb}"] = np.array(vals)
if printer:
keys = sorted(result.keys())
heads = ["Resonator length (μm)",
"Geometrical resonance frequency (GHz)",
"Resonance frequency with kinetic inductance (GHz)",
"Measured resonance frequency (GHz)",
"Measured kinetic inductance (pH/sq)",
"Kinetic inductance from DC measurements (pH/sq)"]
to_print = []
for row in range(len(result[keys[0]])):
to_print.append([heads[row]] + [result[k][row] for k in keys])
print(tabulate(to_print, headers=keys, tablefmt="rounded_grid"))
return result
[docs]
def minimize_Qc(self):
raise NotImplementedError