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:
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 symbolsr1, r2, v_in, a, z_o, i1, i2 = sp.symbols('r1 r2 v_in a z_o i1 i2')# Equationseq1 = 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('-----------------------------')
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).
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.dataifself.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.dataself.res_tol = form.res_tol.dataself.defined =Truedef 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):ifself.calc_mode =='attenuation':# given r1, r2 -> compute attenuationself.solve_attenuation()else:# given attenuation -> compute r1, r2self.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)ifself.use_standard_values: r1, r2 = get_standard_values(r1, r2, self.res_tol)self.outputs['r1'] = r1self.outputs['r2'] = r2self.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.r2ifself.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'] = r1self.outputs['r2'] = r2def 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'] = zindef solve_return_loss(self): zin =self.outputs['zin'] z0 =self.z0self.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.