Multisource duty cycle calculator

Discussion to talk about software related topics only.
Post Reply
User avatar
dciliske
Posts: 624
Joined: Mon Feb 06, 2012 9:37 am
Location: San Diego, CA
Contact:

Multisource duty cycle calculator

Post by dciliske »

Not sure if this is something useful or not, but I made a Python calculator script for utilizing multiple PWM sources to drive at higher frequencies, while still maintaining good granular control. (I'm designing a dimmable LED controller for some hall lighting based off of the L2E)

It's fairly simple with a couple main functions (plotting requires the matplotlib library*):

Code: Select all

CalcDutyCycles(x_max, y_max):
    Calculates the overall duty cycle for each turn-on count for each source.
    Inputs:
        x_max - the period of the primary input source
        y_max - the period of the secondary input source
    Returns:
        A list containing elements of the form:
            (x_turnon, y_turnon, dutycycle, deadtime)
            x_turnon - threshhold cycle count when the primary input is asserted
            y_turnon - threshhold cycle count when the secondary input is asserted
            dutycycle - aggregate duty cycle of the two inputs
            deadtime - percent time of total period spent off in period leading block

GetSortedDutyCycles(x_max, y_max):
    Returns a list from CalcDutyCycles(), sorted ascending by dutycycle

GetDeadtimeDutyCycles(x_max, y_max, deadtimeThreshhold = 1.1):
    returns a list from CalcDutyCycles(), sorted ascending by dutycycle,
    below a maximum deadtime threshhold

PlotDutyCycles(x_max, y_max):
    Creates a plot of the aggregate duty cycles and the deadtime for the
    given input periods.
*On Windows, matplotlib is a bit involved to install. You will need to install:
  1. Matplotlib
  2. dateutil
  3. pyparser
  4. numpy
-Dan

Code: Select all

import collections
import functools
from functools import reduce

class memoized(object):
    '''Decorator. Caches a function's return value each time it is called.
    If called later with the same arguments, the cached value is returned
    (not reevaluated).
    '''
    def __init__(self, func):
        self.func = func
        self.cache = {}
    def __call__(self, *args):
        if not isinstance(args, collections.Hashable):
            # uncacheable. a list, for instance.
            # better to not cache than blow up.
            return self.func(*args)
        if args in self.cache:
            return self.cache[args]
        else:
            value = self.func(*args)
            self.cache[args] = value
            return value
    def __repr__(self):
        '''Return the function's docstring.'''
        return self.func.__doc__
    def __get__(self, obj, objtype):
        '''Support instance methods.'''
        return functools.partial(self.__call__, obj)


# Greatest common divisor of more than 2 numbers.  Am I terrible for doing it this way?

def gcd(*numbers):
    """Return the greatest common divisor of the given integers"""
    from fractions import gcd
    return reduce(gcd, numbers)

# Least common multiple is not in standard libraries? It's in gmpy, but this is simple enough:

def lcm(*numbers):
    """Return lowest common multiple."""
    def lcm(a, b):
        return (a * b) // gcd(a, b)
    return reduce(lcm, numbers, 1)

def CalcDutyCycles(x_max, y_max):
"""
CalcDutyCycles(x_max, y_max):
    Calculates the overall duty cycle for each turn-on count for each source.
    Inputs:
        x_max - the period of the primary input source
        y_max - the period of the secondary input source
    Returns:
        A list containing elements of the form:
            (x_turnon, y_turnon, dutycycle, deadtime)
            x_turnon - threshhold cycle count when the primary input is asserted
            y_turnon - threshhold cycle count when the secondary input is asserted
            dutycycle - aggregate duty cycle of the two inputs
            deadtime - percent time of total period spent off in period leading block
"""
    dutyCycles = []
    period = lcm(x_max, y_max)
    for x_on in range(1, x_max):
        for y_on in range(1, y_max):
            onCount = 0
            deadtime = 0
            for tick in range(period):
                if ( (tick % x_max) == 0 ):
                    started = False
                if ( ((tick % x_max) >= x_on) and ((tick % y_max) >= y_on) ):
                    onCount += 1
                    started = True
                if (not started):
                    deadtime += 1
            dutyCycles.append((x_on, y_on, float(onCount)/period, float(deadtime)/period))
    return dutyCycles

def CalcDutyCycles_surface(x_max, y_max):
    xList, yList, zList, deadList = ([],[],[],[])
    period = lcm(x_max, y_max)
    for y_on in range(1, y_max):
        xList_Curr = []
        yList_Curr = []
        zList_Curr = []
        deadList_Curr = []
        for x_on in range(1, x_max):
            onCount = 0
            deadtime = 0
            started = False
            for tick in range(period):
                if ( (tick % x_max) == 0 ):
                    started = False
                if ( ((tick % x_max) >= x_on) and ((tick % y_max) >= y_on) ):
                    onCount += 1
                    started = True
                if (not started):
                    deadtime += 1
            xList_Curr.append(x_on)
            yList_Curr.append(y_on)
            zList_Curr.append(float(onCount)/period)
            deadList_Curr.append(float(deadtime)/period)
        xList.append(xList_Curr)
        yList.append(yList_Curr)
        zList.append(zList_Curr)
        deadList.append(deadList_Curr)
    return xList, yList, zList, deadList

@memoized
def GetSortedDutyCycles(x_max, y_max):
"""
GetSortedDutyCycles(x_max, y_max):
    Returns a list from CalcDutyCycles(), sorted ascending by dutycycle
"""
    dutyCycles = CalcDutyCycles(x_max, y_max)
    return sorted(CalcDutyCycles(x_max, y_max), key=lambda tup: tup[2])

def GetDeadtimeDutyCycles(x_max, y_max, deadtimeThreshhold = 1.1):
"""
GetDeadtimeDutyCycles(x_max, y_max, deadtimeThreshhold = 1.1):
    returns a list from CalcDutyCycles(), sorted ascending by dutycycle,
    below a maximum deadtime threshhold
"""
    return [x for x in GetSortedDutyCycles(x_max, y_max) if x[3] <= deadtimeThreshhold]

try:
    from mpl_toolkits.mplot3d import Axes3D
    from matplotlib import cm
    from matplotlib.ticker import LinearLocator, FormatStrFormatter
    import matplotlib.pyplot as plt
    import numpy as np
    def PlotDutyCycles(x_max, y_max):
    """
    PlotDutyCycles(x_max, y_max):
        Creates a plot of the aggregate duty cycles and the deadtime for the
        given input periods.
    """
        X,Y,Z,D = CalcDutyCycles_surface(x_max, y_max)
        fig = plt.figure()
        ax1 = fig.add_subplot(111,projection='3d')
        ax2 = fig.add_subplot(222,projection='3d')
        surf1 = ax1.plot_surface(X,Y,Z, rstride=1, cstride=10, cmap=cm.gist_rainbow, linewidth=0)
        ax1.set_xlabel("X Cycles")
        ax1.set_ylabel("Y Cycles")
        ax1.set_zlabel("Duty Cycle")

        surf2 = ax2.plot_surface(X,Y,D, rstride=1, cstride=10, cmap=cm.coolwarm, linewidth=0)
        ax2.set_xlabel("X Cycles")
        ax2.set_ylabel("Y Cycels")
        ax2.set_zlabel("Dead Time")
        plt.show()
except:
    print('\n\nMatplotlib not available:\n    Graphing functions unavailable\n\n')
Attachments
PWM_DutyCycles_Forum.png
PWM_DutyCycles_Forum.png (187.61 KiB) Viewed 4268 times
PWM_DutyCalc.py
(5.59 KiB) Downloaded 405 times
Dan Ciliske
Project Engineer
Netburner, Inc
Post Reply