# Multiline TRL¶

Multiline TRL is a two-port VNA calibration utilizing at least two transmission lines with different physical lengths and at least one reflective standard that is identical on both ports. The electrical parameters of the lines don’t need to be known, but the transmission lines should have identical construction (same propagation constant and characteristic impedance). The reflect standard reflection coefficient doesn’t need to be known exactly, phase needs to be known with 90 degree accuracy.

If the measured phase differences of the lines is a multiple of 180 degrees the calibration is singular. The calibration accuracy is worse the closer the line measurement phases are to the singularities, the best accuracy is obtained in the two lines case when the phase difference is 90 degrees. Multiple lines can be used to extend the frequency range where the calibration is accurate.

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¶

[1]:

%matplotlib inline
import skrf
from skrf.media import CPW, Coaxial
import numpy as np
import matplotlib.pyplot as plt
skrf.stylely()


[2]:

# Load all measurement data into a dictionary

# Pull out measurements by name into an ordered list
measured_names = ['thru','reflect','linep3mm','line2p3mm']
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']

# 50 ohm termination
res_50ohm_meas = data['res_50ohm']


### Simple Multiline TRL¶

[3]:

# define the line lengths in meters (including thru)
l = [0, 0.3e-3, 2.3e-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()


## Compare calibrations with different combinations of lines¶

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

[4]:

# Run NIST Multiline TRL calibration with different combinations of lines

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

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

line_len = l[1:]

cals = []
duts = []

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

for used_lines in line_combinations:

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

# Add thru length to list of line lengths
l = [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)


### Transmission of the corrected DUT¶

Plot the corrected DUT calibrated with different combination of calibration lines.

[5]:

plt.figure()
plt.title('DUT S21')
for dut in duts:
dut.plot_s_db(m=1, n=0)


### S11 of corrected DUT with different amount of calibration lines¶

S11 shows bigger changes.

• With one short line low frequencies are very noisy

• With only the long line the calibration is very inaccurate at frequencies where the phase difference of the thru and line is close to a multiple of 180 degrees

• With both lines calibration accuracy is good everywhere

[6]:

plt.figure()
plt.title('DUT S11')
for dut in duts:
dut.plot_s_db(m=0, n=0)


### Normalized standard deviation of different calibrations¶

Normalized standard deviation can be used to measure the accuracy of the calibration. Lower number means calibration is less sensitive to the measurement noise.

• TRL calibration with one 90 degrees long line has normalized standard deviation of 1.

• TRL calibration with one 180 degree long lossless line is singular and has infinite normalized standard deviation.

• With multiple lines normalized standard deviation less than one is possible.

Note that the nstd is normalized such that it doesn’t consider the actual measurement noise. It’s calculated only from the solved propagation constant and line lengths. The threshold of how large it can be depends on the DUT being measured, measurement noise and the required accuracy of the measurement. If there are large spikes, such as are visible in the long line case below, that’s a sign that the calibration is very close to singular at that frequency and the measurement accuracy is going to be poor.

[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,20])
plt.legend(loc='upper right')
dut.frequency.labelXAxis()


## Calculate effective complex relative permittivity of transmission lines used in the calibration¶

Effective complex relative permittivity $$\epsilon_{r,eff}$$ of a transmission line is related to the propagation constant $$\gamma$$ as:

$$\gamma = \frac{2\pi f}{c}\sqrt{\epsilon_{r,eff}}$$, where $$c$$ equals the speed of light and $$f$$ is frequency.

In general it’s a complex value with the imaginary part indicating losses.

CPW line effective permittivity can be approximated as average of substrate and air permittivities.

[8]:

# Define calibration standard media
freq = dut.frequency
cpw = CPW(freq, z0=55, w=40e-6, s=25e-6, ep_r=12.9,
t=5e-6, rho=2e-8)

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

# Calculate CPW complex permittivity from the propagation constant
c = 299792458
er_eff = -(c*cpw.gamma/(2*np.pi*freq.f))**2

plt.figure()
plt.title('CPW effective permittivity (real part)')
plt.plot(f_ghz, cal.er_eff.real, label='Solved er_eff')
plt.plot(f_ghz, er_eff.real, label='Actual er_eff')
plt.xlabel('Frequency (GHz)')
plt.legend(loc='lower right')

