Calibration With Three Receivers


It has long been known that full error correction is possible given a VNA with only three recievers and no internal coaxial switch. However, since no modern VNA employs such an architecture, the software required to make fully corrected measurements is not available on today’s modern VNA’s.

Recently, the application of Frequency Extender units containing only three receivers has become more common. Thus, there is a need for full error correction capability on systems with three receivers and no internal coaxial switch. This document describes how to use scikit-rf to fully correct two-port measurements made on such a system.


A circuit model for a switch-less three receiver system is shown below.

In [1]:
from IPython.display import *

To fully correct an arbitrary two-port, the device must be measured in two orientations, call these forward and reverse. Because there is no switch present, this requires the operator to physically flip the device, and save the pair of measurements. In on-wafer scenarios, one could fabricate two identical devices, one in each orientation. In either case, a pair of measurements are required for each DUT before correction can occur.

While in reality the device is being flipped, one can imaging that the device is static, and the entire VNA circuitry is flipped. This interpretation lends itself to implementation, as the existing 12-term correction can be re-used by simply copying the forward error coefficients into the corresponding reverse error coefficients. This is what scikit-rf does internally.

Worked Example

This example demonstrates how to create a TwoPortOnePath and EnhancedResponse calibration from measurements taken on a Agilent PNAX with a set of VDI WR-12 TXRX-RX Frequency Extender heads. Comparisons between the two algorithms are made by correcting an asymmetric DUT.

Read in the Data

The measurements of the calibration standards and DUT’s were downloaded from the VNA by saving touchstone files of the raw s-parameter data to disk.

In [2]:
ls three_receiver_cal/data/
attenuator (forward).s2p      simulation.s2p
attenuator (reverse).s2p      thru.s2p
load.s2p                      wr15 shim and swg (forward).s2p
quarter wave delay short.s2p  wr15 shim and swg (reverse).s2p

These files can be read by scikit-rf into Networks with the following.

In [3]:
import skrf as rf
%matplotlib inline
from pylab import *

raw = rf.read_all_networks('three_receiver_cal/data/')
# list the raw measurments
/home/docs/checkouts/ RuntimeWarning: numpy.dtype size changed, may indicate binary incompatibility. Expected 96, got 88
  return f(*args, **kwds)
dict_keys(['short', 'quarter wave delay short', 'attenuator (forward)', 'simulation', 'wr15 shim and swg (reverse)', 'thru', 'attenuator (reverse)', 'load', 'wr15 shim and swg (forward)'])

Each Network can be accessed through the dictionary raw.

In [4]:
thru = raw['thru']
2-Port Network: 'thru',  60.0-90.0 GHz, 721 pts, z0=[50.+0.j 50.+0.j]

If we look at the raw measurement of the flush thru, it can be seen that only \(S_{11}\) and \(S_{21}\) contain meaningful data. The other s-parameters are noise.

In [5]:

Create Calibration

In the code that follows a TwoPortOnePath calibration is created from corresponding measured and ideal responses of the calibration standards. The measured networks are read from disk, while their corresponding ideal responses are generated using scikit-rf. More information about using scikit-rf to do offline calibrations can be found here.

In [6]:
from skrf.calibration import TwoPortOnePath
from import RectangularWaveguide
from skrf import two_port_reflect as tpr
from skrf import  mil

# pull frequency information from measurements
frequency = raw['short'].frequency

# the media object
wg = RectangularWaveguide(frequency=frequency, a=120*mil, z0=50)

# list of 'ideal' responses of the calibration standards
ideals = [wg.short(nports=2),
          tpr(wg.delay_short( 90,'deg'), wg.match()),

# corresponding measurments to the 'ideals'
measured = [raw['short'],
            raw['quarter wave delay short'],

# the Calibration object
cal = TwoPortOnePath(measured = measured, ideals = ideals )
/home/docs/checkouts/ UserWarning: n_thrus is None, guessing which stds are transmissive
  warn('n_thrus is None, guessing which stds are transmissive')

Apply Correction

There are two types of correction possible with a 3-receiver system.

  1. Full (TwoPortOnePath)
  2. Partial (EnhancedResponse)

scikit-rf uses the same Calibration object for both, but employs different correction algorithms depending on the type of the DUT. The DUT used in this example is a WR-15 shim cascaded with a WR-12 1” straight waveguide, as shown in the picture below. Measurements of this DUT are corrected with both full and partial correction and the results are compared below.

In [7]:
Image('three_receiver_cal/pics/asymmetic DUT.jpg', width='75%')

Full Correction ( TwoPortOnePath)

Full correction is achieved by measuring each device in both orientations, forward and reverse. To be clear, this means that the DUT must be physically removed, flipped, and re-inserted. The resulting pair of measurements are then passed to the apply_cal() function as a tuple. This returns a single corrected response.

Partial Correction (Enhanced Response)

If you pass a single measurement to the apply_cal() function, then the calibration will employ partial correction. This type of correction is known as EnhancedResponse. Depending on the measurment application, this type of correction may be good enough, and perhaps the only choice.


Below are direct comparisons of the DUT shown above corrected with full and partial algorithms. It shows that the partial calibration produces a large ripple on the reflect measurements, and slightly larger ripple on the transmissive measurments.

In [8]:
from pylab import *

simulation = raw['simulation']

dutf = raw['wr15 shim and swg (forward)']
dutr = raw['wr15 shim and swg (reverse)']

corrected_full =     cal.apply_cal((dutf, dutr))
corrected_partial =  cal.apply_cal(dutf)

# plot results

f, ax = subplots(1,2, figsize=(8,4))

ax[0].set_title ('$S_{11}$')
ax[1].set_title ('$S_{21}$')

corrected_partial.plot_s_db(0,0, label='Partial Correction',ax=ax[0])
corrected_partial.plot_s_db(1,0, label='Partial Correction',ax=ax[1])

corrected_full.plot_s_db(0,0, label='Full Correction', ax = ax[0])
corrected_full.plot_s_db(1,0, label='Full Correction', ax = ax[1])

simulation.plot_s_db(0,0,label='Simulation', ax=ax[0], color='k')
simulation.plot_s_db(1,0,label='Simulation', ax=ax[1], color='k')

/home/docs/checkouts/ FutureWarning: `rcond` parameter will change to the default of machine precision times ``max(M, N)`` where M and N are the input matrix dimensions.
To use the future default and silence this warning we advise to pass `rcond=None`, to keep using the old, explicitly pass `rcond=-1`.
  abcTmp, residualsTmp = npy.linalg.lstsq(Q,m)[0:2]
/home/docs/checkouts/ UserWarning: only gave a single measurement orientation, error correction is partial without a tuple
  warnings.warn('only gave a single measurement orientation, error correction is partial without a tuple')
/home/docs/checkouts/ RuntimeWarning: divide by zero encountered in log10
  out = 20 * npy.log10(input)

What if my DUT is Symmetric??

If the DUT is known to be reciprocal ( \(S_{21}=S_{12}\) ) and symmetric ( \(S_{11}=S_{22}\) ), then its response should be the identical for both forward anad reverse orientations. In this case, measuring the device twice is unnecessary, and can be circumvented. This is explored in the example: TwoPortOnePath, EnhancedResponse, and FakeFlip