Thursday 19 December 2019

Tandem Directional Coupler: Circuit Theory Approach

This document discusses the analysis of an ideal “tandem” directional coupler using lumped element circuit theory.

A directional coupler is a four port device that is used to sample the forward and reflected wave in a transmission line. It is a key component of a VNA and can be used to measure power flowing in a transmission line, but it can have countless possible uses. Generally they are built using distributed elements structures, but there are also versions built using lumped elements.

The tandem directional coupler, or also tandem bridge, is one of those directional couplers that is built using a lumped element approach. This kind of directional coupler, famous for radio-hams, has inherently large bandwidth and can handle a lot of power. It is also well suited for low frequency scenarios, down to the kHz range. It is composed of two current transformers, or two transformers with a high turn ratio, and two termination resistors with the desired characteristic impedance \(Z_0\), which can be arbitrary but usually 50\(\Omega\).

To give an example, by changing those terminations it is possible to measure S-parameters with a different characteristic impedance and without the need to use any post-processing. It is worth saying that many instruments have a characteristic impedance of 50 Ohm (why 50? There are many articles about power handling, attenuation, etc.)

The load can be also seen as the transformed load after an arbitrary transmission line of arbitrary length at the directional coupler output. This circuit analysis does not include additional losses in the transformers, which would slightly change the insertion loss, and non ideal transformers, which would introduce frequency dependent relations. Including those non-idealities is not fundamental for grasping its behavior and introduces complications for the solutions, which would hide the important aspects. They can be introduced using a SPICE simulation and is done in the section Spice Simulation.

The coupled outputs are taken on \(\mathrm{R_{fw}}\) (forward) and \(\mathrm{R_{rw}}\) (reverse), while the output is on \(\mathrm{R_{L}}\).

The input consists of an ideal voltage generator, with a zero internal impedance, and will results in a load dependent forward (and reverse) voltage. If the generator impedance was equal to \(Z_0\), the forward voltage would be without any dependence on the load impedance \(Z_L\). The analysis does not have any loss of generality, as the analysis with a \(Z_0\) generator impedance can be easily obtained by adding a voltage divider in the generator voltage.

I decided to use Python for performing the analysis: the symbolic mathematics package Sympy for solving the linear system of equations, numpy for numerical stuff, numba and multiprocessing to speed up some number crunching functions and matplotlib for plotting.
# Setting up packages
%reset -f
%matplotlib inline
%config InlineBackend.figure_format = 'svg'
from multiprocessing import Pool
import sympy as sy
import numpy as np
import matplotlib.pyplot as plt
from cycler import cycler
from IPython.display import *
sy.init_printing()

Symbolic solution

In this section, the circuit is solved using a mesh analysis.
Vin, VL1,  VL2, RL, Rfw, Rrw, N, i1, i2, i3, i4, Z0 = sy.symbols(
    r'V_{in}, V_{L1}, V_{L2}, R_L, R_{fw}, R_{rw}, N, i_1, i_2, i_3, i_4, Z_0')
Vin, Rfw, Rrw, N, Z0 = sy.symbols(
    r'V_{in}, R_{fw}, R_{rw}, N, Z_0', real=True)

eq = [Vin - VL1 - RL*(i1 - i4)]
eq += [N*VL1 - Rfw * (i2 - i3)]
eq += [Rfw * (i2 - i3) - Rrw * i3 - VL2]
eq += [-VL2 * N + RL * (i1 - i4)]
eq += [i1/N - i2]
eq += [i4 + i3/N]

sol = sy.solve(eq, (VL1, VL2, i1, i2, i3, i4))
display(sol)

The solutions to the previous system of equations is: \[ \begin{aligned} V_{L1} =& \frac{R_{fw} V_{in} \left(N^{2} R_{L} + N^{2} R_{rw} + R_{L}\right)}{N^{4} R_{L} R_{fw} + N^{4} R_{L} R_{rw} + 2 N^{2} R_{L} R_{fw} + N^{2} R_{fw} R_{rw} + R_{L} R_{fw}}\\[2mm] V_{L2} =& \frac{N R_{L} V_{in} \left(N^{2} \left(R_{fw} + R_{rw}\right) + R_{fw}\right)}{N^{4} R_{L} \left(R_{fw} + R_{rw}\right) + 2 N^{2} R_{L} R_{fw} - N^{2} R_{fw}^{2} + N^{2} R_{fw} \left(R_{fw} + R_{rw}\right) + R_{L} R_{fw}}\\[2mm] i_{1} =& \frac{N^{2} V_{in} \left(N^{2} \left(R_{fw} + R_{rw}\right) + R_{L}\right)}{N^{4} R_{L} \left(R_{fw} + R_{rw}\right) + 2 N^{2} R_{L} R_{fw} - N^{2} R_{fw}^{2} + N^{2} R_{fw} \left(R_{fw} + R_{rw}\right) + R_{L} R_{fw}}\\[2mm] i_{2} =& \frac{N V_{in} \left(N^{2} \left(R_{fw} + R_{rw}\right) + R_{L}\right)}{N^{4} R_{L} \left(R_{fw} + R_{rw}\right) + 2 N^{2} R_{L} R_{fw} - N^{2} R_{fw}^{2} + N^{2} R_{fw} \left(R_{fw} + R_{rw}\right) + R_{L} R_{fw}}\\[2mm] i_{3} =& - \frac{N^{3} V_{in} \left(R_{L} - R_{fw}\right)}{N^{4} R_{L} R_{fw} + N^{4} R_{L} R_{rw} + 2 N^{2} R_{L} R_{fw} + N^{2} R_{fw} R_{rw} + R_{L} R_{fw}}\\[2mm] i_{4} =& \frac{N^{2} V_{in} \left(R_{L} - R_{fw}\right)}{N^{4} R_{L} R_{fw} + N^{4} R_{L} R_{rw} + 2 N^{2} R_{L} R_{fw} + N^{2} R_{fw} R_{rw} + R_{L} R_{fw}} \end{aligned} \]

Vout = (Vin - sol[VL1]).simplify()
Vfw = ((sol[i2] - sol[i3]) * Rfw).simplify()
Vrw = (sol[i3] * Rrw).simplify()

Pin = sy.re(Vin * sol[i1]/2)
Pout = (sol[i1] - sol[i4])**2 * sy.re(RL)/2
Pfw = (sol[i2] - sol[i3])**2 * Rfw/2
Prw = (sol[i3])**2 * Rrw/2

L = (Pin/Pout)
C = (Pin/Pfw)
I = (Pin/Prw)
D = (I / C)
eff = (1/L)
dircouplerf = [L, C, I, D]

p = Pool(processes = 8)
p.map(sy.simplify, dircouplerf)

Ls, Cs, Is, Ds = sy.symbols(r'L, C, I, D')
[display(sy.Eq(x, y)) for x, y in zip([Ls, Cs, Is, Ds], dircouplerf)];

\[ L = \frac{2 \left(\frac{N^{2} V_{in}^{2} \left(N^{2} \left(R_{fw} + R_{rw}\right) + \operatorname{re}{\left(R_{L}\right)}\right) \left(N^{4} \left(R_{fw} + R_{rw}\right) \operatorname{re}{\left(R_{L}\right)} - N^{2} R_{fw}^{2} + N^{2} R_{fw} \left(R_{fw} + R_{rw}\right) + 2 N^{2} R_{fw} \operatorname{re}{\left(R_{L}\right)} + R_{fw} \operatorname{re}{\left(R_{L}\right)}\right)}{2 \left(\left(N^{4} \left(R_{fw} + R_{rw}\right) \operatorname{im}{\left(R_{L}\right)} + 2 N^{2} R_{fw} \operatorname{im}{\left(R_{L}\right)} + R_{fw} \operatorname{im}{\left(R_{L}\right)}\right)^{2} + \left(N^{4} \left(R_{fw} + R_{rw}\right) \operatorname{re}{\left(R_{L}\right)} - N^{2} R_{fw}^{2} + N^{2} R_{fw} \left(R_{fw} + R_{rw}\right) + 2 N^{2} R_{fw} \operatorname{re}{\left(R_{L}\right)} + R_{fw} \operatorname{re}{\left(R_{L}\right)}\right)^{2}\right)} - \frac{N^{2} V_{in}^{2} \left(- N^{4} \left(R_{fw} + R_{rw}\right) \operatorname{im}{\left(R_{L}\right)} - 2 N^{2} R_{fw} \operatorname{im}{\left(R_{L}\right)} - R_{fw} \operatorname{im}{\left(R_{L}\right)}\right) \operatorname{im}{\left(R_{L}\right)}}{2 \left(\left(N^{4} \left(R_{fw} + R_{rw}\right) \operatorname{im}{\left(R_{L}\right)} + 2 N^{2} R_{fw} \operatorname{im}{\left(R_{L}\right)} + R_{fw} \operatorname{im}{\left(R_{L}\right)}\right)^{2} + \left(N^{4} \left(R_{fw} + R_{rw}\right) \operatorname{re}{\left(R_{L}\right)} - N^{2} R_{fw}^{2} + N^{2} R_{fw} \left(R_{fw} + R_{rw}\right) + 2 N^{2} R_{fw} \operatorname{re}{\left(R_{L}\right)} + R_{fw} \operatorname{re}{\left(R_{L}\right)}\right)^{2}\right)}\right)}{\left(- \frac{N^{2} V_{in} \left(R_{L} - R_{fw}\right)}{N^{4} R_{L} R_{fw} + N^{4} R_{L} R_{rw} + 2 N^{2} R_{L} R_{fw} + N^{2} R_{fw} R_{rw} + R_{L} R_{fw}} + \frac{N^{2} V_{in} \left(N^{2} \left(R_{fw} + R_{rw}\right) + R_{L}\right)}{N^{4} R_{L} \left(R_{fw} + R_{rw}\right) + 2 N^{2} R_{L} R_{fw} - N^{2} R_{fw}^{2} + N^{2} R_{fw} \left(R_{fw} + R_{rw}\right) + R_{L} R_{fw}}\right)^{2} \operatorname{re}{\left(R_{L}\right)}} \] \[ C = \frac{2 \left(\frac{N^{2} V_{in}^{2} \left(N^{2} \left(R_{fw} + R_{rw}\right) + \operatorname{re}{\left(R_{L}\right)}\right) \left(N^{4} \left(R_{fw} + R_{rw}\right) \operatorname{re}{\left(R_{L}\right)} - N^{2} R_{fw}^{2} + N^{2} R_{fw} \left(R_{fw} + R_{rw}\right) + 2 N^{2} R_{fw} \operatorname{re}{\left(R_{L}\right)} + R_{fw} \operatorname{re}{\left(R_{L}\right)}\right)}{2 \left(\left(N^{4} \left(R_{fw} + R_{rw}\right) \operatorname{im}{\left(R_{L}\right)} + 2 N^{2} R_{fw} \operatorname{im}{\left(R_{L}\right)} + R_{fw} \operatorname{im}{\left(R_{L}\right)}\right)^{2} + \left(N^{4} \left(R_{fw} + R_{rw}\right) \operatorname{re}{\left(R_{L}\right)} - N^{2} R_{fw}^{2} + N^{2} R_{fw} \left(R_{fw} + R_{rw}\right) + 2 N^{2} R_{fw} \operatorname{re}{\left(R_{L}\right)} + R_{fw} \operatorname{re}{\left(R_{L}\right)}\right)^{2}\right)} - \frac{N^{2} V_{in}^{2} \left(- N^{4} \left(R_{fw} + R_{rw}\right) \operatorname{im}{\left(R_{L}\right)} - 2 N^{2} R_{fw} \operatorname{im}{\left(R_{L}\right)} - R_{fw} \operatorname{im}{\left(R_{L}\right)}\right) \operatorname{im}{\left(R_{L}\right)}}{2 \left(\left(N^{4} \left(R_{fw} + R_{rw}\right) \operatorname{im}{\left(R_{L}\right)} + 2 N^{2} R_{fw} \operatorname{im}{\left(R_{L}\right)} + R_{fw} \operatorname{im}{\left(R_{L}\right)}\right)^{2} + \left(N^{4} \left(R_{fw} + R_{rw}\right) \operatorname{re}{\left(R_{L}\right)} - N^{2} R_{fw}^{2} + N^{2} R_{fw} \left(R_{fw} + R_{rw}\right) + 2 N^{2} R_{fw} \operatorname{re}{\left(R_{L}\right)} + R_{fw} \operatorname{re}{\left(R_{L}\right)}\right)^{2}\right)}\right)}{R_{fw} \left(\frac{N^{3} V_{in} \left(R_{L} - R_{fw}\right)}{N^{4} R_{L} R_{fw} + N^{4} R_{L} R_{rw} + 2 N^{2} R_{L} R_{fw} + N^{2} R_{fw} R_{rw} + R_{L} R_{fw}} + \frac{N V_{in} \left(N^{2} \left(R_{fw} + R_{rw}\right) + R_{L}\right)}{N^{4} R_{L} \left(R_{fw} + R_{rw}\right) + 2 N^{2} R_{L} R_{fw} - N^{2} R_{fw}^{2} + N^{2} R_{fw} \left(R_{fw} + R_{rw}\right) + R_{L} R_{fw}}\right)^{2}} \] \[ I = \frac{2 \left(\frac{N^{2} V_{in}^{2} \left(N^{2} \left(R_{fw} + R_{rw}\right) + \operatorname{re}{\left(R_{L}\right)}\right) \left(N^{4} \left(R_{fw} + R_{rw}\right) \operatorname{re}{\left(R_{L}\right)} - N^{2} R_{fw}^{2} + N^{2} R_{fw} \left(R_{fw} + R_{rw}\right) + 2 N^{2} R_{fw} \operatorname{re}{\left(R_{L}\right)} + R_{fw} \operatorname{re}{\left(R_{L}\right)}\right)}{2 \left(\left(N^{4} \left(R_{fw} + R_{rw}\right) \operatorname{im}{\left(R_{L}\right)} + 2 N^{2} R_{fw} \operatorname{im}{\left(R_{L}\right)} + R_{fw} \operatorname{im}{\left(R_{L}\right)}\right)^{2} + \left(N^{4} \left(R_{fw} + R_{rw}\right) \operatorname{re}{\left(R_{L}\right)} - N^{2} R_{fw}^{2} + N^{2} R_{fw} \left(R_{fw} + R_{rw}\right) + 2 N^{2} R_{fw} \operatorname{re}{\left(R_{L}\right)} + R_{fw} \operatorname{re}{\left(R_{L}\right)}\right)^{2}\right)} - \frac{N^{2} V_{in}^{2} \left(- N^{4} \left(R_{fw} + R_{rw}\right) \operatorname{im}{\left(R_{L}\right)} - 2 N^{2} R_{fw} \operatorname{im}{\left(R_{L}\right)} - R_{fw} \operatorname{im}{\left(R_{L}\right)}\right) \operatorname{im}{\left(R_{L}\right)}}{2 \left(\left(N^{4} \left(R_{fw} + R_{rw}\right) \operatorname{im}{\left(R_{L}\right)} + 2 N^{2} R_{fw} \operatorname{im}{\left(R_{L}\right)} + R_{fw} \operatorname{im}{\left(R_{L}\right)}\right)^{2} + \left(N^{4} \left(R_{fw} + R_{rw}\right) \operatorname{re}{\left(R_{L}\right)} - N^{2} R_{fw}^{2} + N^{2} R_{fw} \left(R_{fw} + R_{rw}\right) + 2 N^{2} R_{fw} \operatorname{re}{\left(R_{L}\right)} + R_{fw} \operatorname{re}{\left(R_{L}\right)}\right)^{2}\right)}\right) \left(N^{4} R_{L} R_{fw} + N^{4} R_{L} R_{rw} + 2 N^{2} R_{L} R_{fw} + N^{2} R_{fw} R_{rw} + R_{L} R_{fw}\right)^{2}}{N^{6} R_{rw} V_{in}^{2} \left(R_{L} - R_{fw}\right)^{2}} \] \[ D = \frac{R_{fw} \left(\frac{N^{3} V_{in} \left(R_{L} - R_{fw}\right)}{N^{4} R_{L} R_{fw} + N^{4} R_{L} R_{rw} + 2 N^{2} R_{L} R_{fw} + N^{2} R_{fw} R_{rw} + R_{L} R_{fw}} + \frac{N V_{in} \left(N^{2} \left(R_{fw} + R_{rw}\right) + R_{L}\right)}{N^{4} R_{L} \left(R_{fw} + R_{rw}\right) + 2 N^{2} R_{L} R_{fw} - N^{2} R_{fw}^{2} + N^{2} R_{fw} \left(R_{fw} + R_{rw}\right) + R_{L} R_{fw}}\right)^{2} \left(N^{4} R_{L} R_{fw} + N^{4} R_{L} R_{rw} + 2 N^{2} R_{L} R_{fw} + N^{2} R_{fw} R_{rw} + R_{L} R_{fw}\right)^{2}}{N^{6} R_{rw} V_{in}^{2} \left(R_{L} - R_{fw}\right)^{2}} \]

The relations above, which are quite long, represents the characteristic parameters of the directional coupler with arbitrary terminations but without additional losses:

  • L is the Insertion Loss
  • C is the Coupling
  • I is the Isolation
  • D is the Directivity

Those can be simplified to the definition case, when the terminations and the load are the same as the characteristic impedance \(Z_0\). Those parameters are obtained from the general expression above, with \(1:50\) transformers. Those are the ideal expressions for this directional coupler without considering uncertainties on the parameters. The general expressions can include uncertainties and be evaluated using random variables or numerically.

subs = {Rrw:Z0, Rfw:Z0, RL:Z0, N:50}
effs = sy.symbols(r'eff')
coupler_param = {Cs: C.subs(subs).simplify(),
 Is: I.subs(subs).simplify(),
 Ls: L.subs(subs).simplify(),
 Ds: D.subs(subs).simplify(),
 effs: (1/L.subs(subs)).simplify(),
}
[display(sy.Eq(key, val)) for key, val in coupler_param.items()];

\[ \begin{aligned} C &= 2501\\[2mm] I &= \tilde{\infty} Z_{0}^{2}\\[2mm] L &= \frac{2501}{2500}\\[2mm] D &= \tilde{\infty} Z_{0}^{2}\\[2mm] eff &= \frac{2500}{2501} \end{aligned} \]

It is possible to notice that the directivity ideally \(\to \infty\) and that the insertion loss is approximately \(N^{2}\), as it can be obtained by a simplified analysis using waves or again a circuit theory approach.

Spice Simulation

In the following is the spice netlist of the simulation, which will be used later for comparison with the theoretical results.
* Simulation of the directional coupler
* Alex Pacini, 2019

