# Measuring a Multiport Device with a 2-Port Network Analyzer¶

## Introduction¶

This notebook demonstrates a numerical test of the technique described in “*A Rigorous Technique for Measuring the Scattering Matrix of a Multiport Device with a 2-Port Network Analyzer*” [1].

In microwave measurements, one commonly needs to measure a n-port deveice with a m-port network analyzer ($ m<n $ of course). Generally, this is done by terminating each non-measured port with a matched load, and assuming the reflected power is negligable. However, in some cases this may not provide the most accurate results, or even be possible in all measurement environments. The paper above presents an elegent solution to this problem, using impedance renormalization. We’ll call it *Tippet’s
technique*, because it has a good ring to it.

In *Tippets technique*, several sub-networks are measured in a similar way as before, but the port terminations are not assumed to be matched. Instead, the terminations just have to be known and no more than one can be completely reflective. So, in general \(|\Gamma| \ne 1\). During measurements, each port is terminated with a consistent termination. So port 1 is always terminated with \(Z_1\) when not being measured. Once measured, each sub-network is re-normalized to these port
impedances. Think about that. Finally the composit network is contructed, and may then be re-normalized to the desired system impedance, say $50 ohm $

- [1] J. C. Tippet and R. A. Speciale, “A Rigorous Technique for Measuring the Scattering Matrix of a Multiport Device with a 2-Port Network Analyzer,” IEEE Transactions on Microwave Theory and Techniques, vol. 30, no. 5, pp. 661–666, May 1982.

## Outline of Tippet’s Technique¶

Following the example given in [1], measuring a 4-port network with a 2-port network analyzer.

An outline of the technique:

- Calibrate 2-port network analyzer
- Get four known terminations (\(Z_1, Z_2, Z_3,Z_4\)). No more than one can have \(|\Gamma| = 1\)
- Measure all combinations of 2-port subnetworks (there are 6). Each port not currently being measured must be terminated with its corresponding load.
- Renormalize each subnetwork to the impedances of the loads used to terminate it when note being measured.
- Build composite 4-port, renormalize to VNA impedance.

## Implementation¶

```
[1]:
```

```
from itertools import combinations
import skrf as rf
%matplotlib inline
from pylab import *
rf.stylely()
```

```
/home/docs/checkouts/readthedocs.org/user_builds/scikit-rf/envs/latest/lib/python3.7/site-packages/skrf/plotting.py:1441: UserWarning: Style includes a parameter, 'interactive', that is not related to style. Ignoring
mpl.style.use(os.path.join(pwd, style_file))
```

First, we create a Media object, which is used to generate networks for testing. We will use WR-10 Rectangular waveguide.

```
[2]:
```

```
wg = rf.wr10
wg.frequency.npoints = 101
```

Next, lets generate a random 4-port network which will be the DUT, that we are trying to measure with out 2-port network analyzer.

```
[3]:
```

```
dut = wg.random(n_ports = 4,name= 'dut')
dut
```

```
[3]:
```

```
4-Port Network: 'dut', 75.0-110.0 GHz, 101 pts, z0=[50.+0.j 50.+0.j 50.+0.j 50.+0.j]
```

Now, we need to define the loads used to terminate each port when it is not being measured, note as described in [1] not more than one can be have full reflection, \(|\Gamma| = 1\)

```
[4]:
```

```
loads = [wg.load(.1+.1j),
wg.load(.2-.2j),
wg.load(.3+.3j),
wg.load(.5),
]
# construct the impedance array, of shape FXN
z_loads = array([k.z.flatten() for k in loads]).T
```

Create required measurement port combinations. There are 6 different measurements required to measure a 4-port with a 2-port VNA. In general, #measurements = \(n\choose 2\), for n-port DUT on a 2-port VNA.

```
[5]:
```

```
ports = arange(dut.nports)
port_combos = list(combinations(ports, 2))
port_combos
```

```
[5]:
```

```
[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]
```

Now to do it. Ok we loop over the port combo’s and connect the loads to the right places, simulating actual measurements. Each raw subnetwork measurement is saved, along with the renormalized subnetwork. Finally, we stuff the result into the 4-port composit network.

```
[6]:
```

```
composite = wg.match(nports = 4) # composite network, to be filled.
measured,measured_renorm = {},{} # measured subnetworks and renormalized sub-networks
# ports `a` and `b` are the ports we will connect the VNA too
for a,b in port_combos:
# port `c` and `d` are the ports which we will connect the loads too
c,d =ports[(ports!=a)& (ports!=b)]
# determine where `d` will be on four_port, after its reduced to a three_port
e = where(ports[ports!=c]==d)[0][0]
# connect loads
three_port = rf.connect(dut,c, loads[c],0)
two_port = rf.connect(three_port,e, loads[d],0)
# save raw and renormalized 2-port subnetworks
measured['%i%i'%(a,b)] = two_port.copy()
two_port.renormalize(c_[z_loads[:,a],z_loads[:,b]])
measured_renorm['%i%i'%(a,b)] = two_port.copy()
# stuff this 2-port into the composite 4-port
for i,m in enumerate([a,b]):
for j,n in enumerate([a,b]):
composite.s[:,m,n] = two_port.s[:,i,j]
# properly copy the port impedances
composite.z0[:,a] = two_port.z0[:,0]
composite.z0[:,b] = two_port.z0[:,1]
# finally renormalize from
composite.renormalize(50)
```