/home/docs/checkouts/readthedocs.org/user_builds/scikit-rf/envs/latest/lib/python3.8/site-packages/skrf/media/cpw.py:158: RuntimeWarning: Conductor loss calculation invalid for lineheight t (5e-06)  < 3 * skin depth (2.250790789780127e-06)
warnings.warn(

[8]:

<matplotlib.legend.Legend at 0x7fa4d888df10>


TRL calibration accuracy is the best when line length difference is 90 degrees. Solved propagation constant and effective permittivity however are more accurate the bigger the line length difference is. At low frequencies the estimate is noisier due to the line phase difference being small.

## Plot the phase of the solved reflection coefficient¶

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)

[9]:

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, label='Solved')
cpw.delay_short(10e-6, 'm').plot_s_deg(n=0, m=0, label='Actual')


## Reference plane shift¶

Because propagation constant of the media is solved during the calibration it’s possible to shift the reference plane by a specified distance.

The reference plane shift can be specified with ref_plane argument. The shift should be specified in meters, negative lengths is towards the VNA. By default the same shift is applied to both ports. Unequal shift on the two ports is supported by passing a two element list.

[10]:

cal_shift = 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
# Shift reference planes twords VNA by this amount (in m) on both ports
ref_plane = -50e-6
)

# Correct the DUT using the above calibration
corrected_thru = cal.apply_cal(measured[0])
corrected_thru_shifted = cal_shift.apply_cal(measured[0])

corrected_thru.plot_s_deg(m=1, n=0, label='Thru phase')
corrected_thru_shifted.plot_s_deg(m=1, n=0, label='Reference plane shifted thru phase')


## Calibration reference impedance renormalization¶

The reference impedance of the calibration is by default the transmission line characteristic impedance. If we know the actual characteristic impedance of the lines we can give it to the calibration routine with the z0_line argument to renormalize the measured S-parameters to a fixed reference z0_ref.

If the conductance per unit length (G) is much lower than the capacitive reactance per unit length ($$j\omega C_0$$), the characteristic impedance of the transmission line can be written in terms of the propagation constant $$\gamma$$ and capacitance per unit length $$C_0$$:

$$Z_0 = \gamma/(j 2 \pi f C_0)$$

If $$C_0$$ is known it can be given to the calibration routine with c0 parameter to renormalize the calibration reference impedance to z0_ref (defaults to 50 ohms) assuming G = 0. If the line is lossy the characteristic impedance is complex valued and giving a single c0 instead of a fixed z0_line is usually more accurate.

In this case we know that the line characteristic impedance is actually 55 ohms. To renormalize the calibration from 55 ohms to 50 ohms we need to give z0_line=55 argument to the calibration routine.

[11]:

cal_ref = 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
z0_line = 55, # Line actual characteristic impedance
z0_ref = 50 # Calibration reference impedance
)

cal.apply_cal(res_50ohm_meas).s11.plot_s_db(label='50 $\Omega$ termination |$S_{11}$|, Z_ref = line')
cal_ref.apply_cal(res_50ohm_meas).s11.plot_s_db(label='50 $\Omega$ termination |$S_{11}$|, Z_ref = 50 $\Omega$')


After renormalization the 50 ohm termination measurement shows good matching. It’s not perfectly matched due to the noise in the measurements.

## Simulation to generate the input data¶

Here is how we made the data used above.

### Create frequency and Media¶

[12]:

freq = skrf.F(1,100,201)

# CPW media used for DUT and the calibration standards
cpw = CPW(freq, z0=55, 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 = 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

[13]:

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()


### Generate Fictitious measurements¶

[14]:

# Lengths of the lines used in the calibration, units are in meters
line_len = [0.3e-3, 2.3e-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=60, embed=True)
dut = dut_feed**cpw.attenuator(-10)**dut_feed

res_50ohm = cpw.resistor(50) ** cpw.short(nports=2) ** cpw.resistor(50)

# 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:

names = ['thru', 'reflect', 'linep3mm', 'line2p3mm']
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'

res_50ohm_meas = skrf.terminate(X**res_50ohm**Y, gamma_f, gamma_r)
res_50ohm_meas.name = 'res_50ohm'

# 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/')
res_50ohm_meas.write_touchstone(dir='multiline_trl_data/')

[ ]: