Multiline TRL

This example demonstrates how to use skrf’s NIST-style Multiline calibration (NISTMultilineTRL). First a simple application is presented, followed by a full simulation to demonstrate the improvements in calibration accuracy vs the number of lines. All data is used in the demonstration is generated by skrf, and the code for this is given at the end of the example.

Simple Multiline TRL

Setup

In [1]:
%matplotlib inline
import skrf
import numpy as np
import matplotlib.pyplot as plt
skrf.stylely()

Load data into skrf

In [2]:
#load all measurement data into a dictionary
data = skrf.read_all_networks('multiline_trl_data/')

# pull  out measurements by name into an ordered list
measured_names = ['thru','reflect','linep3mm','line2p3mm','line10mm']
measured = [data[k]  for k in measured_names]

#  switch terms
gamma_f,gamma_r = data['gamma_f'],data['gamma_r']

# DUT
dut_meas= data['DUT']

Simple Multiline TRL

In [3]:
# define the line lengths in meters (including thru)
l = [0, 0.3e-3, 2.3e-3, 10e-3]

#Do the calibration
cal = skrf.NISTMultilineTRL(
    measured = measured,  #Measured standards
    Grefls = [-1], #Reflection coefficient of the reflect, -1 for short
    l = l,         #Lengths of the lines
    er_est = 7,    #Estimate of transmission line effective permittivity
    switch_terms = (gamma_f, gamma_r) #Switch terms
    )

#Correct the DUT using the above calibration
corrected = cal.apply_cal(dut_meas)

corrected.plot_s_db()
../../_images/examples_metrology_Multiline_TRL_8_0.png

Compare calibration’s with different combinations of lines

Here we loop through different line combinations to demonstrate the difference in calibration accuracy.

In [4]:
# Run NIST Multiline TRL calibation with different combinations of lines

#Put through and reflect to their own list ...
mtr = measured[:2]

#and lines on their own
mlines = measured[2:]

# define the line lengths in meters
line_len = [0.3e-3, 2.3e-3, 10e-3]

cals = []
duts = []

line_combinations = [ [0], [1], [0,1,2]]

for used_lines in line_combinations:

    m = mtr + [mlines[i] for i in used_lines]

    #Add thru length to list of line lengths
    l = [0] + [line_len[i] for i in used_lines]

    #Do the calibration
    cal = skrf.NISTMultilineTRL(
        measured = m,  #Measured standards
        Grefls = [-1], #Reflection coefficient of the reflect, -1 for short
        l = l,         #Lengths of the lines
        er_est = 7,    #Estimate of transmission line effective permittivity
        switch_terms = (gamma_f, gamma_r) #Switch terms
        )

    #Correct the DUT using the above calibration
    corrected = cal.apply_cal(dut_meas)
    corrected.name = 'DUT, lines {}'.format(used_lines)

    duts.append(corrected)
    cals.append(cal)

Results and discussion

Transmission of corrected DUT

Plot the corrected DUT with different amount of lines

In [5]:
plt.figure()
plt.title('DUT S21')
for dut in duts:
    dut.plot_s_db(m=1, n=0)
../../_images/examples_metrology_Multiline_TRL_14_0.png

S11 of corrected DUT with different amount of lines

S11 shows bigger changes.

  • With one line low frequencies are very noisy
  • With only the medium length line calibration is very inaccurate at frequencies where phase of the line is multiple of 180 degrees
  • With three lines calibration accuracy is excellent everywhere
In [6]:
plt.figure()
plt.title('DUT S11')
for dut in duts:
    dut.plot_s_db(m=0, n=0)
../../_images/examples_metrology_Multiline_TRL_16_0.png

Normalized standard deviation of different calibrations

This measures the accuracy of the calibration. Lower number means less noise. * TRL calibration with one 90 degrees long line has normalized standard deviation of 1. * With multiple lines normalized standard deviations less than one is possible.

In [7]:
f_ghz = dut.frequency.f_scaled

plt.figure()
plt.title('Calibration normalized standard deviation')
for e, cal in enumerate(cals):
    plt.plot(f_ghz, cal.nstd, label='Lines: {}'.format(line_combinations[e]))
plt.ylim([0,30])
plt.legend(loc='upper right')
dut.frequency.labelXAxis()
../../_images/examples_metrology_Multiline_TRL_18_0.png

Calculate effective permittivity of the transmission lines used in the calibration

Is there no existing way to get the real er_eff? Anyway, this is how it can be calculated from the propagation constant. CPW line propagation constant can be approximated as average of substrate and air permittivities, but this is not completely true at low frequencies when the line is lossy

In [8]:
#define calibration standard media
freq = dut.frequency
cpw = skrf.media.CPW(freq, z0=50, w=40e-6, s=25e-6, ep_r=12.9,
                     t=5e-6, rho=2e-8)

#Get the cal with all the lines
cal = cals[-1]