## Results¶

### Self-Consistency¶

Note that 6-measurements of 2-port subnetworks works out to 24-sparameters, and we only need 16. This is because each reflect, s-parameter is measured three-times. As, in [1], we will use this redundent measurement as a check of our accuracy.

The renormalized networks are stored in a dictionary with names based on their port indecies, from this you can see that each have been renormalized to the appropriate z0.

```
[7]:
```

```
measured_renorm
```

```
[7]:
```

```
{'01': 2-Port Network: 'dut', 75.0-110.0 GHz, 101 pts, z0=[59.75609756+12.19512195j 67.64705882-29.41176471j],
'02': 2-Port Network: 'dut', 75.0-110.0 GHz, 101 pts, z0=[59.75609756+12.19512195j 70.68965517+51.72413793j],
'03': 2-Port Network: 'dut', 75.0-110.0 GHz, 101 pts, z0=[ 59.75609756+12.19512195j 150. +0.j ],
'12': 2-Port Network: 'dut', 75.0-110.0 GHz, 101 pts, z0=[67.64705882-29.41176471j 70.68965517+51.72413793j],
'13': 2-Port Network: 'dut', 75.0-110.0 GHz, 101 pts, z0=[ 67.64705882-29.41176471j 150. +0.j ],
'23': 2-Port Network: 'dut', 75.0-110.0 GHz, 101 pts, z0=[ 70.68965517+51.72413793j 150. +0.j ]}
```

Plotting all three raw measurements of \(S_{11}\), we can see that they are not in agreement. These plots answer to plots 5 and 7 of [1]

```
[8]:
```

```
s11_set = rf.NS([measured[k] for k in measured if k[0]=='0'])
figure(figsize = (8,4))
subplot(121)
s11_set .plot_s_db(0,0)
subplot(122)
s11_set .plot_s_deg(0,0)
tight_layout()
```

However, the renormalized measurements agree perfectly. These plots answer to plots 6 and 8 of [1]

```
[9]:
```

```
s11_set = rf.NS([measured_renorm[k] for k in measured_renorm if k[0]=='0'])
figure(figsize = (8,4))
subplot(121)
s11_set .plot_s_db(0,0)
subplot(122)
s11_set .plot_s_deg(0,0)
tight_layout()
```

### Test For Accuracy¶

Making sure our composite network is the same as our DUT

```
[10]:
```

```
composite == dut
```

```
[10]:
```

```
True
```

Nice!. How close ?

```
[11]:
```

```
sum((composite - dut).s_mag)
```

```
[11]:
```

```
9.917536367984054e-13
```

Dang!

## Practical Application¶

This could be used in many ways. In waveguide, one could just make a measurement of a radiating open after a standard two-port calibration (like TRL). Then using *Tippets technique*, you can leave each port wide open while not being measured. This way you dont have to buy a bunch of loads. How sweet would that be?

## More Complex Simulations¶

```
[12]:
```

```
def tippits(dut, gamma, noise=None):
'''
simulate tippits technique on a 4-port dut.
'''
ports = arange(dut.nports)
port_combos = list(combinations(ports, 2))
loads = [wg.load(gamma) for k in ports]
# construct the impedance array, of shape FXN
z_loads = array([k.z.flatten() for k in loads]).T
composite = wg.match(nports = dut.nports) # composite network, to be filled.
#measured,measured_renorm = {},{} # measured subnetworks and renormalized sub-networks
# ports `a` and `b` are the ports we will connect the VNA too
for a,b in port_combos:
# port `c` and `d` are the ports which we will connect the loads too
c,d =ports[(ports!=a)& (ports!=b)]
# determine where `d` will be on four_port, after its reduced to a three_port
e = where(ports[ports!=c]==d)[0][0]
# connect loads
three_port = rf.connect(dut,c, loads[c],0)
two_port = rf.connect(three_port,e, loads[d],0)
if noise is not None:
two_port.add_noise_polar(*noise)
# save raw and renormalized 2-port subnetworks
measured['%i%i'%(a,b)] = two_port.copy()
two_port.renormalize(c_[z_loads[:,a],z_loads[:,b]])
measured_renorm['%i%i'%(a,b)] = two_port.copy()
# stuff this 2-port into the composite 4-port
for i,m in enumerate([a,b]):
for j,n in enumerate([a,b]):
composite.s[:,m,n] = two_port.s[:,i,j]
# properly copy the port impedances
composite.z0[:,a] = two_port.z0[:,0]
composite.z0[:,b] = two_port.z0[:,1]
# finally renormalize from
composite.renormalize(50)
return composite
```

```
[13]:
```

