I’m brand new to programming and am working on a program to solve for values in series DC circuits, and possibly parallel DC circuits as well. Originally, I was hoping to not import anything for the main functionality; however, I’ve run into an issue. The problem is that variables (resistance for specific resistors, total resistance, source voltage etc.) for which a value is given change with each circuit. As a human, the solution is simple algebraic manipulation, but I’m not sure how to express this in Python. I could do the manipulation myself and write an equation for every possible scenario, but that seems overly complicated and messy. Is the answer just to use something like SymPy, or what am I missing?
You don’t need symbolic algebra at first, just encode the physics rules and let the program apply them.
Store known values (voltage, current, resistance) and use simple formulas like Ohm’s law to compute the missing one when two are known.
Libraries like Pint help with units, and SymPy is only needed later if you want the computer to do algebra automatically.
Okay. If I’m understanding you correctly, that’s what I’ve done so far. I’m running into issues when the problems go beyond the simple stuff—solving single resistors and adding their values to get totals. I’m currently using a for loop to iterate through my resistors, solve for missing values, and apply addition and basic rules to find totals, but some problems are trickier. Here’s an example from my terminal showing what my code is and isn’t capable of doing currently:
”maybe/ $ python maybe.py
What type of circuit? series
How many resistors? 2
What values are known for resistor 1?
resistance What’s resistance for resistor 1? 3
What values are known for resistor 2? resistance
What’s resistance for resistor 2? 4
What values are known for the total circuit ? current voltage
What’s current for the total circuit ? 4
What’s voltage for the total circuit ? 28
Power is 48 watts for resistor 1. Resistance is 3.0 ohms for resistor 1. Current is 4.0 amps for resistor 1. Voltage is 12.0 volts for resistor 1. Power is 64 watts for resistor 2. Resistance is 4.0 ohms for resistor 2. Current is 4.0 amps for resistor 2. Voltage is 16.0 volts for resistor 2. Total power is 112.0 watts. Total resistance is 7.0 ohms. Total current is 4.0 amps. Total voltage is 28.0 volts.”
This is a problem it was unable to solve:
“maybe/ $ python maybe.py
What type of circuit? series
How many resistors? 3
What values are known for resistor 1? resistance
What’s resistance for resistor 1? 1
What values are known for resistor 2? resistance
What’s resistance for resistor 2? 2
What values are known for resistor 3? resistance
What’s resistance for resistor 3? 3
What values are known for the total circuit ? voltage
What’s voltage for the total circuit ? 24
NOT ENOUGH INFORMATION FOR THIS PROGRAM!!”
So I guess my question is really when do I stop ‘hard-coding’ this stuff and start using a more versatile method and what would that method be?
The general way of doing this is to look at each junction of unfixed voltage and write an equation for each one that enforces zero net current going into that junction.
You’d then write that out as a system of linear equations which you can solve generally with matrices (most likely using numpy). It’ll be fiddly turning arbitrary systems of voltages and resistances into matrices but no more so than turning them into algebraic equations.
If it is algebraic manipulation that you need then SymPy is the obvious choice. It is only really useful to do this with algebraic manipulation though if there are some unknowns that need to be determined from an implicit equation. Otherwise it is better to work directly towards the solution using numerical calculations.
Here is your first example using SymPy although I don’t know exactly what it is you wanted to do because the example has redundant inputs (I and V are not both needed):
In [10]: from sympy import *
In [11]: R1, R2 = symbols('R1, R2')
In [12]: V, I = symbols('V, I')
In [13]: eq = Eq(V, I*(R1 + R2))
In [15]: eq
Out[15]: V = I⋅(R₁ + R₂)
In [16]: eq.subs({R1: 3, R2: 4, I: 4})
Out[16]: V = 28
In [17]: solve(_, V)
Out[17]: [28]
Note that we didn’t need V as an input. I don’t know what your code does but it sounds like what you want to do is substitute some values into the equation eq and then at some point have code that solves for the remaining values and then computes all the answers?
Here is your second example:
In [18]: R1, R2, R3 = symbols('R1, R2, R3')
In [19]: V, I = symbols('V, I')
In [20]: eq = Eq(V, I*(R1 + R2 + R3))
In [21]: eq.subs({R1: 1, R2: 2, R3: 3, V: 24})
Out[21]: 24 = 6⋅I
In [22]: solve(_, I)
Out[22]: [4]
So having solved for I we now know everything and can just apply the formulae for each component. Is solving for I the part that your program cannot do?
Using SymPy here potentially makes sense if you don’t know ahead of time which combinations of symbols are going to have their values specified as inputs and you just want to substitute all but one and then solve to get numeric solutions for everything else.
Otherwise if you choose say that V and the resistances are always going to be in the input and your goal is always to determine I implicitly and then compute everything else then it makes more sense to formulate this in terms of the general equation as Brénainn says.
I would look up Sympy (and Pint), and also read a bit on SPICE and netlist.
It’s possible to implement circuits in quite simple and elegant ways, e.g.
Rx = Param("R4")
circuit = Series(
Parallel(
Resistor("R1", 10 * OHM),
Series(
Resistor("R2", 5 * OHM),
Resistor("R3", 5 * OHM),
),
),
Resistor("R4", Rx * OHM),
)
Vsrc = 28 * VOLT
Isrc = 4 * AMPERE
sol = solve_params(circuit, ...)
for sym, val in sol.items():
print(f"{sym} = {Q_(val, OHM)}")
Could produce:
R4 = 2 ohm
where
@dataclass(frozen=True)
class Resistor:
name: str
resistance: object # pint.Quantity (units: ohm), or None for unknown
etc.
Ho, ho, ho, … Merry Christmas!
![]()
Being that you are new to programming, creating such a generalized request with limited tools may not be adequate. More advanced tools may be necessary to tackle this kind of problem. On the upside, there is no need to import external libraries. The following explanation will demonstrate how to potentially approach this kind of problem for future reference.
There are many ways to approach this problem as already highighted by others, including using the LTSpice circuit simulationi tool which greatly simplifies the problem of obtaining the equivalent resistance of a multi-node series / parallel resistance circuit. For example, it you create the following circuit, the voltage is known (since you define it at creation time). The only thing that you need to solve for is the current. You can do this by measuring the current being supplied by the voltage source. Using Ohm’s Law, V / I, you obtain the equivalent resistance. For example, in the following circuit simulation, we have set the voltage source to 10V. We put a probe on the DC source and measure it by moving the curser over the red signal on the plot (the current signal). It reads 225.5 mA. By way of Ohms Law, the equivalent resistance is: 10V / 0.2255 = 44.4 Ohms.
Alternatively, if you want to calculate the circuit equivalent resistance via Python, you can create two classes. One for series resistance and one for parallel resistance.
You begin by defining each branch by instantiating the SeriesBranch class. You do so by passing a dictionary that includes reference designators and their corresponding values. At the time of instantiation, it auto-calculates the series resistance by way of this method call:
self.branch_resistance = self.total_branch_re() # calculate branch series resistance
The next step is to calculate the parallel resistances by making use of the ParallelBranch class. To instantiate the class, you pass in a list having the series branch resistances that are in parallel. Again, the equivalent resistance will be calculated at instantiation. This time, by way of this method call:
self.parallel_resistance = self.parallel_branch_re() # calculate parallel resistance
After having calculated the fundamental series and parallel resistances, proceed by condensing them. Note that you can now treat the equivalent resistances as a single resistor. You may add them in series or via a parallel calculation. The beauty of this is that you can reuse the same classes used for the fundamental branch calculations.
Here is the test script:
# Create two classes: One for series branches and one for calculating equivalent
# parallel resistance based on the branches that are in parallel
class SeriesBranch:
def __init__(self, res: dict[str, float]) -> None:
self.res = res
self.branch_resistance = self.total_branch_re()
def total_branch_re(self) -> float:
# sum total of branch series resistance
return sum(self.res.values())
class ParallelBranch:
def __init__(self, par_res: list[float]) -> None:
self.par = par_res
self.parallel_resistance = self.parallel_branch_re()
def parallel_branch_re(self) -> float:
# Obtain reciprocal of each resistance
reciprocal_res = [1 / res for res in self.par]
# Return equivalent parallel resistance
return 1 / sum( reciprocal_res )
# Define individual series resistance branches
s_branch_1 = SeriesBranch({'r8': 120, 'r9': 200})
s_branch_2 = SeriesBranch({'r6': 70, 'r7': 65})
s_branch_3 = SeriesBranch({'r4': 85, 'r5': 40})
s_branch_4 = SeriesBranch({'r2': 50, 'r3': 75})
s_branch_5 = SeriesBranch({'r1': 100})
# Obtain equivalent resistance for branches in parallel
p_branch_1 = ParallelBranch([s_branch_1.branch_resistance,
s_branch_2.branch_resistance])
p_branch_2 = ParallelBranch([s_branch_4.branch_resistance,
s_branch_5.branch_resistance])
# ----- Begin condensing circuit -----
# p_branch_1 and s_branch_3 are in series - add them in series
s_branch_6 = SeriesBranch({'p_branch_1': p_branch_1.parallel_resistance,
's_branch_3': s_branch_3.branch_resistance})
# Total equivalent resistance: s_branch_6 is in parallel with p_branch_2
p_branch_3 = ParallelBranch([s_branch_6.branch_resistance,
p_branch_2.parallel_resistance])
print(f'\nThe equivalent parallel resistance is: {round(p_branch_3.parallel_resistance, 1)} Ohms.')
After running the script, you’ll note that the equivalent resistance matches the value obtained from the LTSpice simulation tool. Because the resistor values of the fundametal branches were passed via dictionaries, you can easily change resistor values and recalculate new equivalent total resistances rather easily.
Hope this helps.
Merry Christmas!
Thank you all for your insight, and if anyone else has anything to add I would greatly appreciate it!
As Oscar put it my goal is to create a program where, “you don’t know ahead of time which combinations of symbols are going to have their values specified as inputs and you just want to substitute all but one and then solve to get numeric solutions for everything else.”
It looks like I have some reading and thinking ahead of me, but also a great learning experience to look forward to.
Thanks again!
