Pi attenuators via Sagemath!

Attenuators are one of those “needed in practise, but not in theory” devices. For example, my signal generator has a frequency/counter mode – but the manual warns that the maximum safe voltage is 5V. If my “device under test” is outputting a 9v signal, then I need to bring that down by half to measure its frequency safely.

This sounds simple – potential dividers are commonly used in circuits for exactly this purpose.

A simple potential divider – let’s say consisting of two 100 ohm resistors – will half the voltage … so long as you don’t connect any load. Once you connect a load (say a 1k resistor), it appears in parallel to the lower resistor turning your 100ohm resistor into a 91ohm resistor – and now you only get 47% of the initial voltage instead of 50%. This “sagging” effect can be reduced by using smaller resistors in the potential divider, but at the cost of a larger current flowing through them, wasting lots of energy.

But when dealing with radio frequencies, we have an additional concern: we need to ensure that we stick with a 50 ohm impedance everywhere – otherwise we’ll cause reflections. This means that 1) to our upstream, we need to look like a 50ohm load (assuming our downstream load is also 50ohm), and 2) to our downstream load we need to look like a 50ohm source (assuming our upstream is 50ohm).

The simple “potential divider” doesn’t work like that. If our load is 50ohm, then a divider that uses 100ohm resistors will appear as 133ohms overall – no good!

Is there some ‘magic’ value for the resistors that’ll make everything look like a nice 50ohms? Here’s some sagemath code to calculate the resistor value you’d need …

# Resistor equations
rser(a,b) = a + b
rpar(a,b) = 1/ ((1/a) + (1/b))
potentialDivider(r,load) = rser(r,rpar(r,load))
r = var('r')
result = solve(potentialDivider(r,50) == 50,r,solution_dict=True)

So a potential divider made from 30.9ohm resistors would make the source think we’re a well-behaved 50ohm load.

But what would the load see? It’ll see the source impedance in series with one 30.9 resistor, all in parallel with the other 30.9ohm resistor – namely 22.3ohms. No good!

So a simple potential divider, using 2 resistors, isn’t working out. We need to go up to a 3-component approach.

Textbooks will tell us that we can solve this problem with a pi network; namely a series resistor with two equal-sized shunt resistor before and after it. They’ll also give us a bunch of equations for finding the resistor values for various attenuations.

But wouldn’t it be fun if we could just describe the various contraints to a computer, and have it figure out all the equations for us?

General pi attenuator from first principles

First we have to explain to sagemath what a pi-network looks like: namely, how to calculate the impedance that the source and load will see. This allows us to express the “source/load impedance must match” criteria.

Then we need a couple of helpers to let figure out what the power through the load will be. This allows us to express the attenuation (ie. the ratio between source power and load power).

# Impedance of a general pi-network (a=shunt, b=series, c=shunt), as seen from the source
rpi_src(zload,a,b,c) = rpar(a,rser(b,rpar(zload,c)))

# Impedance of a general pi network, as seen from the load
rpi_load(zsrc,a,b,c) = rpar(c,rser(b,rpar(zsrc,a)))

# If you have [v] volts across  a potential divider consisting of resistor [a] on top, [b] below, connected to [load], what voltage does the load see?
pd_vout(v, a, b, load) = v * rpar(b,load) / rser(a,rpar(b,load))

# What's the power through a resistor [r] with [v] volts across it
power(v,r) = v**2 / r

pa = var('pa') # power attenuation

# source/load impedances must match, and power
# over load and source must match attenuation
pi_eqns = [ 
    (power(pd_vout(v,b,c,zload), zload)) / power(v,zsrc) == pa  ]

# Design a -3dB attenuator for 50ohm source and 50ohm load
soln = solve( pi_eqns + [
    zload == 50, 
    zsrc == 50, 
    pa == 1/2, # power attenuation is half, ie. 3dB
    # we want the load-impedance seen by the source 
    # to match the source impedance and vice-versa

{a: -100*sqrt(2) + 150,  # 291ohm
 b: -25/2*sqrt(2),       # 17.7ohm
 c: -100*sqrt(2) + 150,  # 291ohm
 pa: 1/2,
 zload: 50,
 zsrc: 50},

So now we know what values our resistors need to be! Interestingly, A and C turn out to be the same value (this happens whenever source and load impedance match) but we did not have to bake in this as a special case .. it just falls out of the equations.

Since we’re using an equation solver, we can use the same setup to find out what the attenuation would be for given resistor values. Here we provide values for a/b/c resistors but omit zload, zsrc and the pa (power attenuation) and the equation solver figures them out.

soln = solve(pi_eqns + [
    a == -100*sqrt(2) + 150,
    b == -25/2*sqrt(2),
    c == -100*sqrt(2) + 150],
  pa: 1/2, # ie. power is halved
  zload: 50,
  zsrc: 50

And if we pick some random values for a/b/c, we can see the consequences for the impedance and the power attenuation:

soln = solve( pi_eqns + [
    a == 10,
    b == 20,
    c == 30],
{a: 10,
  b: 20,
  c: 30,
  pa: -4*sqrt(5) + 9,  # ie. 0.0557280900008408
  zload: 6*sqrt(5),    # ie. 13.4164078649987
  zsrc: 10/3*sqrt(5)}, # ie. 7.45355992499930

So if you ever need an 18x power attenuation for your 13.4ohm source and 7.45ohm load, you’re in luck!