Solving the T-Pad

Author

erfx5361

Published

August 27, 2025

In this post I wanted to look at some ways to speed up solving the attenuator calculation problem, so I used those strategies on the T-Pad.

Figure 1: T-Pad Schematic

I defaulted to trying to solve this with nodal analysis because that is my preference even though the circuit is obviously conducive to mesh analysis. What I found when trying to solve with nodal analysis is that you end up with \(V_T\) as an unknown without a way to get rid of it. Looking back at solving the Pi-Pad, you can see that the two unknown voltage nodes can be solved simply by defining the circuit assumptions. For example, the input voltage node is 1/2 of the source voltage, and the output voltage node is determined by the attenuation factor. Due to this simplificaiton, we were able to solve for \(R_1\) and \(R_2\) directly because we never had any unknown voltage variables to deal with.

In order to have this same setup for an easy solution, we need to use mesh analysis to solve in terms of currents.

The mesh analysis equations are easy to write by inspection:

\[ \begin{equation} i_1(Z_o + R_2 + R_1) - i_2R_1 = V_s = 2V_{in} \end{equation} \tag{1}\]

\[ \begin{equation} i_2(R_2 + Z_o + R_1) = i_1R_1 \end{equation} \tag{2}\]

and applying the input impedance constraint we know \(i_1\) and \(i_2\):

\[ \begin{equation} i_1 = \frac{V_{in}}{Z_o} \end{equation} \tag{3}\]

\[ \begin{equation} i_2 = \frac{V_{out}}{Z_o} = \frac{aV_{in}}{Z_o} \end{equation} \tag{4}\]

So we end up with: \[ \begin{equation} \frac{V_{in}}{Z_o}\left(Z_o + R_2 + R_1\right) - \frac{aV_{in}R_1}{Z_o} = 2V_in \end{equation} \tag{5}\]

\[ \begin{equation} \frac{aV_{in}}{Z_o}\left(R_2 + Z_o + R_1\right) = \frac{V_{in}}{Z_o}R_1 \end{equation} \tag{6}\]

At this point its looking like it would be quite easy to solve. However, I mentioned I was looking for better ways. I discovered that there is a symbolic equation solver package for Python called sympy. This should have been no surprise given Python’s vast package library, but it was still an exciting discovery. Using sympy I simplified the equations to solve for \(R_1\) and \(R_2\). I used it to solve for \(a\) as well.

Code
import sympy as sp

# Define symbols
r1, r2, v_in, a, z_o, i1, i2 = sp.symbols('r1 r2 v_in a z_o i1 i2')

# Equations
eq1 = sp.Eq(v_in/z_o*(z_o+r2+r1)-a*v_in*r1/z_o, 2*v_in)
eq2 = sp.Eq(a*v_in/z_o*(r2+z_o+r1), v_in/z_o*r1)

# Solve system for r1 and r2 
solutions = sp.solve([eq1, eq2], [r1, r2], dict=True)
atten_sol = sp.solve([eq2],[a], dict=True )

print('-----------------------------')
print('Outputs:')
print(solutions)
print(atten_sol)
print('-----------------------------')
-----------------------------
Outputs:
[{r1: -2*a*z_o/(a**2 - 1), r2: (-a*z_o + z_o)/(a + 1)}]
[{a: r1/(r1 + r2 + z_o)}]
-----------------------------

Using my existing code for the Pi-Pad calculator and these equations, I asked an LLM to create a T_Atten class for me, mirroring the approach for my Pi_Atten class (which I did by hand).

Here’s the code I gave it (the original Pi_Atten code for comparison).

Code
class Attenuator():

    def __init__(self, calc_mode='resistors', attenuation=float(), impedance=float(), 
                 use_standard_values=True, res_tol='1%', r1=float(), r2=float()):
        self.defined = False
        
        self.calc_mode = calc_mode

        self.z0 = float(impedance)
        self.use_standard_values = use_standard_values
        self.res_tol  = res_tol

        # if attenuation value given
        if attenuation:
            self.attenuation = float(attenuation)
        # if resistor values given
        if r1:
            self.r1 = float(r1)
            self.r2 = float(r2)

        self.outputs = {
            'r1': float(),
            'r2': float(),
            'attenuation': float(),
            'zin': float(),
            'return_loss': float()
        }

        self.outputs['power'] = {
            'r2_in': float(),
            'r1': float(),
            'r2_out': float()
        }

class Pi_Atten(Attenuator):

    def __init__(self, calc_mode='resistors', attenuation=float(), impedance=float(), 
                 use_standard_values=True, res_tol='1%', r1=float(), r2=float()):
        super().__init__(calc_mode, attenuation, impedance, 
                         use_standard_values, res_tol, r1, r2)

    
    def define_from_form(self, form):
        self.calc_mode = form.calc_mode.data
        if self.calc_mode == 'resistors':
            self.attenuation = float(form.attenuation.data)
        else:
            self.r1 = float(form.r1_input.data)
            self.r2 = float(form.r2_input.data)
        self.z0 = float(form.impedance.data)
        self.use_standard_values = form.use_standard_values.data
        self.res_tol = form.res_tol.data
        self.defined = True


    def define_output_from_form(self, form):
        # load pi-pad output values into pdiss_form
        self.outputs['r1'] = float(form.get('r1'))
        self.outputs['r2'] = float(form.get('r2'))
        self.outputs['attenuation'] = float(form.get('attenuation_output'))
        self.outputs['return_loss'] = float(form.get('return_loss'))
        self.z0 = float(form.get('impedance'))


    def pi_pad(self):
        if self.calc_mode == 'attenuation':
            self.solve_attenuation()
        else:
            self.solve_resistors()
        
        self.solve_zin()
        self.solve_return_loss()

        
    def get_pi_pad(self):
        self.pi_pad()
        return self.outputs['r1'], self.outputs['r2'], self.outputs['attenuation'], self.outputs['return_loss']


    def solve_attenuation(self):
        z0 = self.z0
        r1 = self.r1
        r2 = self.r2

        if self.use_standard_values:
            r1, r2 = get_standard_values(r1, r2, self.res_tol)
    
        # solve systems of equations 
        v1_term_1 = 1 + z0/r2 + z0/r1
        vout_term_1 = -z0/r1

        v1_term_2 = -1/r1
        vout_term_2 = 1/r1 + 1/r2 + 1/z0

        vin = 1
        voltage_coefficients = [[v1_term_1, vout_term_1], [v1_term_2, vout_term_2]]

        A = np.array(voltage_coefficients)
        B = np.array([vin, 0])

        X = np.linalg.solve(A, B)
        attenuation = round(-20*np.log10(2*X[1]/vin),2)

        self.outputs['attenuation'] = attenuation
        self.outputs['r1'] = r1
        self.outputs['r2'] = r2


    def solve_resistors(self):
        
        z0 = self.z0

        # convert attenuation in dB to linear factor a
        a = 10**(-self.attenuation/20)

        # delay rounding r1 to preserve floating point math
        r1 = z0*(1-a**2)/(2*a)

        r2 = round((a*r1) / (1 - a - (a*r1/z0)),2)
        r1 = round(r1,2)

        if self.use_standard_values:
            r1, r2 = get_standard_values(r1, r2, self.res_tol)

        self.outputs['r1'] = r1
        self.outputs['r2'] = r2
        self.outputs['attenuation'] = round(self.attenuation,2)


    def solve_zin(self):
        r1, r2 = self.outputs['r1'], self.outputs['r2']
        z0 = self.z0

        self.outputs['zin'] = parallel(parallel(z0, r2) + r1, r2)


    def solve_return_loss(self):
        zin = self.outputs['zin']
        z0 = self.z0

        self.outputs['return_loss'] = round(-20 * np.log10(abs(zin - z0) / abs(zin + z0)),2)


    def get_dissipation(self, p_in, units):
        p_in = float(p_in)
        # ensure p_in is in W
        if units == 'dBm':
            p_in = 10**(p_in/10) / 1000
        elif units == 'mW':
            p_in = p_in / 1000
        
        v_in_rms = np.sqrt(p_in*self.z0)
        a = 10**(-self.outputs['attenuation']/20)
        v_out_rms = v_in_rms*a

        i_r2_in = v_in_rms / self.outputs['r2']
        i_r1 = (v_in_rms - v_out_rms) / self.outputs['r1']
        i_r2_out = v_out_rms / self.outputs['r2']

        r2_in = round(i_r2_in**2 * self.outputs['r2'] * 1000, 2)
        r1 = round(i_r1**2 * self.outputs['r1'] * 1000, 2)
        r2_out = round(i_r2_out**2 * self.outputs['r2'] * 1000, 2)

        p_out = v_out_rms**2 / self.z0 * 1000

        return r2_in, r1, r2_out

Here’s the code it produced:

Code
class T_Atten(Attenuator):

    def __init__(self, calc_mode='resistors', attenuation=float(), impedance=float(),
                 use_standard_values=True, res_tol='1%', r1=float(), r2=float()):
        super().__init__(calc_mode, attenuation, impedance,
                         use_standard_values, res_tol, r1, r2)

    # ----- form helpers to keep parity with Pi_Atten -----
    def define_from_form(self, form):
        self.calc_mode = form.calc_mode.data
        if self.calc_mode == 'resistors':
            # user will provide attenuation (to compute r1, r2)
            self.attenuation = float(form.attenuation.data)
        else:
            # user provides resistors (to compute attenuation)
            self.r1 = float(form.r1_input.data)
            self.r2 = float(form.r2_input.data)
        self.z0 = float(form.impedance.data)
        self.use_standard_values = form.use_standard_values.data
        self.res_tol = form.res_tol.data
        self.defined = True

    def define_output_from_form(self, form):
        self.outputs['r1'] = float(form.get('r1'))
        self.outputs['r2'] = float(form.get('r2'))
        self.outputs['attenuation'] = float(form.get('attenuation_output'))
        self.outputs['return_loss'] = float(form.get('return_loss'))
        self.z0 = float(form.get('impedance'))

    # ----- public runners -----
    def t_pad(self):
        if self.calc_mode == 'attenuation':
            # given r1, r2 -> compute attenuation
            self.solve_attenuation()
        else:
            # given attenuation -> compute r1, r2
            self.solve_resistors()

        self.solve_zin()
        self.solve_return_loss()

    def get_t_pad(self):
        self.t_pad()
        return (self.outputs['r1'],
                self.outputs['r2'],
                self.outputs['attenuation'],
                self.outputs['return_loss'])

    # ----- core solvers -----
    def solve_resistors(self):
        """
        Given desired attenuation (dB) and z0, compute r1 and r2 using:
            a = 10**(-atten/20)
            r1 = -2*a*z0 / (a**2 - 1)
            r2 = (-a*z0 + z0) / (a + 1)
        """
        z0 = self.z0
        a = 10**(-self.attenuation/20)

        # keep float precision until final rounding
        r1 = (-2.0 * a * z0) / (a**2 - 1.0)
        r2 = (z0 * (1.0 - a)) / (a + 1.0)

        # round to 2 decimals like Pi_Atten, then optionally snap to E-series
        r1 = round(r1, 2)
        r2 = round(r2, 2)

        if self.use_standard_values:
            r1, r2 = get_standard_values(r1, r2, self.res_tol)

        self.outputs['r1'] = r1
        self.outputs['r2'] = r2
        self.outputs['attenuation'] = round(self.attenuation, 2)

    def solve_attenuation(self):
        """
        Given r1, r2, z0, compute attenuation using:
            a = r1 / (r1 + r2 + z0)
            attenuation_dB = -20*log10(a)
        """
        z0 = self.z0
        r1 = self.r1
        r2 = self.r2

        if self.use_standard_values:
            r1, r2 = get_standard_values(r1, r2, self.res_tol)

        # linear voltage ratio
        a = r1 / (r1 + r2 + z0)

        attenuation = -20.0 * np.log10(a)

        self.outputs['attenuation'] = round(attenuation, 2)
        self.outputs['r1'] = r1
        self.outputs['r2'] = r2

    def solve_zin(self):
        """
        Use:
            z_in = r2 + (r1*(r2 + z0)) / (r1 + r2 + z0)
        """
        r1, r2 = self.outputs['r1'], self.outputs['r2']
        z0 = self.z0
        zin = r2 + (r1 * (r2 + z0)) / (r1 + r2 + z0)
        self.outputs['zin'] = zin

    def solve_return_loss(self):
        zin = self.outputs['zin']
        z0 = self.z0
        self.outputs['return_loss'] = round(-20 * np.log10(abs(zin - z0) / abs(zin + z0)), 2)

    # Intentionally omitted power dissipation methods per user instruction
    # def get_dissipation(...): pass

I don’t like that it made the constants floats such as 1.0 to indicate floating point (attenuation and a are already floats), but everything else seems correct. I have not implemented a Tee Attenuator Calculator yet, but this will be a good start.

Using sympy and LLM code generation definitely saved me some time. The benefits of sympy for this problem were probably quite small, but I could see it being quite helpful for more complex problems. Moving forward I would like to look into Lcapy or other alternatives to explore effective techniques to acquire rapid symbolic circuit solutions.

My hand calculations are included as well for reference.

Figure 2: Mesh Analysis
Figure 3: Nodal Analysis