#Plot the solved effective permittivity of the transmission lines
c = 299792458.0
real_er_eff = -(cpw.gamma/(2*np.pi*f_ghz*1e9/c))**2

plt.figure()
plt.title('CPW effective permittivity')
plt.plot(f_ghz, cal.er_eff.real, label='Solved er_eff')
plt.plot(f_ghz, real_er_eff.real, label='Actual er_eff')
plt.legend(loc='upper right')
Out[8]:
<matplotlib.legend.Legend at 0x7fbfa18b30b8>
../../_images/examples_metrology_Multiline_TRL_20_1.png

Depending on the noise there might be some wrong choices for some lines during the propagation constant determination that cause small spikes in the solved effective permittivity. In general they don’t matter much, but incorrectly determined propagation constant affects the weighting of the lines and accuracy of the calibration at that frequency. With many lines though even if one line has incorrectly determined propagation constant the effect to the total calibration will be small.

Plot the phase of the solved reflection coefficient

Is reflect correct?

Since we know the ideals in this simulation we can re-define them here, and compare the determined reflect to the actual reflect. (see below for simulation details)

In [9]:
lines = [cpw.line(l, 'm') for l in line_len]
short = cpw.delay_short(10e-6, 'm')

actuals = [
    cpw.thru(),
    skrf.two_port_reflect(short, short),
    ]

actuals.extend(lines)
In [10]:
plt.figure()
plt.title('Solved and actual reflection coefficient of the reflect standard')
cal.apply_cal(measured[1]).plot_s_deg(n=0, m=0)
actuals[1].plot_s_deg(n=0, m=0)

plt.show(block=True)
../../_images/examples_metrology_Multiline_TRL_25_0.png

Simulation to Generate Data

Here is how we made the data used above.

Create frequency and Media

In [11]:
freq = skrf.F(1,100,401)

#CPW media used for DUT and the calibration standards
cpw = skrf.media.CPW(freq, z0=50, w=40e-6, s=25e-6, ep_r=12.9,
                     t=5e-6, rho=2e-8)

#1.0 mm coaxial media for calibration error boxes
coax1mm = skrf.media.Coaxial(freq, z0=50, Dint=0.44e-3, Dout=1.0e-3, sigma=1e8)

f_ghz = cpw.frequency.f*1e-9

Make realistic looking error networks.

Propagation constant determination is iterative and doesn’t work as well when the error networks are randomly generated

In [12]:
X = coax1mm.line(1, 'm', z0=58, name='X', embed=True)
Y = coax1mm.line(1.1, 'm', z0=40, name='Y', embed=True)

plt.figure()
plt.title('Error networks')
X.plot_s_db()
Y.plot_s_db()

#Realistic looking switch terms
gamma_f = coax1mm.delay_load(0.2, 21e-3, 'm', z0=60, embed=True)
gamma_r = coax1mm.delay_load(0.25, 16e-3, 'm', z0=56, embed=True)

plt.figure()
plt.title('Switch terms')
gamma_f.plot_s_db()
gamma_r.plot_s_db()
../../_images/examples_metrology_Multiline_TRL_31_0.png
../../_images/examples_metrology_Multiline_TRL_31_1.png

Generate Ficticous measurements

In [13]:
#Lengths of the lines used in the calibration, units are in meters

line_len = [0.3e-3, 2.3e-3, 10e-3]
lines = [cpw.line(l, 'm') for l in line_len]

#Attenuator with mismatched feed lines
dut_feed = cpw.line(1.5e-3, 'm', z0=55, embed=True)
dut = dut_feed**cpw.attenuator(-10)**dut_feed

#Through and non-ideal short
#Real reflection coefficient is solved during the calibration

short = cpw.delay_short(10e-6, 'm')

actuals = [
    cpw.thru(),
    skrf.two_port_reflect(short, short),
    ]

actuals.extend(lines)

#Measured
measured = [X**k**Y for k in actuals]

#Switch termination
measured = [skrf.terminate(m, gamma_f, gamma_r) for m in measured]

#Add little noise to the measurements
for m in measured:
    m.add_noise_polar(0.0005, 0.005)

names = ['thru','reflect','linep3mm','line2p3mm','line10mm']
for k,name in enumerate(names):
    measured[k].name=name


#Noiseless DUT so that all the noise will be from the calibration
dut_meas = skrf.terminate(X**dut**Y, gamma_f, gamma_r)
dut_meas.name = 'DUT'

#Put through and reflect to their own list ...
mtr = measured[:2]

#and lines on their own
mlines = measured[2:]

# write data to disk
write_data = False
if write_data:
    [k.write_touchstone(dir='multiline_trl_data/') for k in measured]
    gamma_f.write_touchstone('multiline_trl_data/gamma_f.s1p')
    gamma_r.write_touchstone('multiline_trl_data/gamma_r.s1p')
    dut_meas.write_touchstone(dir='multiline_trl_data/')
In [14]: