All computer source code presented on this page, unless it includes attribution to another author, is provided by Ed Halley under the Artistic License. Use such code freely and without any expectation of support. I would like to know if you make anything cool with the code, or need questions answered.
# pids - Generic proportional-integral-derivative controllers.


Generic proportional-integral-derivative controllers.


    A proportional-integral-derivative controller (PID controller) is a
    generic control loop feedback mechanism widely used in industrial
    control systems.  A PID controller calculates an "error" value as the
    difference between a measured process variable and a desired
    setpoint. The controller attempts to minimize the error by adjusting
    the process control inputs.

    For instance, a sensor may return a voltage representing a
    thermocouple reading for an oven temperature, and an actuator may
    be sent a signal using completely different voltage scale to describe
    the amount of fuel to burn in the oven.  A PID controller can link
    the two systems as a thermostat:  add more fuel when the oven cools,
    reduce fuel when the oven is overly hot, closing in on a desired


    >>> import pids

    >>> p = pids.Pid( Kproportional, Kintegral, Kderivative )
    >>> p.range( get_my_actuator_minimum(), get_my_actuator_maximum() )

    >>> while True:
    ...   output = p.step( my_clock.get_dt(), get_my_sensor_value() )
    ...   set_my_actuator_value( output )

    The tuning of the PID parameters is complicated, and depends
    on the design of the devices involved.  Experimentation is

    The meaning of the input, output and limit parameters are explained
    through a couple of simple example applications.


    A PID Controller is often selected to control a servomotor.

    Many common servomotors swing an arm through an arc, from a
    minimum position (e.g., -80 degrees) to a maximum (+80 degrees).
    The angle of the arm may be measured by eye in degrees, but
    electrically, this is sensed by an electrical resistance value
    (e.g., 100 Ohms to 1000 Ohms).

    The input signal is a pulse width modulated signal, with pulses
    between a minimum width (e.g., 1ms high out of every 20ms) and a
    maximum width (2ms high out of every 20ms).

    For the PID, this describes the units required for ranges and the
    set point.

    * the input is the measured angle (say, in ohms)
    * the set point is the desired angle (also in ohms)
    * the output is the pwm rate applied to motor (in milliseconds)

    Another purpose for a PID controller is as an oven thermostat.

    A measuring thermocouple is used to detect the current temperature
    in the oven.  It has a reading in electrical resistance (e.g., 10
    K Ohms at 300 degrees Celsius, and +100 Ohms per 10 degrees
    Celsius below that level).  Sensors may have a non-linear
    relationship but the curve is generally sufficiently smooth to
    work here.

    A heating coil is used to produce heat in the oven.  It is rated
    to produce heat with a known level of electrical current (e.g.,
    anywhere from 0 to 3 Amperes).  An actual coil may simply be "on"
    or "off," but by alternating the state, heat can be regulated more
    smoothly.  The output of the controller should decide the level of
    alternation requested (e.g., from 0 or always off, to 1 or always

    * The input is measured temperature (in ohms)
    * The set point is desired temperature (in ohms)
    * The output is the heating coil activation (from zero to one)



class Pid (object):

    '''A discrete PID (Proportional-Integral-Derivative) controller.'''

    def __init__(self, P=1.0, I=1.0, D=1.0, point=0.0, below=-1.0, above=1.0):
        '''Sets up basic operational parameters for the controller.
        Three constants for the "tuning" of the controller can be given.
          * P (proportional gain) scales acceleration to new setpoints
          * I (integral gain) scales correction of error buildup
          * D (derivative gain) scales bounded rate of output change
        The initial desired ouput value or "point" can be given.
        The overall output range ("below" and "above") can be given.
        self.tune(P, I, D)
        self.range(below, above)
        self.output = below
        self.input = self.measure()

    def reset(self):
        self._integral = 0.0
        self._previous = 0.0

    def step(self, dt=1.0, input=None):
        '''Update the controller with a new input, to get new output.
        The time step "dt" can be given, or is assumed as an arbitrary 1.0.
        If a new "input" value a callable object, it is called for a value.
        If a new "input" value is not given here, measure() is called.
        if input is None:
            self.input = self.measure()
        elif callable(input):
            self.input = input()
            self.input = input
        err = self.setpoint - self.input
        self._integral += err * dt
        I = self._integral
        D = (err - self._previous) / dt
        output = self.Kp*err + self.Ki*I + self.Kd*D
        self._previous = err
        self.output = self.bound(output)

    def bound(self, output):
        '''Ensure the output falls within the current output range.
        May be overridden with a new method if overshoot is allowed.
        return max(min(output, self.maxout), self.minout)

    def range(self, below, above):
        '''Set the overall output range.
        Outputs are bounded to remain within this range with the bound()
        overridable method.
        if below > above:
            (above, below) = (below, above)
        self.minout = below
        self.maxout = above

    def tune(self, P, I, D):
        '''Sets the three constant tuning parameters, P, I, and D.'''
        self.Kp = P
        self.Ki = I
        self.Kd = D

    def set(self, point):
        '''Sets the desired output value to which the controller seeks.'''
        self.setpoint = point

    def get(self):
        '''Returns the current output value at any time.'''
        return self.output

    def measure(self):
        '''May be overridden to calculate a new input value.'''
        return 0.0


if __name__ == '__main__':

    import interpolations

    def worm(terms, width=120):
        line = ' '*width
        for term in terms:
            left, x, right, sym = term
            h = int( interpolations.linear(left, right, x, 0, width-2) )
            line = line[:h] + sym + line[h+1:]
        print("|" + line + "|")

    class ServoPid (Pid):
        def __init__(self, **config):
            self.speed = 1
            self.where = 0
            self.maxwhere = 90
            self.minwhere = -90
            super(ServoPid, self).__init__(**config)
        def measure(self):
            self.where += self.output / 3.0
            self.where = max(min(self.where, self.maxwhere), self.minwhere)
            return self.where

    import math
    import random

    pid = ServoPid()
    pid.range(-10.0, 10.0)
    for i in range(70):
        #worm(pid.minout, pid.get(), pid.maxout)
        worm( [ (pid.minwhere, pid.setpoint, pid.maxwhere, '+'),
                (pid.minwhere, pid.where, pid.maxwhere, '*') ] )
        if random.random() < 0.10:
            pid.set(random.random() * 25 - 12)

Contact Ed Halley by email at
Text, code, layout and artwork are Copyright © 1996-2013 Ed Halley.
Copying in whole or in part, with author attribution, is expressly allowed.
Any references to trademarks are illustrative and are controlled by their respective owners.
Make donations with PayPal - it's fast, free and secure!