# r_finder.py source code

### Hopefully the indentation was preserved during the cut/paste and nothing has been inappropriately wrapped. Your milage may vary!

```#
# Quick and dirty utility to find either two or three serial resistors or two parallel
# and one additional serial resistor that comes within a stated tolerance of a given
# value.
#

import argparse
import os.path as Path

#
# All of the resistor values found in the Jameco Electronics resistor assortment
# part number 10720.
#

r_inventory = [10.0, 12.0, 15.0, 18.0, 22.0, 27.0, 33.0, 39.0, 47.0, 56.0, 68.0,
82.0, 100.0, 120.0, 150.0, 180.0, 220.0, 270.0, 330.0, 390.0, 470.0,
560.0, 680.0, 820.0, 1000.0, 1200.0, 1500.0, 1800.0, 2200.0, 2700.0,
3300.0, 3900.0, 4700.0, 5600.0, 6800.0, 8200.0, 10000.0, 12000.0,
15000.0, 18000.0, 22000.0, 27000.0, 33000.0, 39000.0, 47000.0,
56000.0, 68000.0, 82000.0, 100000.0, 120000.0, 150000.0, 180000.0,
220000.0, 270000.0, 330000.0, 390000.0, 470000.0, 560000.0, 680000.0,
820000.0, 1000000.0, 1200000.0, 1500000.0, 1800000.0, 2200000.0,
2700000.0, 3300000.0, 3900000.0, 4700000.0, 5100000.0]

percentage_off     = lambda x, y: abs(x-y)/x
parallel_resistors = lambda r_one, r_two: (r_one*r_two)/(r_one+r_two)

#
# Given a target resistor value search the inventory for an initial resistor
# whose value is:
#
# 1) for serial resistors one resistor value less than the target
# 2) for parallel resistors one resistor value greater than the target.
#
# One less for serial because subsequent resistors will add to that
# value to approach the target value. One more for parallel because
# the second resistor in parallel will significantly decrease the
# overall value.
#
# Return -1 if no resistor value is found.
#
def pick_resistor(r_target, r_inventory, serial_or_parallel):
for i in range(len(r_inventory)):
if (r_inventory[i] > r_target):
if (serial_or_parallel == "serial"):
return(i-1)
else:
return(i)
return(-1)

#
# Find resistors to make a series circuit whose value is as close as
# possible to r_target.
#
def series(r_target, tolerance, r_inventory, verbose=False):
r_cumulative = 0.0
r_working = r_target
solution_found = False

#
# Limit the number of resistors to 3. Any more than that use a trimmer.
#
for i in range(3):
#
# Find a resistor in the appropriate range. If one isn't
# found issue a message and bail.
#
r_working_index = pick_resistor(r_working, r_inventory, "serial")

if (r_working_index >= 0):
r_working = r_inventory[r_working_index]
else:
print("No resistor found less than %d ohms." % (r_working))
break

#
# Print the value of the found resistor, accumulate its value,
# and calculate the percentage error.
#
print("Resistor %d: %d ohms" % (i+1, r_working))
r_cumulative = r_cumulative + r_working
percent = percentage_off(r_target, r_cumulative)

if verbose:
print("Tolerance: %.2f%%" % (percent*100.0))

#
# If we happened to nail it exactly return the resistor
# value.
#
if (r_working == r_target):
print("Exact match found")
return(r_target)

#
# If the percentage error is not within our stated tolerance
# set the solution found flag, calculate the next target
# resistor value, and go again to see if a better fit is
# found.
#
if (percent <= tolerance):
solution_found = True
r_working = r_target - r_cumulative

#
# If no solution was found issue a message, otherwise return the
# accumulated resistance.
#
if (not solution_found):
print("No solution found with tolerance %.2f%%" % (tolerance*100.0))            return(r_cumulative)

#
# Loop looking for best parallel combination. Start the
# search at the NEXT resistor above the first one found.
# Search and calculate the parallel resistance until the
# target is exceeded then back off one value.
#
def find_parallel(r1_index, r1, r_inventory):
for i in range(r1_index+1, len(r_inventory)):
r2 = r_inventory[i]
r12_parallel = parallel_resistors(r1, r2)
if (r12_parallel > r_target):
return(i-1)

#
# No parallel resistor was found.
#
return(-1)

#
# Find resistors to make a two parallel resistor with one series resistor
# circuit whose value is as close as possible to r_target.
#
def parallel_series(r_target, tolerance, r_inventory, verbose=False):
#
# Pick an initial resistor that's one value larger than the
# target value.
#
r1_index = pick_resistor(r_target, r_inventory, "parallel")
r1 = r_inventory[r1_index]
print("   First resistor: %d ohms" % (r1))

r2_index = find_parallel(r1_index, r1, r_inventory)
if(r2_index >= 0):
#
# Calculate the value of the parallel resistors and
# note their values. If in verbose mode report the
# percentage error for inspection purposes.
#
r2 = r_inventory[r2_index]
r12_parallel = parallel_resistors(r1, r2)
print("Parallel resistor: %d ohms" % (r2))
print("    Network value: %.2f ohms" % (r12_parallel))

if verbose:
percent = percentage_off(r_target, r12_parallel)
print("Tolerance: %.2f%%" % (percent*100.0))

#
# Starting at the first resistor value start searching
# backwards through the list looking for the largest
# resistor that when added to the parallel resistors is
# less than the target.
#

indexes=list(range(r1_index))
indexes.reverse()
for i in indexes:
r3 = r_inventory[i]
r123 = r12_parallel + r3
percent = percentage_off(r_target, r123)

#
# Some people may want to see the search details.
#
if verbose:
print("r3=%d   percent=%.2f%%" % (r3, percent*100))

#
# If the percentage error is less than the tolerance,
# report the value found and return the total resistance.
op looking for b
#
if (percent <= tolerance):
print("  Serial resistor: %d ohms" % (r3))

if verbose:
print("Tolerance: %.2f%%" % (percent*100.0))

return(r123)

#
# If no serial resistor value was found return the value
# of the parallel network as a best fit.
#
print("No serial resistor found.")
return(r12_parallel)
else:
return(r1)

if __name__ == "__main__":
#
# Set up an argument parser and get the target resistance value
# and percentage tolerance.
#

parser = argparse.ArgumentParser()
help='resistance in ohms e.g. 1000000 not 1M')
help='tolerance in  e.g. 5%% not 0.05')
help='If provided write results to r_finder.txt')

args = parser.parse_args()
r_target = args.resistance
tolerance = args.tolerance/100.0

#
# Print the target and the range of resistances based on the tolerance.
#
r_variance = r_target * tolerance
print("\n\nTarget resistance: %d ohms" % (r_target))
print("Acceptable range: %f to %f ohms" % (r_target-r_variance, r_target+r_variance))

#
# Find the serial and parallel/serial solutions. The returned values are the
# total resistances found by each function.
#

print("\n\n                    Series Solution\n")
series_solution = series(r_target, tolerance, r_inventory, verbose=False)
print("\n\n               Parallel/Series Solution\n")
parallel_solution = parallel_series(r_target, tolerance, r_inventory, verbose=False)

#
# Print the resistances found and the percentage they vary from the target.
#
print("\n")

series_percent   = percentage_off(r_target, series_solution)*100.0
parallel_percent = percentage_off(r_target, parallel_solution)*100.0

print("  Series Solution: %.2f ohms at %.2f%%" % (series_solution, series_percent))
print("Parallel Solution: %.2f ohms at %.2f%%" % (parallel_solution, parallel_percent))

print("\n\n")

#
# If the record option was provided write the values out to a data file
# for further processing. This is mostly used for testing and verification.
# If the data file does not exist it is created and the column headers
# are written as the first line. Otherwise, the data is just appended
# to the file.
#

if args.record:
output_file = "./r_finder.dat"
if not Path.exists(output_file):
outfile = open(output_file, 'w')
outfile.write("resistance series_resistance series_tolerance parallel_resistance parallel_tolerance\n")
else:
outfile = open(output_file, 'a')

outfile.write("%f %f %f %f %f\n" % (r_target, series_solution, series_percent,     parallel_solution, parallel_percent))
outfile.close()

exit(0)```