Fork PyRPL on GitHub

Source code for pyrpl.hardware_modules.pid

"""
We have already seen some use of the pid module above. There are three
PID modules available: pid0 to pid2.

.. code:: python

    print r.pid0.help()

Proportional and integral gain
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code:: python

    #make shortcut
    pid = r.pid0

    #turn off by setting gains to zero
    pid.p,pid.i = 0,0
    print("P/I gain when turned off:", pid.i,pid.p)

.. code:: python

    # small nonzero numbers set gain to minimum value - avoids rounding off to zero gain
    pid.p = 1e-100
    pid.i = 1e-100
    print("Minimum proportional gain: ", pid.p)
    print("Minimum integral unity-gain frequency [Hz]: ", pid.i)

.. code:: python

    # saturation at maximum values
    pid.p = 1e100
    pid.i = 1e100
    print("Maximum proportional gain: ", pid.p)
    print("Maximum integral unity-gain frequency [Hz]: ", pid.i)

Control with the integral value register
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code:: python

    import numpy as np
    #make shortcut
    pid = r.pid0

    # set input to asg1
    pid.input = "asg1"

    # set asg to constant 0.1 Volts
    r.asg1.setup(waveform="dc", offset = 0.1)

    # set scope ch1 to pid0
    r.scope.input1 = 'pid0'

    #turn off the gains for now
    pid.p,pid.i = 0, 0

    #set integral value to zero
    pid.ival = 0

    #prepare data recording
    from time import time
    times, ivals, outputs = [], [], []

    # turn on integrator to whatever negative gain
    pid.i = -10

    # set integral value above the maximum positive voltage
    pid.ival = 1.5

    #take 1000 points - jitter of the ethernet delay will add a noise here but we dont care
    for n in range(1000):
        times.append(time())
        ivals.append(pid.ival)
        outputs.append(r.scope.voltage_in1)

    #plot
    import matplotlib.pyplot as plt
    %matplotlib inline
    times = np.array(times)-min(times)
    plt.plot(times,ivals,times,outputs)
    plt.xlabel("Time [s]")
    plt.ylabel("Voltage")

Again, what do we see? We set up the pid module with a constant
(positive) input from the ASG. We then turned on the integrator (with
negative gain), which will inevitably lead to a slow drift of the output
towards negative voltages (blue trace). We had set the integral value
above the positive saturation voltage, such that it takes longer until
it reaches the negative saturation voltage. The output of the pid module
is bound to saturate at +- 1 Volts, which is clearly visible in the
green trace. The value of the integral is internally represented by a 32
bit number, so it can practically take arbitrarily large values compared
to the 14 bit output. You can set it within the range from +4 to -4V,
for example if you want to exloit the delay, or even if you want to
compensate it with proportional gain.

Input filters
^^^^^^^^^^^^^

The pid module has one more feature: A bank of 4 input filters in
series. These filters can be either off (bandwidth=0), lowpass
(bandwidth positive) or highpass (bandwidth negative). The way these
filters were implemented demands that the filter bandwidths can only
take values that scale as the powers of 2.

.. code:: python

    # off by default
    r.pid0.inputfilter

.. code:: python

    # minimum cutoff frequency is 1.1 Hz, maximum 3.1 MHz (for now)
    r.pid0.inputfilter = [1,1e10,-1,-1e10]
    print(r.pid0.inputfilter)

.. code:: python

    # not setting a coefficient turns that filter off
    r.pid0.inputfilter = [0,4,8]
    print(r.pid0.inputfilter)

.. code:: python

    # setting without list also works
    r.pid0.inputfilter = -2000
    print(r.pid0.inputfilter)

.. code:: python

    # turn off again
    r.pid0.inputfilter = []
    print(r.pid0.inputfilter)

You should now go back to the Scope and ASG example above and play
around with the setting of these filters to convince yourself that they
do what they are supposed to.
"""

import numpy as np
from qtpy import QtCore
from ..attributes import FloatProperty, BoolRegister, FloatRegister, GainRegister
from ..modules import SignalLauncher
from . import FilterModule
from ..widgets.module_widgets import PidWidget


[docs]class IValAttribute(FloatProperty): """ Attribute for integrator value """
[docs] def get_value(self, obj): return float(obj._to_pyint(obj._read(0x100), bitlength=16))\ / 2 ** 13
# bitlength used to be 32 until 16/7/2016 # still, FPGA has an asymmetric representation for reading and writing # from/to this register
[docs] def set_value(self, obj, value): """set the value of the register holding the integrator's sum [volts]""" return obj._write(0x100, obj._from_pyint( int(round(value * 2 ** 13)), bitlength=16))
[docs]class SignalLauncherPid(SignalLauncher): update_ival = QtCore.Signal() # the widget decides at the other hand if it has to be done or not # depending on the visibility def __init__(self, module): super(SignalLauncherPid, self).__init__(module) self.timer_ival = QtCore.QTimer() self.timer_ival.setInterval(1000) # max. refresh rate: 1 Hz self.timer_ival.timeout.connect(self.update_ival) self.timer_ival.setSingleShot(False) self.timer_ival.start() def _clear(self): """ kill all timers """ self.timer_ival.stop() super(SignalLauncherPid, self)._clear()
[docs]class Pid(FilterModule): """ A proportional/Integrator/Differential filter. The PID filter consists of a 4th order filter input stage, followed by a proportional and integral stage in parallel. .. warning:: at the moment, the differential stage of PIDs is disabled. Example: .. code-block :: python from pyrpl import Pyrpl pid = Pyrpl().rp.pid0 # set a second order low-pass filter with 100 Hz cutoff frequency pid.inputfilter = [100, 100] # set asg0 as input pid.input = 'asg0' # setpoint at -0.1 pid.setpoint = -0.1 # integral gain at 0.1 pid.i = 0.1 # proportional gain at 0.1 pid.p = 0.1 .. code-block :: python >>> print(pid.ival) 0.43545 .. code-block :: python >>> print(pid.ival) 0.763324 """ _widget_class = PidWidget _signal_launcher = SignalLauncherPid _setup_attributes = ["input", "output_direct", "setpoint", "p", "i", #"d", "inputfilter", "max_voltage", "min_voltage"] _gui_attributes = _setup_attributes + ["ival"] # the function is here so the metaclass generates a setup(**kwds) function def _setup(self): """ sets up the pid (just setting the attributes is OK). """ pass _delay = 3 # min delay in cycles from input to output_signal of the module # with integrator and derivative gain, delay is rather 4 cycles _PSR = 12 # Register(0x200) _ISR = 32 # Register(0x204) _DSR = 10 # Register(0x208) _GAINBITS = 24 # Register(0x20C) ival = IValAttribute(min=-4, max=4, increment= 8. / 2**16, doc="Current " "value of the integrator memory (i.e. pid output voltage offset)") setpoint = FloatRegister(0x104, bits=14, norm= 2 **13, doc="pid setpoint [volts]") min_voltage = FloatRegister(0x124, bits=14, norm= 2 **13, doc="minimum output signal [volts]") max_voltage = FloatRegister(0x128, bits=14, norm= 2 **13, doc="maximum output signal [volts]") p = GainRegister(0x108, bits=_GAINBITS, norm= 2 **_PSR, doc="pid proportional gain [1]") i = GainRegister(0x10C, bits=_GAINBITS, norm= 2 **_ISR * 2.0 * np.pi * 8e-9, doc="pid integral unity-gain frequency [Hz]") #d = GainRegister(0x110, bits=_GAINBITS, norm= 2 ** _DSR /( 2.0 *np. pi * # 8e-9), # invert=True, # doc="pid derivative unity-gain frequency [Hz]. Off # when 0.") @property def proportional(self): return self.p @property def integral(self): return self.i @property def derivative(self): return self.d @property def reg_integral(self): return self.ival @proportional.setter def proportional(self, v): self.p = v @integral.setter def integral(self, v): self.i = v @derivative.setter def derivative(self, v): self.d = v @reg_integral.setter def reg_integral(self, v): self.ival = v # deactivated for performance reasons # normalization_on = BoolRegister(0x130, 0, # doc="if True the PID is used " # "as a normalizer") # # # current normalization gain is p-register # normalization_i = FloatRegister(0x10C, bits=_GAINBITS, # norm=2 ** (_ISR) * 2.0 * np.pi * # 8e-9 / 2 ** 13 / 1.5625, # # 1.5625 is empirical value, # # no time/idea to do the maths # doc="stablization crossover frequency [Hz]") # # @property # def normalization_gain(self): # """ current gain in the normalization """ # return self.p / 2.0 # # normalization_inputoffset = FloatRegister(0x110, bits=(14 + _DSR), # norm=2 ** (13 + _DSR), # doc="normalization inputoffset [volts]")
[docs] def transfer_function(self, frequencies, extradelay=0): """ Returns a complex np.array containing the transfer function of the current PID module setting for the given frequency array. The settings for p, i, d and inputfilter, as well as delay are aken into account for the modelisation. There is a slight dependency of delay on the setting of inputfilter, i.e. about 2 extracycles per filter that is not set to 0, which is however taken into account. Parameters ---------- frequencies: np.array or float Frequencies to compute the transfer function for extradelay: float External delay to add to the transfer function (in s). If zero, only the delay for internal propagation from input to output_signal is used. If the module is fed to analog inputs and outputs, an extra delay of the order of 200 ns must be passed as an argument for the correct delay modelisation. Returns ------- tf: np.array(..., dtype=np.complex) The complex open loop transfer function of the module. """ return Pid._transfer_function(frequencies, p=self.p, i=self.i, d=0, # d is currently not available filter_values=self.inputfilter, extradelay_s=extradelay, module_delay_cycle=self._delay, frequency_correction=self._frequency_correction)
@classmethod def _transfer_function(cls, frequencies, p, i, filter_values=list(), d=0, module_delay_cycle=_delay, extradelay_s=0.0, frequency_correction=1.0): return Pid._pid_transfer_function(frequencies, p=p, i=i, d=d, frequency_correction=frequency_correction)\ * Pid._filter_transfer_function(frequencies, filter_values=filter_values, frequency_correction=frequency_correction)\ * Pid._delay_transfer_function(frequencies, module_delay_cycle=module_delay_cycle, extradelay_s=extradelay_s, frequency_correction=frequency_correction) @classmethod def _pid_transfer_function(cls, frequencies, p, i, d=0, frequency_correction=1.): """ returns the transfer function of a generic pid module delay is the module delay as found in pid._delay, p, i and d are the proportional, integral, and differential gains frequency_correction is the module frequency_corection as found in pid._frequency_corection """ frequencies = np.array(frequencies, dtype=np.complex) # integrator with one cycle of extra delay tf = i / (frequencies * 1j) \ * np.exp(-1j * 8e-9 * frequency_correction * frequencies * 2 * np.pi) # proportional (delay in self._delay included) tf += p # derivative action with one cycle of extra delay # if self.d != 0: # tf += frequencies*1j/self.d \ # * np.exp(-1j * 8e-9 * self._frequency_correction * # frequencies * 2 * np.pi) # add delay delay = 0 # module_delay * 8e-9 / self._frequency_correction tf *= np.exp(-1j * delay * frequencies * 2 * np.pi) return tf @classmethod def _delay_transfer_function(cls, frequencies, module_delay_cycle=_delay, extradelay_s=0, frequency_correction=1.0): """ Transfer function of the eventual extradelay of a pid module """ delay = module_delay_cycle * 8e-9 / frequency_correction + extradelay_s frequencies = np.array(frequencies, dtype=np.complex) tf = np.ones(len(frequencies), dtype=np.complex) tf *= np.exp(-1j * delay * frequencies * 2 * np.pi) return tf @classmethod def _filter_transfer_function(cls, frequencies, filter_values, frequency_correction=1.): """ Transfer function of the inputfilter part of a pid module """ frequencies = np.array(frequencies, dtype=np.complex) module_delay = 0 tf = np.ones(len(frequencies), dtype=complex) # input filter modelisation if not isinstance(filter_values, list): filter_values = list([filter_values]) for f in filter_values: if f == 0: continue elif f > 0: # lowpass tf /= (1.0 + 1j * frequencies / f) module_delay += 2 # two cycles extra delay per lowpass elif f < 0: # highpass tf /= (1.0 + 1j * f / frequencies) # plus is correct here since f already has a minus sign module_delay += 1 # one cycle extra delay per highpass delay = module_delay * 8e-9 / frequency_correction tf *= np.exp(-1j * delay * frequencies * 2 * np.pi) return tf