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

Load data into skrf

[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']
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()
../../_images/examples_metrology_Multiline_TRL_9_0.png

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)
../../_images/examples_metrology_Multiline_TRL_14_0.png

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)
../../_images/examples_metrology_Multiline_TRL_16_0.png

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()
../../_images/examples_metrology_Multiline_TRL_18_0.png

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.

[8]:
# Define calibration standard media
freq = dut.frequency

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

plt.figure()
plt.title('CPW effective permittivity (real part)')
plt.plot(f_ghz, cal.er_eff.real, label='Solved er_eff')
plt.xlabel('Frequency (GHz)')
plt.legend(loc='lower right')
[8]:
<matplotlib.legend.Legend at 0x7fbbe0d7b0a0>
../../_images/examples_metrology_Multiline_TRL_20_1.png

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

Applying the calibration to the measured reflect standard we can get the calibrated S-parameters of the unknown reflect.

[9]:
plt.figure()
plt.title('Solved reflection coefficient of the reflect standard')
cal.apply_cal(measured[1]).plot_s_deg(n=0, m=0, label='Solved short')
../../_images/examples_metrology_Multiline_TRL_24_0.png

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')
../../_images/examples_metrology_Multiline_TRL_26_0.png

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$')
../../_images/examples_metrology_Multiline_TRL_28_0.png

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
/home/docs/checkouts/readthedocs.org/user_builds/scikit-rf/envs/v0.23.0/lib/python3.8/site-packages/skrf/media/cpw.py:481: RuntimeWarning: Conductor loss calculation invalid for lineheight t (5e-06)  < 3 * skin depth (2.250790789780127e-06)
  warnings.warn(

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()
/tmp/ipykernel_1204/1418691314.py:1: FutureWarning: In a future version,`embed` will be deprecated.
The line and media port impedance z0 and characteristic impedance Z0 will be used instead to determine if the line has to be renormalized.
  X = coax1mm.line(1, 'm', z0=58, name='X', embed=True)
/tmp/ipykernel_1204/1418691314.py:2: FutureWarning: In a future version,`embed` will be deprecated.
The line and media port impedance z0 and characteristic impedance Z0 will be used instead to determine if the line has to be renormalized.
  Y = coax1mm.line(1.1, 'm', z0=40, name='Y', embed=True)
/home/docs/checkouts/readthedocs.org/user_builds/scikit-rf/envs/v0.23.0/lib/python3.8/site-packages/skrf/media/media.py:905: FutureWarning: In a future version,`embed` will be deprecated.
The line and media port impedance z0 and characteristic impedance Z0 will be used instead to determine if the line has to be renormalized.
  return self.line(d=d, unit=unit, **kwargs) ** self.load(Gamma0=Gamma0, **kwargs)
../../_images/examples_metrology_Multiline_TRL_35_1.png
../../_images/examples_metrology_Multiline_TRL_35_2.png

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:
    m.add_noise_polar(0.001, 0.1)

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/')
/tmp/ipykernel_1204/641534798.py:6: FutureWarning: In a future version,`embed` will be deprecated.
The line and media port impedance z0 and characteristic impedance Z0 will be used instead to determine if the line has to be renormalized.
  dut_feed = cpw.line(1.5e-3, 'm', z0=60, embed=True)
[ ]: