bfu520-test-amplifier/sim/calc-matching-networks.ipynb

2.5 MiB

In [32]:
import numpy as np

import skrf
import skrf.media as media
from skrf.media import DistributedCircuit
import skrf.frequency as freq
import skrf.network as net
import skrf.util

import matplotlib.pyplot as plt
%matplotlib inline
plt.rcParams['figure.figsize'] = [10, 10]

f = freq.Frequency(0.1, 0.95, 1001)
tem = DistributedCircuit(f, z0=50)

bjt = net.Network('bfu520_7ma_attenuated10db_calibratedout_082519.s2p').interpolate(f)

bjt

bjt.plot_s_smith()
In [34]:
# well, on the bright side it stays mostly inside the unit circle
# let's calculate the matching values for 144 MHz and 915 MHz

# calculate the stability circles for the source and load impedances

idx_915mhz = skrf.util.find_nearest_index(bjt.f, 915.e+6)
idx_144mhz = skrf.util.find_nearest_index(bjt.f, 144.e+6)

sqabs = lambda x: np.square(np.absolute(x))

delta = bjt.s11.s*bjt.s22.s - bjt.s12.s*bjt.s21.s
rl = np.absolute((bjt.s12.s * bjt.s21.s)/(sqabs(bjt.s22.s) - sqabs(delta)))
cl = np.conj(bjt.s22.s - delta*np.conj(bjt.s11.s))/(sqabs(bjt.s22.s) - sqabs(delta))

rl_915mhz = rl[idx_915mhz][0, 0]
cl_915mhz = cl[idx_915mhz][0, 0]

rl_144mhz = rl[idx_144mhz][0, 0]
cl_144mhz = cl[idx_144mhz][0, 0]

rl_144mhz, cl_144mhz, rl_915mhz, cl_915mhz
Out[34]:
(0.24538732275188085,
 (0.9099636993110758+0.5418029954577884j),
 1.2327504787552828,
 (1.3139029137857448+1.8217439279569125j))
In [35]:
def calc_circle(c, r):
    theta = np.linspace(0, 2*np.pi, 1000)
    return c + r*np.exp(1.0j*theta)

def plot_smith(pts, name=None):
    n = net.Network(s=pts, name=name)
    n.plot_s_smith()

rs = np.absolute((bjt.s12.s * bjt.s21.s)/(sqabs(bjt.s11.s) - sqabs(delta)))
cs = np.conj(bjt.s11.s - delta*np.conj(bjt.s22.s))/(sqabs(bjt.s11.s) - sqabs(delta))

rs_144mhz = rs[idx_144mhz][0, 0]
cs_144mhz = cs[idx_144mhz][0, 0]
rs_915mhz = rs[idx_915mhz][0, 0]
cs_915mhz = cs[idx_915mhz][0, 0]

print(rs_144mhz, cs_144mhz, rs_915mhz, cs_915mhz)

cl_points = calc_circle(cl_915mhz, rl_915mhz)
cs_points = calc_circle(cs_915mhz, rs_915mhz)
plot_smith(cl_points)
plot_smith(cs_points)
0.9628500183606965 (0.6456138840404008+1.3232778444002673j) 15.151970228344709 (13.724619885448135+3.3431448999354894j)
In [36]:
cl_points = calc_circle(cl_144mhz, rl_144mhz)
cs_points = calc_circle(cs_144mhz, rs_144mhz)
plot_smith(cl_points)
plot_smith(cs_points)
In [37]:
# so the amplifier's pretty stable at 915 MHz, and we need to avoid inductive loads at 144 MHz

# eh let's try a simultaneous conjugate match at 144 MHz after adding some shunt resistance, and see how stable that is
# it looks like a little series output resistance would stabilize 915 MHz, so let's add some and then do a simultaneous
# conjugate match there as well

def conjugate_match(ntwk, idx):
    # that doesn't look too bad, so let's move forward and try to conjugate match
    delta2 = ntwk.s11.s[idx, 0, 0]*ntwk.s22.s[idx, 0, 0] - ntwk.s12.s[idx, 0, 0]*ntwk.s21.s[idx, 0, 0]

    B1 = 1 + sqabs(ntwk.s11.s[idx, 0, 0]) - sqabs(ntwk.s22.s[idx, 0, 0]) - sqabs(delta2)
    B2 = 1 + sqabs(ntwk.s22.s[idx, 0, 0]) - sqabs(ntwk.s11.s[idx, 0, 0]) - sqabs(delta2)
    C1 = ntwk.s11.s[idx, 0, 0] - delta2*np.conj(ntwk.s22.s[idx, 0, 0])
    C2 = ntwk.s22.s[idx, 0, 0] - delta2*np.conj(ntwk.s11.s[idx, 0, 0])

    gamma_s = (B1 - np.sqrt(np.square(B1) - 4*sqabs(C1) + 0j))/(2*C1)
    gamma_l = (B2 - np.sqrt(np.square(B2) - 4*sqabs(C2) + 0j))/(2*C2)

    z_s = net.s2z(np.array([[[gamma_s]]]))[0,0,0]
    z_l = net.s2z(np.array([[[gamma_l]]]))[0,0,0]

    return z_s, z_l

bjt_144 = bjt ** tem.shunt(tem.resistor(110.) ** tem.short())

zs_144mhz, zl_144mhz = conjugate_match(bjt_144, idx_144mhz)
zs_144mhz, zl_144mhz
Out[37]:
((84.01641752546088+116.58199865471924j),
 (79.98842398858322+49.29275687104739j))
In [38]:
def K(ntwk):
    delta2 = ntwk.s11.s*ntwk.s22.s - ntwk.s12.s*ntwk.s21.s
    k = ((1 - sqabs(ntwk.s11.s) - sqabs(ntwk.s22.s) + sqabs(delta2))/(2*np.absolute(ntwk.s12.s*ntwk.s21.s)))[:,0,0]
    return k

plt.plot(bjt_144.f, K(bjt_144))
Out[38]:
[<matplotlib.lines.Line2D at 0x7f086821fe10>]
In [39]:
def calc_matching_network(z1, z2):
    flipped = ((abs(np.imag(z2)) < 1e-6 and np.real(z1) < np.real(z2)) or
            (abs(np.imag(z2)) > 1e-6 and np.real(z1) < np.real(1/(1/z2-1/(1.j*np.imag(z2))))))
    if flipped:
        z2, z1 = z1, z2
        
    # cancel out the imaginary parts of both input and output impedances    
    z1_par = 1e+10
    if abs(np.imag(z1)) > 1e-6:
        # parallel something to cancel out the imaginary part of
        # z1's impedance
        z1_par = 1/(-1j*np.imag(1/z1))
        z1 = 1/(1./z1 + 1/z1_par)
    z2_ser = 0.0
    if abs(np.imag(z2)) > 1e-6:
        z2_ser = -1j*np.imag(z2)
        z2 = z2 + z2_ser
        
    Q = np.sqrt((np.real(z1) - np.real(z2))/np.real(z2))
    x1 = -1.j * np.real(z1)/Q
    x2 = 1.j * np.real(z2)*Q
    
    x1_tot = 1/(1/z1_par + 1/x1)
    x2_tot = z2_ser + x2
    if flipped:
        return x2_tot, x1_tot
    else:
        return x1_tot, x2_tot
    
zs_144mhz_1, zs_144mhz_2 = calc_matching_network(50., np.conj(zs_144mhz))
zl_144mhz_1, zl_144mhz_2 = calc_matching_network(np.conj(zl_144mhz), 50.)

zs_144mhz_1, zs_144mhz_2, zl_144mhz_1, zl_144mhz_2
Out[39]:
(98.94107678366402j,
 -415.7338067603979j,
 -228.72461381828086j,
 54.93861027793749j)
In [40]:
ls_144mhz = np.real(zs_144mhz_1/(2.j*np.pi*144e+6))
cs_144mhz = np.real(1/(2.j*np.pi*144e+6*zs_144mhz_2))

cl_144mhz = np.real(1/(2.j*np.pi*144e+6*zl_144mhz_1))
ll_144mhz = np.real(zl_144mhz_2/(2.j*np.pi*144e+6))

ls_144mhz, cs_144mhz, cl_144mhz, ll_144mhz
Out[40]:
(1.0935389892329788e-07,
 2.6585344814100594e-12,
 4.8321981701474735e-12,
 6.072049578008999e-08)
In [41]:
# a little bit of playing around lets me figure out which one's the shunt and which is in series
#input_network_144 = tem.inductor(ls_144mhz) ** tem.shunt_capacitor(cs_144mhz)
input_network_144 = tem.inductor(120.e-9) ** tem.shunt_capacitor(3e-12)
output_network_144 = tem.shunt_capacitor(4.7e-12) ** tem.inductor(ll_144mhz)

amplifier_144 = input_network_144 ** bjt ** tem.shunt(tem.resistor(120.) ** tem.short()) ** output_network_144

amplifier_144.plot_s_smith()
In [42]:
amplifier_144.s21.plot_s_db()
In [43]:
amplifier_144.s11.plot_s_db()
In [44]:
amplifier_144.s22.plot_s_db()
In [45]:
# that looks pretty good, I'm going to set it up and see how well it actually performs
# quick stability check (K>1 for stability)
plt.plot(amplifier_144.f, K(amplifier_144))
Out[45]:
[<matplotlib.lines.Line2D at 0x7f0862f6ca20>]
In [46]:
# let's see how it actually measures
bjt_144_matched_measurement = net.Network('bfu520_7ma_matched.s2p').interpolate(f)
bjt_144_matched_measurement.plot_s_smith()
In [47]:
bjt_144_matched_measurement.s11.plot_s_db()
bjt_144_matched_measurement.s22.plot_s_db()
In [48]:
bjt_144_matched_measurement.s21.plot_s_db()
In [49]:
# let's be accurate and use measurements from the new board
f2 = freq.Frequency(0.1, 1.2, 1101)
tem2 = DistributedCircuit(f2, z0=50)
bjt_915 = net.Network('bfu520_7ma_083019_nomatch_10dbattenuated.s2p').interpolate(f2)
bjt_915.plot_s_smith()
In [50]:
plt.plot(bjt_915.f, K(bjt_915))
Out[50]:
[<matplotlib.lines.Line2D at 0x7f0868052828>]
In [51]:
# stability's not great, let's figure out how to make it a little better
def calc_load_circles(ntwk):
    delta = ntwk.s11.s*ntwk.s22.s - ntwk.s12.s*ntwk.s21.s
    rl = np.absolute((ntwk.s12.s * ntwk.s21.s)/(sqabs(ntwk.s22.s) - sqabs(delta)))
    cl = np.conj(ntwk.s22.s - delta*np.conj(ntwk.s11.s))/(sqabs(ntwk.s22.s) - sqabs(delta))
    return cl, rl

def calc_source_circles(ntwk):
    delta = ntwk.s11.s*ntwk.s22.s - ntwk.s12.s*ntwk.s21.s
    rs = np.absolute((ntwk.s12.s * ntwk.s21.s)/(sqabs(ntwk.s11.s) - sqabs(delta)))
    cs = np.conj(ntwk.s11.s - delta*np.conj(ntwk.s22.s))/(sqabs(ntwk.s11.s) - sqabs(delta))
    return cs, rs

load_circles_915 = calc_load_circles(bjt_915)
source_circles_915 = calc_source_circles(bjt_915)

for i in range(len(load_circles_915[0])):
    (cl, rl) = (load_circles_915[0][i], load_circles_915[1][i])
    if i % 100 == 0:
        points = calc_circle(cl, rl)
        plot_smith(points, name=str(bjt_915.f[i]))
In [52]:
for i in range(len(source_circles_915[0])):
    (cl, rl) = (source_circles_915[0][i], source_circles_915[1][i])
    if i % 100 == 0:
        points = calc_circle(cl, rl)
        plot_smith(points, name=str(bjt_915.f[i]))
In [53]:
# yeah I dunno, let's just conjugate match and see if it's stable, and mess with this stuff if it isn't
idx_915mhz = skrf.util.find_nearest_index(bjt_915.f, 915.e+6)