Vin Vin_ 0 SIN(0 10 200E3) AC 1
RVin Vin_ Vin 1u

L11 Vin Vout 1E-6
L12 Vfwd 0 2.5E-3
K1 L11 L12 1
Rfwd Vfwd 0 49.98
L21 Vfwd Vref 1E-6
Rref Vref 0 50
L22 Vout 0 2.5E-3
K2 L21 L22 1

RL Vout 0 {RL}


***********************************************************
*.tran 0 16u 10u 100n
*.print tran V(Vout) I(RL) V(Vfwd) V(Vref)

.step param RL 1 1000 1
.ac lin 1 200E3 200E3
*.print ac V(Vout) I(RL) V(Vfwd) V(Vref)

The following code is used to import the LTSpice exported data to ascii (from the plot, as LTSpice does not normally print to a file).

import re

def converter_func(s):
    subs = {r',(?!-)(.*)': r'+\1j',
            r',(?=-)(.*)': r'\1j',
           }
    for key, val in subs.items():
        s = re.sub(key, val, s)
    return complex(s)

converters = {
        1: converter_func,
        2: converter_func,
        3: converter_func,
        }

sim = np.genfromtxt('dcoupler.txt', delimiter='\t', names=True,
                    dtype=None, encoding='iso-8859-1', converters=converters)

# To change the dtype, LTspice likes to call the independent variable Freq, but in
# this case it is RL
sim.dtype.names =  tuple(['RL'] + list(sim.dtype.names)[1:])
sim.dtype.names
('RL', 'Vvfwd', 'Vvout', 'Vvref')

Numerical evaluation

In this section the symbolic equations are converted to their numerical equivalent using numpy. sympy provides a tool (lambdify) for doing this conversion. Using the numba decorator on those functions, the speed-up is around five times.

from numba import njit
funcs = {key: njit(sy.lambdify((Rfw, Rrw, RL, Vin, N), val,'numpy'))
     for key, val in zip('Vout,Vfw,Vrw,i1,i2,i3,i4'.split(','),
                         [Vout, Vfw, Vrw, sol[i1], sol[i2], sol[i3], sol[i4]])}

Parameter uncertainties

This can be done using multiple approaches.

The theoretical way is to treat the forward and reverse terminations as random variables with a certain distribution. The mean value will be the nominal value, \(Z_0\), while the variance should be a value that provides the manufacturer tolerance.

With a Gaussian distribution the variance can be found by setting standard deviation to a fifth of the tolerance. The problem is that the distribution is unknown and using a Gaussian random distribution might be the wrong choice. Indeed, the distribution depends on the manufacturing process and component selection process. The selection process might select the resistors with tighter tolerance and include them in a series with better tolerance, leaving a hole in the middle of the distribution and placing de-facto all the values out of the mean value, like a bimodal distribution.

In this case it might be better to do an evaluation for the worst case for all the possible combinations, but the equations are quite tedious and analytically computing the maximum error might be overly complicated, especially because the function has multiple variables. Doing it numerically provides very similar insights, especially because the probability distribution of the random variable is unknown, and is simpler to compute.

We can test a number of steps inside that error band and select the one with the greatest distance. If done for each load, this means taking the maximum and minimum of all the possible variations of the termination resistances.

It turns out, as was also possible to expect, that the the error functions have only a local minimum and that the maximum errors are at the boundaries. This is true apart from the numerical error, which requires more values to provide a more consistent result in some specific conditions (they are discussed below).

a = np.geomspace(1, 10000, 10000)
a_sim = sim['RL']
p = 0.05
size = 11
Rfw = 50 * np.linspace(1-p, 1+p, size)
Rrw = 50 * np.linspace(1-p, 1+p, size)

args = (50, 50, a, 1, 50, size)
xlabel = 'RL'
simmap = {'Vout': sim['Vvout'],
           'Vfw': sim['Vvfwd'],
           'Vrw': sim['Vvref'],
          }

def varf(fun, Rfw, Rrw, RL, Vin, N, size):
    temp = np.empty((size, size, a.size))
    for i in range(size):
        for j in range(size):
            temp[i, j, :] = fun(Rfw[i], Rrw[j], RL, Vin, N)
    return np.stack((np.amin(temp, axis=(0,1)),
                     np.amax(temp, axis=(0,1))), 0)

rho = lambda *args: -funcs['Vrw'](*args)/funcs['Vfw'](*args)