```
wg.frequency.npoints = 11
dut = wg.random(4)
#er = lambda gamma: mean((tippits(dut,gamma)-dut).s_mag)/mean(dut.s_mag)
def er(gamma, *args):
return max(abs(tippits(dut, rf.db_2_mag(gamma),*args).s_db-dut.s_db).flatten())
gammas = linspace(-80,0,11)
title('Error vs $|\Gamma|$')
plot(gammas, [er(k) for k in gammas])
plot(gammas, [er(k) for k in gammas])
semilogy()
xlabel('$|\Gamma|$ of Loads (dB)')
ylabel('Max Error in DUT\'s dB(S)')
figure()
#er = lambda gamma: max(abs(tippits(dut,gamma,(1e-5,.1)).s_db-dut.s_db).flatten())
noise = (1e-5,.1)
title('Error vs $|\Gamma|$ with reasonable noise')
plot(gammas, [er(k, noise) for k in gammas])
plot(gammas, [er(k,noise) for k in gammas])
semilogy()
xlabel('$|\Gamma|$ of Loads (dB)')
ylabel('Max Error in DUT\'s dB(S)')
```

```
/home/docs/checkouts/readthedocs.org/user_builds/scikit-rf/envs/latest/lib/python3.7/site-packages/skrf/network.py:4784: RuntimeWarning: invalid value encountered in sqrt
npy.einsum('ijj->ij', F)[...] = 1.0/npy.sqrt(z0.real)*0.5
/home/docs/checkouts/readthedocs.org/user_builds/scikit-rf/envs/latest/lib/python3.7/site-packages/skrf/network.py:4526: RuntimeWarning: invalid value encountered in sqrt
npy.einsum('ijj->ij', F)[...] = 1.0/npy.sqrt(z0.real)*0.5
```

```
---------------------------------------------------------------------------
LinAlgError Traceback (most recent call last)
<ipython-input-1-27798b6ba67d> in <module>
10
11 title('Error vs $|\Gamma|$')
---> 12 plot(gammas, [er(k) for k in gammas])
13 plot(gammas, [er(k) for k in gammas])
14 semilogy()
<ipython-input-1-27798b6ba67d> in <listcomp>(.0)
10
11 title('Error vs $|\Gamma|$')
---> 12 plot(gammas, [er(k) for k in gammas])
13 plot(gammas, [er(k) for k in gammas])
14 semilogy()
<ipython-input-1-27798b6ba67d> in er(gamma, *args)
4 #er = lambda gamma: mean((tippits(dut,gamma)-dut).s_mag)/mean(dut.s_mag)
5 def er(gamma, *args):
----> 6 return max(abs(tippits(dut, rf.db_2_mag(gamma),*args).s_db-dut.s_db).flatten())
7
8 gammas = linspace(-80,0,11)
<ipython-input-1-ec21708c8bf8> in tippits(dut, gamma, noise)
44
45 # finally renormalize from
---> 46 composite.renormalize(50)
47
48 return composite
~/checkouts/readthedocs.org/user_builds/scikit-rf/envs/latest/lib/python3.7/site-packages/skrf/network.py in renormalize(self, z_new, s_def)
2648 fix_z0_shape
2649 '''
-> 2650 self.s = renormalize_s(self.s, self.z0, z_new, s_def)
2651 self.z0 = fix_z0_shape(z_new, self.frequency.npoints, self.nports)
2652
~/checkouts/readthedocs.org/user_builds/scikit-rf/envs/latest/lib/python3.7/site-packages/skrf/network.py in renormalize_s(s, z_old, z_new, s_def)
5743 raise ValueError('s_def parameter should be either:', S_DEFINITIONS)
5744 # thats a heck of a one-liner!
-> 5745 return z2s(s2z(s, z0=z_old, s_def=s_def), z0=z_new, s_def=s_def)
5746
5747
~/checkouts/readthedocs.org/user_builds/scikit-rf/envs/latest/lib/python3.7/site-packages/skrf/network.py in s2z(s, z0, s_def)
4527 npy.einsum('ijj->ij', G)[...] = z0
4528 # z = npy.linalg.inv(F) @ npy.linalg.inv(Id - s) @ (s @ G + npy.conjugate(G)) @ F # Python > 3.5
-> 4529 z = npy.matmul(npy.linalg.inv(F),
4530 npy.matmul(npy.linalg.inv(Id - s),
4531 npy.matmul(npy.matmul(s, G) + npy.conjugate(G), F)))
<__array_function__ internals> in inv(*args, **kwargs)
~/checkouts/readthedocs.org/user_builds/scikit-rf/envs/latest/lib/python3.7/site-packages/numpy/linalg/linalg.py in inv(a)
544 signature = 'D->D' if isComplexType(t) else 'd->d'
545 extobj = get_linalg_error_extobj(_raise_linalgerror_singular)
--> 546 ainv = _umath_linalg.inv(a, signature=signature, extobj=extobj)
547 return wrap(ainv.astype(result_t, copy=False))
548
~/checkouts/readthedocs.org/user_builds/scikit-rf/envs/latest/lib/python3.7/site-packages/numpy/linalg/linalg.py in _raise_linalgerror_singular(err, flag)
86
87 def _raise_linalgerror_singular(err, flag):
---> 88 raise LinAlgError("Singular matrix")
89
90 def _raise_linalgerror_nonposdef(err, flag):
LinAlgError: Singular matrix
```