# it wasn't stable so I added a 200 ohm shunt resistor
zs_915mhz, zl_915mhz = conjugate_match(bjt_915 ** tem2.shunt(tem2.resistor(200.) ** tem2.short()), idx_915mhz)
zs_915mhz, zl_915mhz
Out[53]:
((18.936926346631363+0.5190209442606563j),
 (55.673493495726305+52.08945298641145j))
In [54]:
# as a quick aside let's see how much the resistor improved stability

load_circles_915 = calc_load_circles(bjt_915 ** tem2.shunt(tem2.resistor(200.) ** tem2.short()))
source_circles_915 = calc_source_circles(bjt_915 ** tem2.shunt(tem2.resistor(200.) ** tem2.short()))

for i in range(len(load_circles_915[0])):
    (cl, rl) = (load_circles_915[0][i], load_circles_915[1][i])
    if i % 100 == 0:
        points = calc_circle(cl, rl)
        plot_smith(points, name=str(bjt_915.f[i]))
In [55]:
for i in range(len(source_circles_915[0])):
    (cl, rl) = (source_circles_915[0][i], source_circles_915[1][i])
    if i % 100 == 0:
        points = calc_circle(cl, rl)
        plot_smith(points, name=str(bjt_915.f[i]))
In [56]:
# so it helps a lot! good to know
# let's calculate matching networks and whatever now

zs_915mhz_1, zs_915mhz_2 = calc_matching_network(50., np.conj(zs_915mhz))
zl_915mhz_1, zl_915mhz_2 = calc_matching_network(np.conj(zl_915mhz), 50.)

zs_915mhz_1, zs_915mhz_2, zl_915mhz_1, zl_915mhz_2
Out[56]:
((1.5240705538308625e-07-39.03934622699082j),
 24.772662688828152j,
 -970.8839687874747j,
 52.158236359336954j)
In [57]:
# aside from the ~-1000j (which I'm guessing is shunt), those all look like pretty reasonable values
# let's see what they are

ls_915mhz = np.real(zs_915mhz_2/(2.j*np.pi*915e+6))
cs_915mhz = np.real(1/(2.j*np.pi*915e+6*zs_915mhz_1))

cl_915mhz = np.real(1/(2.j*np.pi*915e+6*zl_915mhz_1))
ll_915mhz = np.real(zl_915mhz_2/(2.j*np.pi*915e+6))

ls_915mhz, cs_915mhz, cl_915mhz, ll_915mhz
Out[57]:
(4.308952699972857e-09,
 4.455500548200138e-12,
 1.7915614440823858e-13,
 9.072394688026119e-09)
In [58]:
# a little bit of playing around lets me figure out which one's the shunt and which is in series
#input_network_915 = tem.inductor(ls_915mhz) ** tem.shunt_capacitor(cs_915mhz)
input_network_915 = tem2.shunt_capacitor(cs_915mhz) ** tem2.inductor(ls_915mhz)
output_network_915 = tem2.inductor(ll_915mhz)

amplifier_915 = input_network_915 ** bjt_915 ** tem2.shunt(tem2.resistor(200.) ** tem2.short()) ** output_network_915

amplifier_915.plot_s_smith()
In [59]:
# that doesn't quite look stable, but it might also just be because of measurement error;
# there were some weird spikes in the scattering parameters anyways
amplifier_915.s11.plot_s_db()
In [60]:
amplifier_915.s22.plot_s_db()
In [61]:
amplifier_915.s21.plot_s_db()
In [64]:
# let's see what the actual measurements look like
amplifier_915_measured = net.Network('bfu520_7ma_matched915.s2p').interpolate(f2)
amplifier_915_measured.plot_s_smith()
In [65]:
amplifier_915_measured.s11.plot_s_db()
In [66]:
amplifier_915_measured.s22.plot_s_db()
In [67]:
amplifier_915_measured.s21.plot_s_db()
In [ ]:
# not the best matching, but it's not too bad either
# the gain is a little low at just 14 dB, but it's probably good enough
# maybe next time I'll play around with some emitter inductor degeneration and actually using the noise data