for (key, val) in list(funcs.items()):
    if key in ['Vout','Vfw','Vrw']:
        fig = plt.figure(key)
        ax = fig.add_subplot(1,1,1)
        evf = val(*args[:-1])
        lines = plt.semilogx(a, evf, label=key)
        ax.set_xlabel(xlabel)
        if key not in ['Vout']:
            plt.plot([a.min(), a.max()], [0, 0], linewidth=0.5);
        plt.semilogx(a_sim, np.real(simmap[key]), label=(key + ' spice'),
                     color=lines[0].get_color(), linestyle='-.')
        bands = varf(val, Rfw, Rrw, a, *args[3:6])
        lines += ax.semilogx(np.repeat(a[np.newaxis, :], 2, 0).T, bands.T,
                             color=lines[0].get_color(), linewidth=0.1);
        ax.fill_between(a, bands[0], bands[1], color=lines[0].get_color(), alpha=0.2)
        ax.legend()
        fig.tight_layout()

The plots above are representative of \(V_{out}\), \(V_{fw}\) and \(V_{rw}\) with a \(V_{in}=\mathrm{1V}\). The orange line is the \(\mathrm{0V}\) line for reference.

The maximum error from the variation of the termination resistances has been included in the plot as a filled area around the main line. In this case the termination resistances are supposed to be \(\pm 5 \%\).

It is possible to see that this directional coupler has \(V_{rw}\) with the opposite sign, so when it is open \(V_{rw}\) is negative instead of positive.

Since \(C\) is defined as \(P_{in}/P_{fw}\) and the terminating resistances \(\mathrm{R_{fw}}\) and \(\mathrm{R_{rw}}\) when the coupler is in nominal conditions are \(\mathrm{Z_0}\) (also the load is \(\mathrm{R_L} = \mathrm{Z_0}\)), it follows that: \[ C = \frac{\mathrm{P_{in}}}{\mathrm{P_{fw}}} = \left(\frac{\mathrm{V_{in}}}{\mathrm{V_{fw}}}\right)^2 \] For this particular directional coupler, the forward wave is therefore \(\mathrm{Vf = Vfw/\sqrt{C}}\) and the reflected wave is \(\mathrm{Vr = -Vrw/\sqrt{C}}\). The reflected wave is scaled, with a good approximation, by the same \(\sqrt{C}\), since the definition is the same but using the output port as the input.

If an OSL calibration is added, the calibration will compensate the sign (there are various resources online, for example from Keysight (1, 2), Anritsu, 10.1109/MMM.2008.919925, A Review of VNA Calibration Methods, Multiport Vector Network Analyzer Calibration: A General Formulation).

Otherwise to get \((V_{r} + V_{f}) = V_{out}\), \(V_{rw}\) must be multiplied by \(-1\). In other words, \((V_{rw} - V_{fw})/\sqrt{C} = V_{out}\).

In the following I therefore decided to define the reflection coefficient of the load \(\mathrm{S_{11}}\) as

\[\mathrm{S_{11}} = \mathrm{-\frac{V_{rw}}{V_{fw}}}.\]

This definition, with this particular coupler, provides that \(\mathrm{\frac{V_{rw}}{V_{fw}}(0) = -1}\) as it should be by line theory.

The crossing point of the orange lines denotes the matched load (\(\mathrm{R_L}/\mathrm{Z_0} = 50/50 = 1\))

The spice simulations are included in all plots using dash dotted lines and are representative of transformers having a finite inductance. Those lines are perfectly over-imposed to the theoretical results.

fig = plt.figure('rho')
ax = fig.add_subplot(1,1,1)
evf = rho(*args[:-1])
lines = ax.semilogx(a/50, evf, label='S11')
zero = ax.semilogx([a.min()/50,a.max()/50], [0,0], linewidth=0.5)
plt.semilogx([1,1], [-1,1], linewidth=0.5, color=zero[0].get_color())
plt.semilogx(a_sim/50, np.real(-simmap['Vrw']/simmap['Vfw']), label='S11 spice', linewidth=0.5)
bands = varf(rho, Rfw, Rrw, a, *args[3:6])
lines += ax.semilogx(np.repeat((a/50)[np.newaxis, :], 2, 0).T, bands.T,
                     color=lines[0].get_color(), linewidth=0.1);
ax.fill_between(a/50, bands[0], bands[1], color=lines[0].get_color(), alpha=0.2)
ax.set_xlabel(xlabel + '/50');
ax.legend()
fig.tight_layout()

In this plot it is clear that the error due to the tolerance is mostly affecting the impedance near the open load.

It is worth noting that this variation, if it is constant during time, can be effectively reduced using the calibration procedure.

def z(*args):
    return 50 * (1 + rho(*args))/(1 - rho(*args))

fig = plt.figure('z')
ax = fig.add_subplot(1,1,1)
lines = ax.loglog(a, z(*args[:-1]), label='RL sense')
bands = varf(z, Rfw, Rrw, a, *args[3:6])
lines += ax.plot(np.repeat(a[np.newaxis, :], 2, 0).T, bands.T,
                     color=lines[0].get_color(), linewidth=0.1);
ax.fill_between(a, bands[0], bands[1], color=lines[0].get_color(), alpha=0.2)
ax.set_xlabel(xlabel);
ax.legend()
fig.tight_layout()

The large variation is because \(\mathrm{S_{11}}\) gets near to one and a singularity arises, which creates the large oscillations between positive and negative values (the log of a negative value does not exist in the real space). This is due to the tolerance in the resistor and represents all the possible variations of the impedance due to the tolerance. There is also some numerical error in the plot.

A OSL calibration can compensate most of those errors and restore the proper relation when the errors remain constant in time after calibration. Consider anyway that the calibration can still affect the accuracy due to noise, since the range of inputs are reduced. Refer to the previously listed resources for more information.

The following is an example of calibration using the package scikit-rf. The frequency is not important and is selected to be 1 Hz just to create the network object.

import skrf as rf
n = rf.Network()
n.frequency = rf.Frequency.from_f([1], unit='Hz')
n.s = [0] # To initialize the network

ideals = [n.copy() for i in range(3)]
ideals[0].s = [-1] # Short
ideals[1].s = [0] # Load
ideals[2].s = [1] # Open

def rhof(Rfw, Rrw, RL, Vin, N, size):
    temp = np.empty((size, size, a.size))
    for i in range(size):
        for j in range(size):
            temp[i, j, :] = rho(Rfw[i], Rrw[j], RL, Vin, N)
    return np.unravel_index(temp.argmax(), temp.shape)

x = rhof(Rfw, Rrw, 50, 1, 50, size)
rho(Rfw[x[0]], Rrw[x[1]], 50, 1, 50)

measured = [n.copy() for i in range(3)]
measured[0].s = rho(Rfw[x[0]], Rrw[x[1]], 0, 1, 50) # Short
measured[1].s = rho(Rfw[x[0]], Rrw[x[1]], 50, 1, 50) # Load
measured[2].s = rho(Rfw[x[0]], Rrw[x[1]], 1E10, 1, 50) # Open

cal = rf.OnePort(ideals = ideals, measured = measured)
cal.run()

def dutf(i):
    temp = n.copy()
    temp.s = rho(Rfw[x[0]], Rrw[x[1]], i, 1, 50)
    return temp

p = Pool(processes = 8)
dut = p.map(dutf, a)
dut_cal = p.map(cal.apply_cal, dut)

ttt_uncal = [dut_i.z[0,0,0] for dut_i in dut]
ttt = [dut_cal_i.z[0,0,0] for dut_cal_i in dut_cal]

plt.plot(a, np.real(ttt), label='RL(S11) - determ.')
plt.plot(a, np.real(ttt_uncal), label='RL(S11) - uncal.')
plt.loglog(a, np.real(z(*args[:-1])), label='RL(S11) - cal.')
plt.legend();

The previous plot proves that in a condition where the error due to a variation of the termination resistance creates a very high error, if this is not time dependent, it can be corrected with a calibration. Anyway, when the input has noise, it affects the result by increasing the inaccuracy, as stated before.

The code block below defines the powers in the output (\(\mathrm{P_{out}}\)) and on the coupler ports (\(\mathrm{P_{fw}}\), \(\mathrm{P_{rw}}\)):
def Pin(Rfw, Rrw, RL, Vin, N):
    return np.real(Vin * funcs['i1'](Rfw, Rrw, RL, Vin, N) * 0.5)

def Pout(Rfw, Rrw, RL, Vin, N):
    return np.real(funcs['Vout'](Rfw, Rrw, RL, Vin, N) *
                   (funcs['i1'](Rfw, Rrw, RL, Vin, N) -
                    funcs['i4'](Rfw, Rrw, RL, Vin, N)) * 0.5)

def Pfw(Rfw, Rrw, RL, Vin, N):
    return np.real(Rfw * (funcs['i2'](Rfw, Rrw, RL, Vin, N) -
                          funcs['i3'](Rfw, Rrw, RL, Vin, N))**2 * 0.5)

def Prw(Rfw, Rrw, RL, Vin, N):
    return np.real(Rrw * (funcs['i3'](Rfw, Rrw, RL, Vin, N))**2 * 0.5)
The following is a check for the Tellegen theorem, or that the sum of all the powers in the circuit must be zero. This is a statement of conservation of energy. The figure shows that energy is conserved apart from a small numerical error of maximum \(10^{16}\).
#Tellegen check
tellegen = lambda *args: Pin(*args) - (Pout(*args) + Pfw(*args) + Prw(*args))
plt.figure('tellegen')
plt.semilogx(a, tellegen(*args[:-1]), label='Sum P', linewidth=0.5)
plt.xlabel(xlabel);
plt.tight_layout();

In the following part of this section, the parameters of the coupler are shown when subject to parameter uncertainties. Note that the coupler parameters, by definition, are defined only when \(R_L= 50\), and is indicated with a orange line.

The measurements in the following section are performed with a 50 ohm termination, which can have its small uncertainty around the orange vertical line. If it can be supposed to be 50 ohm, as per definitions, the uncertainty will be mainly on the coupled output terminations.

L = lambda *args: Pin(*args)/Pout(*args)
C = lambda *args: Pin(*args)/Pfw(*args)
I = lambda *args: Pin(*args)/Prw(*args)
D = lambda *args: I(*args)/C(*args)

for name, f in zip('L,C,I,D'.split(','), [L, C, I, D]):
    fig = plt.figure(name + 'db')
    ax = fig.add_subplot(1,1,1)
    evf = 10*np.log10(f(*args[:-1]))
    lines = ax.semilogx(a, evf, label=(name + ' (dB)'))
    bands = 10*np.log10(varf(f, Rfw, Rrw, a, *args[3:6]))
    lines += ax.semilogx(np.repeat(a[np.newaxis, :], 2, 0).T, bands.T,
                         color=lines[0].get_color(), linewidth=0.1);
    ax.fill_between(a, bands[0], bands[1], color=lines[0].get_color(), alpha=0.2)
    ax.set_xlabel(xlabel)
    ax.plot([50, 50], [evf.min()*0.9 - 0.01, evf.max()*1.1 + 0.01], linewidth= 0.5)
    ax.legend()
    fig.tight_layout()

from scipy import optimize
optimize.minimize(lambda RL: -(C(50, 50, RL, 1, 50)), [25])
      fun: -2501.0000999999925
 hess_inv: array([[1.9709295]])
      jac: array([0.])
  message: 'Optimization terminated successfully.'
     nfev: 54
      nit: 8
     njev: 18
   status: 0
  success: True
        x: array([49.97999471])

The previous code block minimizes the insertion loss and the result is not by chance, but because the power is not lost in the \(V_{rw}\) resistor since the reflected wave has zero amplitude.

Measurements

A directional coupler built using two Coilcraft SCS-050L_ current transformers and two Minicircuit ANNE-50L+ 50 Ohm terminations has been characterized with a R&S ZNL3 VNA. This measurement is frequency dependent, as it uses real components. The previous analysis results, without including the spice simulation, are not frequency dependent because they used ideal components.

It is worth noting that the measurements are done only with 50 Ohm terminations and, in the previous plots those would be represented by a single point in the non-dispersive (or non frequency dependent) case. If those were frequency dependent, then the plot would be a vertical band (or vertical line), similarly to the effect of uncertainty on the terminations but with a completely different meaning. The two cases shall not be confused.

# P1 in, P2 out
iLosss2p = rf.Network(file="s2p/il.s2p")

# P1 in, P2 Vfwd
couplings2p = rf.Network(file="s2p/c.s2p")

# P1 in, P2 Vrev
isolations2p = rf.Network(file="s2p/i.s2p")

# Computing directivity: Isolation/Coupling
directivityS = rf.Network()
directivityS.frequency = isolations2p.frequency
directivityS.s = isolations2p.s[:, 0, 1] / couplings2p.s[:, 0, 1]

frequency = isolations2p.frequency.f
iLoss = iLosss2p.s[:, 0, 1]
rLoss = iLosss2p.s[:, 0, 0]
coupling = couplings2p.s[:, 0, 1]
isolation = isolations2p.s[:, 0, 1]
directivity = directivityS.s[:, 0, 0]

figA = plt.figure('Insertion Loss')
axa = figA.add_subplot(1, 1, 1)
axa.set_xlabel('Frequency (kHz)')
axa.set_ylabel('Magnitude (dB)')
axa.semilogx(frequency/1E3, -20*np.log10(np.abs(iLoss)),
             label='Insertion Loss')
axa.legend()
figA.tight_layout()

figB = plt.figure('Other Directional Coupler Parameters')
axb = figB.add_subplot(1, 1, 1)
axb.set_xlabel('Frequency (kHz)')
axb.set_ylabel('Magnitude (dB)')
axb.semilogx(frequency/1E3, -20*np.log10(np.abs(rLoss)), label='Return Loss')
axb.semilogx(frequency/1E3, -20*np.log10(np.abs(coupling)), label='Coupling')
axb.semilogx(frequency/1E3, -20*np.log10(np.abs(isolation)), label='Isolation')
axb.semilogx(frequency/1E3, -20*np.log10(np.abs(directivity)), label='Directivity')
axb.legend()
figB.tight_layout()

Conclusions

This article described the tandem directional coupler using circuit theory and focused on the error due to uncertainties on the termination resistances. It also shows that the OSL calibration can theoretically minimise this error if the uncertainty is not time dependent (or dependent on the other circuit parameters).

No comments:

Post a Comment