Skip to content

Commit

Permalink
Improved DFitReflectanceResonance and added accompanying documentation.
Browse files Browse the repository at this point in the history
  • Loading branch information
PP501 committed Jul 11, 2024
1 parent 94800f3 commit ecd7b5e
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 12 deletions.
87 changes: 83 additions & 4 deletions docs/User/Data_Fitting_RF.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
The following RF resonator fitting functions exist:

- [RF notch resonance](#rf-notch-resonance) - used for **transmission spectra** of resonators side-coupled to a transmission line.
- RF reflectance
- [RF reflectance](#rf-reflectance) - used for **reflection spectra** of resonators terminating a transmission line.


## RF notch resonance
Expand All @@ -25,9 +25,6 @@ Note that:
The syntax is as follows:

```python
from sqdtoolz.Utilities.DataFitting import*
import numpy as np

from sqdtoolz.Utilities.DataFitting import DFitNotchResonance

#Assuming that the data is given as freqs, i_vals, q_vals
Expand Down Expand Up @@ -80,3 +77,85 @@ Now computing the fitted function (on the detrended data - so taking $I_0=Q_0=\a
Note that the final fit is done on the detrended data (thus, setting $\alpha=\tau=0$). The fit may still readjust and find finite values for $\alpha$ and $\tau$. Simply add these fitted values to the previous values of $\alpha$ and $\tau$ found it the detrending step to get the final fitting parameters.

In addition, it is noteworthy that least-squares residual used in circle fits can be easily skewed by the resonance datasets as many points tend to bunch up in a single region. To alleviate this bias, the data is first interpolated in equal arc-lengths. Then to combat the possibility of data going back and forth (due to noise), points that are close by are culled. The resulting dataset is used in the circle fitting procedures.


## RF reflectance

This applies to a **reflection spectra for resonators terminating transmission line**. The corresponding equation can be shown to be (c.f. [appendix D in thesis](https://unsworks.unsw.edu.au/entities/publication/6d9ea387-bc52-49d2-b5c2-6979779f4d30)):

$$
S_{11}(f)=-A\cdot\frac{1-\tfrac{\text{Q}_\text{ext}}{\text{Q}_\text{int}}\left(1+j\text{Q}_\text{int}\left(\tfrac{f}{f_0}-\tfrac{f_0}{f}\right)\right)}
{1+\tfrac{\text{Q}_\text{ext}}{\text{Q}_\text{int}}\left(1+j\text{Q}_\text{int}\left(\tfrac{f}{f_0}-\tfrac{f_0}{f}\right)\right)}\cdot \exp\left({j\frac{4\pi L}{c}f+j\phi}\right)
$$

Note that:
- The loaded quality factor is defined in terms of the internal and external quality factors as $Q_L^{-1}=Q_{\textnormal{int}}^{-1}+Q_{\textnormal{ext}}^{-1}$
- The complex exponential factor represents the phase trend that occurs due to the finite cable lengths ($L$ given the speed of light $c$)


### General usage

The syntax is as follows:

```python
from sqdtoolz.Utilities.DataFitting import DFitReflectanceResonance

#Assuming that the data is given as freqs, i_vals, q_vals
dFit = DFitReflectanceResonance()
dpkt = dFit.get_fitted_plot(freqs, i_vals, q_vals)
```

The returned data packet `dpkt` contains the fitted parameters mapped as:

- `'fres'`: $f_0$ (resonant frequency)
- `'Qint'`: $Q_\textnormal{int}$
- `'Qext'`: $Q_\textnormal{ext}$
- `'Qeff'`: $Q_L$
- `'Length'`: $L$
- `'Amplitude'`: $A$
- `'Phase'`: $\phi$

The `get_fitted_plot` function has optional arguments:

- `phase_slope_smooth_num` - The number of taps in the filter used to smooth out the phase derivative when fitting the maximum phase derivative (at the resonant frequency). Defaults to `25`. Increasing it will smoothen the phase derivative prior to fitting.
- `prop_detrend_start` - The proportion (defaults to `0.05`) of data from the beginning (lower end of the frequencies) to use in fitting the detrending line.
- `prop_detrend_end` - The proportion (defaults to `0.05`) of data in the end (higher end of the frequencies) to use in fitting the detrending line.
- `dont_plot` - Defaults to `False`. If `True`, nothing is plotted (just the fitted parameters are returned)
- `dont_plot_estimates` - Defaults to `False`. If `True`, the row of estimated plots is omitted and only the final amplitude, phase and IQ curves are drawn.

### Methodology

The fitting function requires accurate estimates of the final parameters. This section highlights the methods used to compute good initial guesses.

The first step is to remove any phase trends due to the finite length of the probing cables. The idea is to fit a line to the phase trend off-resonant from the main peak. The gradient and y-intercept of the linear fit readily gives $L$ and $\phi$. Now by multiplying $\exp\left({-j\frac{4\pi L}{c}f-j\phi}\right)$ to negate the phase slope, the detrended phase slope at resonance is given by:

$$
p_0=\left.\frac{d\phi}{df}\right|_{f=f_0}=\frac{\text{Q}_\text{ext}\text{Q}_\text{int}^2}{2\pi f_0(\text{Q}_\text{ext}^2-\text{Q}_\text{int}^2)}.
$$

Now looking at the amplitude, the resulting trough will have a height (that is, the vertical size when viewing the amplitude plot - found by fitting a Lorentzian to the amplitude response $|S_{11}|$):

$$
h=\frac{2A\text{Q}_\text{int}}{\text{Q}_\text{int}+\text{Q}_\text{ext}}.
$$

Now algebraically solve the equations to get:

$$
\begin{align}
\text{Q}_\text{ext}&=\left\lbrace
\begin{array}{lc}
-\tfrac{2\pi(h-1)f_0}{h^2}\cdot\tfrac{d\phi}{df} & \tfrac{d\phi}{df}>0 \\
\tfrac{2\pi(h-1)f_0}{(h-2)^2}\cdot\tfrac{d\phi}{df} & \tfrac{d\phi}{df}<0
\end{array}
\right.\\
\text{Q}_\text{int}&=\left\lbrace
\begin{array}{lc}
\tfrac{2\pi(h-1)f_0}{h(h-2)}\cdot\tfrac{d\phi}{df} & \tfrac{d\phi}{df}>0 \\
-\tfrac{2\pi(h-1)f_0}{h(h-2)}\cdot\tfrac{d\phi}{df} & \tfrac{d\phi}{df}<0
\end{array}
\right..
\end{align}
$$

The resonance phase slope be found by smoothing a finite difference taken over $\arg(S_{11})$.
20 changes: 12 additions & 8 deletions sqdtoolz/Utilities/DataFitting.py
Original file line number Diff line number Diff line change
Expand Up @@ -641,9 +641,10 @@ def get_min_max(val1, val2):
cut_ind2 = int(freq_vals.size-freq_vals.size*prop_detrend_end)
coefs2 = np.polyfit(freq_vals[cut_ind2:], phase_vals[cut_ind2:], 1)
coefs = 0.5*(coefs1+coefs2)
# coefs[1] = coefs1[1]
poly1d_fn = np.poly1d(coefs*1.0)
coefs[1] = phase_vals[0] - coefs[0]*freq_vals[0]
poly1d_fnData = np.poly1d(coefs)
#Plot the line...
poly1d_fn = np.poly1d(coefs)
if not dont_plot and not dont_plot_estimates:
axPhsDetrend.plot(freq_vals, phase_vals, 'k', alpha=0.5)
poly1d_fn0 = np.poly1d(coefs1)
Expand All @@ -658,7 +659,7 @@ def get_min_max(val1, val2):
axPhsDetrend.axvspan(freq_vals[cut_ind2-1], freq_vals[-1], alpha=0.2)

#Phase slope estimation
detrended_phase = phase_vals - poly1d_fn(freq_vals)
detrended_phase = phase_vals - poly1d_fnData(freq_vals)
def smooth(y, box_pts):
box = np.ones(box_pts)/box_pts
y_smooth = np.convolve(y, box, mode='valid')
Expand All @@ -675,10 +676,12 @@ def smooth(y, box_pts):
f0 = dpkt['centre']
#Plot the Detrended+Slope Estimate...
if not dont_plot:
axPhs.plot(freq_vals, phase_vals - poly1d_fn(freq_vals), 'k', alpha=0.5)
phsData = phase_vals - poly1d_fnData(freq_vals)
axPhs.plot(freq_vals, phsData, 'k', alpha=0.5)
if not dont_plot_estimates:
tempYlims = axPhs.get_ylim()
axPhs.plot(freq_vals, ps*(freq_vals-f0), 'r', alpha=0.5)
f0ind = np.argmin(np.abs(freq_vals-f0))
axPhs.plot(freq_vals, ps*(freq_vals-f0)+phsData[f0ind], 'r', alpha=0.75)
axPhs.set_ylim(tempYlims)
axPhs.set_xlabel('Frequency (Hz)')
axPhs.set_ylabel('Detrended Phase (rad)')
Expand Down Expand Up @@ -707,7 +710,7 @@ def smooth(y, box_pts):
else:
Qext = (h-1)*w0/(h-2)**2*p
p0 = coefs[0]/(np.pi*2)
phi0 = coefs[1]
phi0 = phase_vals[0] - coefs[0]*freq_vals[0]
l = coefs[0]*3e8/(4*np.pi)

#Perform actual fitting...
Expand Down Expand Up @@ -740,8 +743,9 @@ def cost_func(params, iq_vals):
axAmp.set_xlabel('Frequency (Hz)')
axAmp.set_ylabel('Amplitude')

axPhs.plot(freq_vals,np.angle(func(freq_vals, init_conds[:4]+[0,0])))
axPhs.plot(freq_vals,np.angle(func(freq_vals, sol.x[:4].tolist()+[0,0])))
axPhs.plot(freq_vals,np.unwrap(np.angle(func(freq_vals, init_conds[:4]+[0,0]))), alpha=0.75)
axPhs.plot(freq_vals,np.unwrap(np.angle(func(freq_vals, sol.x[:4].tolist()+[0,0]))), alpha=0.75)
axPhs.legend(['Data', 'Slope', 'Guess', 'Fitted'])

axIQ.plot(i_vals, q_vals, 'k', alpha=0.5)
axIQ.plot(np.real(func(freq_vals, init_conds)), np.imag(func(freq_vals, init_conds)))
Expand Down

0 comments on commit ecd7b5e

Please sign in to comment.