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:
        print("Parallel resistor not found.")
        return(r1)

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

parser = argparse.ArgumentParser()
parser.add_argument('resistance', type=float,
help='resistance in ohms e.g. 1000000 not 1M')
parser.add_argument('tolerance', type=float,
help='tolerance in  e.g. 5%% not 0.05')
parser.add_argument('--record', action="store_true",
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)