class Signal(LockboxModule, SignalModule):
    """
    represention of a physial signal. Can be either an imput or output signal.
    """
    _widget = None
    calibration_data = ModuleProperty(CalibrationData)

    def signal(self):
        """ derived class should define this method which yields the scope-
        compatible signal that can be used to monitor this signal"""
        raise ValueError("Please define the method 'signal()' if the Signal "
                         "%s to return a valid scope-compatible input.",
                         self.name)
        return 'off'

    def get_analog_offset(self, duration=1.0):
        """ function to acquire the analog offset of the signal (with nothing connected).
         This offset is subtracted from all raw signals"""
        # make sure the lockbox is unlocked, just in case
        self.lockbox.unlock()
        # sample the input with a rather long duration to get a good average
        self.stats(t=duration)
        current_residual_offset, current_rms = self.mean, self.rms
        last_offset = self.calibration_data._analog_offset
        # current_residual_offset = current_offset - last_offset
        current_offset = last_offset + current_residual_offset
        self.calibration_data._analog_offset = current_offset
        self.calibration_data._analog_offset_rms = current_rms
        self._logger.info("Calibrated analog offset of signal %s. "
                          "Old value: %s, new value: %s, difference: %s. "
                          "Rms of the measurement: %s.",
                          self.name,
                          last_offset,
                          self.calibration_data._analog_offset,
                          current_residual_offset,
                          current_rms)

    ##################################################
    # Sampler routines for diagnostics of the signal #
    ##################################################
    @property
    def sampler_time(self):
        """ specifies the duration over which to sample a signal """
        if hasattr(self, '_sampler_time') and self._sampler_time is not None:
            return self._sampler_time
        elif hasattr(self.lockbox, '_sampler_time') and self.lockbox._sampler_time is not None:
            return self.lockbox._sampler_time
        else:
            return 0.01

    def stats(self, t=None):
        """
        returns a tuple containing the mean, rms, max, and min of the signal.
        """
        # generate new samples for mean, rms, max, min if
        # a) executed for the first time,
        # b) nonstandard sampler time
        # c) last sample older than sampler time
        # Point c) ensures that we can call stats several times in
        # immediate succession, e.g. to get mean and rms
        if not hasattr(self, '_lasttime') or t is not None or \
                time() - self._lasttime >= self.sampler_time:
            # choose sampler time
            if t is None:
                t = self.sampler_time
            # get fresh data
            self._lastmean, self._lastrms, self._lastmax, self._lastmin\
                = self.pyrpl.rp.sampler.stats(self.signal(), t=t)
            # subtract analog offset from all non-relative values
            self._lastmean -= self.calibration_data._analog_offset
            self._lastmax -= self.calibration_data._analog_offset
            self._lastmin -= self.calibration_data._analog_offset
            # save a timestamp and the employed sampler time
            self._lasttime = time()
            self._lastt = t
        return self._lastmean, self._lastrms, self._lastmax, self._lastmin

    @property
    def mean(self):
        # get fresh data
        mean, rms, max, min= self.stats()
        # compute relative quantity
        return mean

    @property
    def rms(self):
        # get fresh data
        mean, rms, max, min= self.stats()
        # compute relative quantity
        return rms

    @property
    def max(self):
        # get fresh data
        mean, rms, max, min= self.stats()
        # compute relative quantity
        return max

    @property
    def min(self):
        # get fresh data
        mean, rms, max, min= self.stats()
        # compute relative quantity
        return min

    @property
    def relative_mean(self):
        """
        returns the ratio between the measured mean value and the expected one.
        """
        # compute relative quantity
        return self.mean / self.calibration_data.amplitude

    @property
    def relative_rms(self):
        """
        returns the ratio between the measured rms value and the expected mean.
        """
        # compute relative quantity
        return self.rms / self.calibration_data.amplitude

    def diagnostics(self, duration = 1.0):
        """
        example code for lock diagnostics:

        Parameters
        ----------
        duration: duration over which to average

        Returns
        -------
        relative rms of the signal, normalized by
        """
        # samples the input over duration
        self.stats(t=duration)
        return self.relative_rms class BoolProperty(BaseProperty):
    """
    A property for a boolean value
    """
    _widget_class = BoolAttributeWidget
    default = False

    def validate_and_normalize(self, obj, value):
        """
        Converts value to bool.
        """
        return bool(value) class FloatProperty(NumberProperty):
    """
    An attribute for a float value.
    """
    _widget_class = FloatAttributeWidget
    default = 0.0

    def validate_and_normalize(self, obj, value):
        """
        Try to convert to float, then saturates with min and max
        """
        return NumberProperty.validate_and_normalize(self,
                                                     obj,
                                                     float(value)) class SelectProperty(BaseProperty):
    """
    An attribute for a multiple choice value.

    The options can be specified at the object creation as a list or an
    (ordered) dict, or as a callable with one argument (which is None or the
    module that contains this attribute, depending on when the call is made).
    Options can be specified at attribute creation, but it can also be updated
    later on a per-module basis using change_options(new_options). If
    options are callable, they are evaluated every time they are needed.
    """
    _widget_class = SelectAttributeWidget
    default = None

    def __init__(self,
                 options=[],
                 **kwargs):
        self.default_options = options
        BaseProperty.__init__(self, **kwargs)

    @property
    def __doc__(self):
        # Append available options to docstring
        return self.doc + "\r\nOptions:\r\n" + str(list(self.options(None)))

    @__doc__.setter
    def __doc__(self, value):
        self.doc = value

    def get_default(self, instance):
        """ returns the default value. default is pre-defined value
        if that is not a valid option. Otherwise the first valid option
        is taken, and if that is not possible (no options), None is taken. """
        default = self.default  # internal default
        # at startup, we cannot access the instance, so we must continue without it
        if instance is not None:
            # make sure default is stored in the instance, such that it can be easily modified
            if not hasattr(instance, '_' + self.name + '_' + 'default'):
                setattr(instance, '_' + self.name + '_' + 'default', default)
            default = getattr(instance, '_' + self.name + '_' + 'default')
        # make sure default is a valid option
        options = self.options(instance)
        if not default in options:
            # if not valid, default default is the first options
            default = list(options)[0]
        # if no options are availbale, fall back to None
        if default is None:
            logger.warning("Default of SelectProperty %s "
                           "is None. ", self.name)
        return default

    def options(self, instance=None):
        """
        options are evaluated at run time. options may be callable with instance as optional argument.
        """
        options = self.default_options
        # at startup, we cannot access the instance, so we must continue without it
        if instance is not None:
            # make sure default is stored in the instance, such that it can be easily modified
            if not hasattr(instance, '_' + self.name + '_' + 'options'):
                setattr(instance, '_' + self.name + '_' + 'options', options)
            options = getattr(instance, '_' + self.name + '_' + 'options')
        if callable(options):
            try:
                options = options(instance)
            except (TypeError, AttributeError):
                try:
                    options = options()
                except (TypeError, AttributeError):
                    options = OrderedDict()
        if not hasattr(options, "keys"):
            options = OrderedDict([(v, v) for v in options])
        if len(options) == 0:
            logger.debug("SelectProperty %s of module %s has no options!", self.name, instance)
            options = {None: None}
        # check whether options keys have changed w.r.t. last time and emit a signal in that
        # case. Also create a list of valid options in the parent module called
        # self.name+'_options'.
        if instance is not None:
            try:
                lastoptions = getattr(instance, '_' + self.name + '_lastoptions')
            except AttributeError:
                lastoptions = None
            if options != lastoptions:
                setattr(instance, '_' + self.name + '_lastoptions', options)
                # save the keys for the user convenience
                setattr(instance, self.name + '_options', list(options.keys()))
                instance._signal_launcher.change_options.emit(self.name,
                                                              list(options))
        # return the actual options
        return options

    def change_options(self, instance, new_options):
        """
        Changes the possible options acceptable by the Attribute:

        - New validation takes effect immediately (otherwise a script
          involving 1. changing the options / 2. selecting one of the
          new options could not be executed at once)
        - Update of the ComboxBox is performed behind a signal-slot
          mechanism to be thread-safe
        - If the current value is not in the new_options, then value
          is changed to some available option
        """
        setattr(instance, '_' + self.name + '_' + 'options', new_options)
        # refresh default options in case options(None) is called (no instance in argument)
        # this also triggers the signal emission in the method options()
        self.default_options = self.options(instance)

    def validate_and_normalize(self, obj, value):
        options = self.options(obj)
        if not (value in options):
            msg = "Value '%s' is not an option for SelectAttribute %s of " \
                  "module %s with options %s" \
                  % (value, self.name, obj.name, options)
            if self.ignore_errors:
                value = self.get_default(obj)
                logger.warning(msg + ". Picking an arbitrary value %s instead."
                               % str(value))
            else:
                logger.error(msg)
                raise ValueError(msg)
        return value

    def get_value(self, obj):
        if not hasattr(obj, '_' + self.name):
            setattr(obj, '_' + self.name, self.get_default(obj))
        value = getattr(obj, '_' + self.name)
        # make sure the value is a valid option
        value = self.validate_and_normalize(obj, value)
        return value

    def set_value(self, obj, value):
        BaseProperty.set_value(self, obj, value) class FilterProperty(BaseProperty):
    """
    An attribute for a list of bandwidth. Each bandwidth has to be chosen in a list given by
    self.valid_frequencies(module) (evaluated at runtime). If floats are provided, they are normalized to the
    nearest values in the list. Individual floats are also normalized to a singleton.
    The number of elements in the list are also defined at runtime.
    A property for a list of float values to be chosen in valid_frequencies(module).
    """
    _widget_class = FilterAttributeWidget

    def validate_and_normalize(self, obj, value):
        """
        Returns a list with the closest elements in module.valid_frequencies
        """
        if not np.iterable(value):
            value = [value]
        value = [min([opt for opt in self.valid_frequencies(obj)],
                      key=lambda x: abs(x - val)) for val in value]
        if len(value) == 1:
            return value[0]
        else:
            return value

    def get_value(self, obj):
        if not hasattr(obj, '_' + self.name):
            # choose any value in the options as default.
            default = self.valid_frequencies(obj)[0]
            setattr(obj, '_' + self.name, default)
        return getattr(obj, '_' + self.name)

    def set_value(self, obj, value):
        return BaseProperty.set_value(self, obj, value)

    def valid_frequencies(self, module):
        raise NotImplementedError("this is a baseclass, your derived class "
                                  "must implement the following function")

    def refresh_options(self, module):
        module._signal_launcher.refresh_filter_options.emit(self.name) class FrequencyProperty(FloatProperty):
    """
    An attribute for frequency values
    Same as FloatAttribute, except it cannot become negative.
    """
    def __init__(self, **kwargs):
        if 'min' not in kwargs:
            kwargs['min'] = 0
        FloatProperty.__init__(self, **kwargs) class IntProperty(NumberProperty):
    def __init__(self,
                 min=-np.inf,
                 max=np.inf,
                 increment=1,
                 log_increment=False,  # if True, the widget has log increment
                 **kwargs):
        super(IntProperty, self).__init__(min=min,
                                          max=max,
                                          increment=increment,
                                          log_increment=log_increment,
                                          **kwargs)

    def validate_and_normalize(self, obj, value):
        """
        Accepts float, but rounds to integer
        """
        if value is None:  # setting a number to None essentially calls setup()
            value = self.get_value(obj)
        return NumberProperty.validate_and_normalize(self,
                                                     obj,
                                                     int(round(value))) class CurveDB(object):
    _dirname = user_curve_dir
    file_extension = '.dat'

    if not os.path.exists(_dirname): # if _dirname doesn't exist, some unexpected errors will occur.
        os.mkdir(_dirname)

    def __init__(self, name="some_curve"):
        """
        A CurveDB object has
        - name   = string to give the curve a name
        - pk     = integer to uniquely identify the curve (the database primary key)
        - data   = pandas.Series() object to hold any data
        - params = dict() with all kinds of parameters
        """
        self.logger = logging.getLogger(name=__name__)
        self.params = dict()
        x, y = np.array([], dtype=np.float), np.array([], dtype=np.float)
        self.data = (x, y)
        self.name = name

    @property
    def name(self):
        return self.params["name"]

    @name.setter
    def name(self, val):
        self.params["name"] = val
        return val

    @classmethod
    def create(cls, *args, **kwds):
        """
        Creates a new curve, first arguments should be either
        Series(y, index=x) or x, y.
        kwds will be passed to self.params
        """
        if len(args) == 0:
            ser = (np.array([], dtype=np.float), np.array([], dtype=np.float))
        if len(args) == 1:
            if isinstance(args[0], pd.Series):
                x, y = args[0].index.values, args[0].values
                ser = (x, y)
            elif isinstance(args[0], (np.array, list, tuple)):
                ser = args[0]
            else:
                raise ValueError("cannot recognize argument %s as numpy.array or pandas.Series.", args[0])
        elif len(args) == 2:
            x = np.array(args[0])
            y = np.array(args[1])
            ser = (x, y)
        else:
            raise ValueError("first arguments should be either x or x, y")
        obj = cls()
        obj.data = ser
        obj.params = kwds
        if not 'name' in obj.params:
            obj.params['name'] = 'new_curve'
        pk = obj.pk  # make a pk
        if "childs" not in obj.params:
            obj.params["childs"] = None
        if ("autosave" not in kwds) or (kwds["autosave"]):
            obj.save()
        return obj

    def plot(self):
        x, y = self.data
        pd.Series(y, index=x).plot()

    # Implement the following methods if you want to save curves permanently
    @classmethod
    def get(cls, curve):
        if isinstance(curve, CurveDB):
            return curve
        elif isinstance(curve, list):
            return [CurveDB.get(c) for c in curve]
        else:
            with open(os.path.join(CurveDB._dirname, str(curve) + cls.file_extension),
                      'rb' if file_backend.__name__ == 'pickle' else 'r')\
                    as f:
                # rb is for compatibility with python 3
                # see http://stackoverflow.com/questions/5512811/builtins-typeerror-must-be-str-not-bytes
                curve = CurveDB()
                curve._pk, curve.params, data = file_backend.load(f)
                curve.data = tuple([np.asarray(a) for a in data])
            if isinstance(curve.data, pd.Series):  # for backwards compatibility
                x, y = curve.data.index.values, curve.data.values
                curve.data = (x, y)
            return curve

    def save(self):
        with open(os.path.join(self._dirname, str(self.pk) + self.file_extension),
                  'wb' if file_backend.__name__ == 'pickle' else 'w')\
                as f:
            # wb is for compatibility with python 3
            # see http://stackoverflow.com/questions/5512811/builtins-typeerror-must-be-str-not-bytes
            data = [a.tolist() for a in self.data]
            file_backend.dump([self.pk, self.params, data], f, )

    def delete(self):
        # remove the file
        delpk = self.pk
        parent = self.parent
        childs = self.childs
        if isinstance(childs, list) and len(childs)> 0:
            self.logger.debug("Deleting all childs of curve %d"%delpk)
            for child in childs:
                child.delete()
        self.logger.debug("Deleting curve %d" % delpk)
        try:
            filename = os.path.join(self._dirname, str(self.pk) + self.file_extension)
            os.remove(filename)
        except OSError:
            self.logger.warning("Could not find and remove the file %s. ",
                                filename)
        if parent:
            parentchilds = parent.childs
            parentchilds.remove(delpk)
            parent.childs = parentchilds
            parent.save()

    # Implement the following methods if you want to use a hierarchical
    # structure for curves
    @property
    def childs(self):
        try:
            childs = self.params["childs"]
        except KeyError:
            return []
        if childs is None:
            return []
        else:
            try:
                return CurveDB.get(childs)
            except KeyError:
                return []

    @property
    def parent(self):
        try:
            parentid = self.params["parent"]
        except KeyError:
            self.logger.debug("No parent found.")
            return None
        else:
            return CurveDB.get(parentid)

    def add_child(self, child_curve):
        child = CurveDB.get(child_curve)
        child.params["parent"] = self.pk
        child.save()
        childs = self.params["childs"] or []
        self.params["childs"] = list(childs+[child.pk])
        self.save()

    @classmethod
    def all_pks(cls):
        """
        Returns:
            list of int: A list of the primary keys of all CurveDB objects on the computer.
        """
        pks = [int(f.split('.dat')[0])
               for f in os.listdir(cls._dirname) if f.endswith('.dat')]
        return sorted(pks, reverse=True)

    @classmethod
    def all(cls):
        """
        Returns:
            list of CurveDB: A list of all CurveDB objects on the computer.
        """
        return [cls.get(pk) for pk in cls.all_pks()]

    @property
    def pk(self):
        """
        (int): The primary Key of the
        """
        if hasattr(self, "_pk"):
            return self._pk
        else:
            pks = self.all_pks()
            if len(pks) == 0:
                self._pk = 1
            else:
                self._pk = max(pks) + 1
            # create the file to make this pk choice persistent
            with open(os.path.join(self._dirname,
                                   str(self._pk) + ".dat"), 'w') as f:
                f.close()
            return self._pk
        return -1
        # a proper implementation will assign the database primary key for pk
        # the primary key is used to load a curve from the storage into memory

    def sort(self):
        """numerically sorts the data series so that indexing can be used"""
        X, Y = self.data
        xs = np.array([x for (x, y) in sorted(zip(X, Y))], dtype=np.float64)
        ys = np.array([y for (x, y) in sorted(zip(X, Y))], dtype=np.float64)
        self.data = (xs, ys)

    def fit(self):
        """ prototype for fitting a curve """
        self.logger.warning("Not implemented")
        pass

    def get_child(self, name):
        """
        Returns the child of the curve with name 'name'

        Arguments:
            name (str): Name of the child curve to be retrieved. If
                several childs have the same name, the first one is
                returned.

        Returns:
            CurveDB: the child curve
        """
        for c in self.childs:
            if c.name == name:
                return c class WaveformAttribute(SelectProperty):
class AsgAmplitudeAttribute(FloatRegister):
class AsgOffsetAttribute(FloatProperty):
    class Asg(HardwareModule, SignalModule):
    def set_value(self, instance, waveform):
    def get_value(self, obj):
    def set_value(self, obj, val):
    def __init__(self, **kwargs):
    def set_value(self, instance, val):
    def get_value(self, obj):
def make_asg(channel=0):
        def __init__(self, parent, name=None):
        def output_directs(self):
        def _noise_distribution(self):
        def _noise_V2_per_Hz(self):
        def trig(self):
        def data(self):
        def data(self, data):
        def _setup(self):
        def enable_advanced_trigger(self,
                                    frequency,
                                    amplitude,
                                    duration,
                                    invert=False,
                                    autorearm=False,
                                    output_direct='out1'):
        def disable_advanced_trigger(self):
        _DATA_OFFSET = set_DATA_OFFSET
        _VALUE_OFFSET = set_VALUE_OFFSET
        _BIT_OFFSET = set_BIT_OFFSET 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",
                         "pause_gains",
                         "paused",
                         "differential_mode_enabled",
                         ]
    _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.")

    pause_gains = SelectRegister(0x12C,
                                 options=sorted_dict(
                                       off=0,
                                       i=1,
                                       p=2,
                                       pi=3,
                                       d=4,
                                       id=5,
                                       pd=6,
                                       pid=7),
                                 bitmask=0b111,
                                 doc="Selects which gains are frozen during pausing/synchronization."
                                 )

    differential_mode_enabled = BoolRegister(0x12C,
                                             bit=3,
                                             doc="If True, the differential mode is enabled. "
                                                 "In this mode, the setpoint is given by the "
                                                 "input signal of another pid module. "
                                                 "Only pid0 and pid1 can be paired in "
                                                 "differential mode. "
                                             )

    paused = PauseRegister(0xC,
                           invert=True,
                           doc="While True, the gains selected with `pause` are "
                               "temporarily set to zero ")

    @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]")

    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 class OutputSignalWidget(ModuleWidget):
    @property
    def name(self):
        return self.module.name

    @name.setter
    def name(self, value):
        # name is read-only
        pass

    def change_analog_tf(self):
        self.main_props.change_analog_tf()

    def init_gui(self):
        #self.main_layout = QtWidgets.QVBoxLayout()
        #self.setLayout(self.main_layout)
        self.init_main_layout(orientation="vertical")
        self.init_attribute_layout()
        for widget in self.attribute_widgets.values():
            self.main_layout.removeWidget(widget)
        self.upper_layout = QtWidgets.QHBoxLayout()
        self.main_layout.addLayout(self.upper_layout)
        self.col1 = QtWidgets.QVBoxLayout()
        self.col2 = QtWidgets.QVBoxLayout()
        self.col3 = QtWidgets.QVBoxLayout()
        self.col4 = QtWidgets.QVBoxLayout()
        self.upper_layout.addStretch(1)
        self.upper_layout.addLayout(self.col1)
        self.upper_layout.addStretch(1)
        self.upper_layout.addLayout(self.col2)
        self.upper_layout.addStretch(1)
        self.upper_layout.addLayout(self.col3)
        self.upper_layout.addStretch(1)
        self.upper_layout.addLayout(self.col4)
        self.upper_layout.addStretch(1)

        aws = self.attribute_widgets
        self.main_props = MainOutputProperties(self)
        self.col1.addWidget(self.main_props)
        self.col1.addStretch(5)

        self.sweep_props = SweepOutputProperties(self)
        self.col2.addWidget(self.sweep_props)
        self.col2.addStretch(5)

        self.pid_props = PidProperties(self)
        self.pid_props.update_assisted_design()
        self.col3.addWidget(self.pid_props)

        self.col3.addStretch(5)

        self.post_props = PostFiltering(self)
        self.col4.addWidget(self.post_props)

        self.col4.addStretch(5)

        self.win = pg.GraphicsWindow(title="Amplitude")
        self.win_phase = pg.GraphicsWindow(title="Phase")
        self.plot_item = self.win.addPlot(title="Magnitude (dB)")
        self.plot_item_phase = self.win_phase.addPlot(title="Phase (deg)")
        self.plot_item.showGrid(y=True, x=True, alpha=1.)
        self.plot_item_phase.showGrid(y=True, x=True, alpha=1.)

        self.plot_item_phase.setXLink(self.plot_item)

        self.curve = self.plot_item.plot(pen='y')
        self.curve_phase = self.plot_item_phase.plot(pen=None, symbol='o', symbolSize=1)

        self.plot_item.setLogMode(x=True, y=True)
        self.plot_item_phase.setLogMode(x=True, y=None)
        self.curve.setLogMode(xMode=True, yMode=True)
        self.curve_phase.setLogMode(xMode=True, yMode=None)

        self.plotbox = QtWidgets.QGroupBox(self)
        self.plotbox.layout = QtWidgets.QVBoxLayout(self.plotbox)
        self.plotbox.setTitle("Complete open-loop transfer function (V/V)")
        self.plotbox.layout.addWidget(self.win)
        self.plotbox.layout.addWidget(self.win_phase)
        self.main_layout.addWidget(self.plotbox)
        self.update_transfer_function()

    def update_transfer_function(self):
        """
        Updates the transfer function curve of the output.
        """
        freqs = self.module.tf_freqs()
        curve = self.module.transfer_function(freqs)
        abs_curve = abs(curve)
        if(max(abs_curve)>0): # python 2 crashes when plotting zeros in log_mode
            self.curve.setData(freqs, abs_curve)
            self.curve_phase.setData(freqs, 180./np.pi*np.angle(curve))
import numpy as np
from scipy import interpolate
from ...software_modules.lockbox.input import Signal
from ...attributes import BoolProperty, FloatProperty, SelectProperty, \
    FilterProperty, FrequencyProperty, IntProperty
from ...curvedb import CurveDB
from ...hardware_modules.asg import Asg0, Asg1
from ...hardware_modules.pid import Pid
from ...widgets.module_widgets import OutputSignalWidget
from __future__ import division




class AdditionalFilterAttribute(FilterProperty):
    # proxy to the pid inputfilter attribute that emits a signal when changed
    def valid_frequencies(self, obj):
        return obj.pid.__class__.inputfilter.valid_frequencies(obj.pid)

    def get_value(self, obj):
        return obj.pid.inputfilter

    def set_value(self, obj, value):
        obj.pid.inputfilter = value
        obj.lockbox._signal_launcher.update_transfer_function.emit([obj])


class OutputSignal(Signal):
    """
    As many output signals as desired can be added to the lockbox. Each
    output defines:
      - name: the name of the output.
      - dc_gain: how much the model's variable is expected to change for 1 V
        on the output (in *unit*)
      - unit: see above, should be one of the units available in the model.
      - sweep_amplitude/offset/frequency/waveform: what properties to use when
        sweeping the output
      - output_channel: what physical output is used.
      - p/i: the gains to use in a loop: those values are to be understood as
        full loop gains (p in [1], i in [Hz])
      - additional_filter: a filter (4 cut-off frequencies) to add to the loop
        (in sweep and lock mode)
      - extra_module: extra module to add just before the output (usually iir).
      - extra_module_state: name of the state to use for the extra_module.
      - tf_curve: the index of the curve describing the analog transfer
        function behind the output.
      - tf_filter: alternatively, the analog transfer function can be specified
        by a filter (4 cut-off frequencies).
      - desired_unity_gain_frequency: desired value for unity gain frequency.
      - tf_type: ["flat", "curve", "filter"], how is the analog transfer
        function specified.
    """
    _widget_class = OutputSignalWidget
    _gui_attributes = ['unit',
                      'sweep_amplitude',
                      'sweep_offset',
                      'sweep_frequency',
                      'sweep_waveform',
                      'dc_gain',
                      'output_channel',
                      'p',
                      'i',
                      'additional_filter',
                      'analog_filter_cutoff',
                      'extra_module',
                      'extra_module_state',
                      'desired_unity_gain_frequency',
                      'max_voltage',
                      'min_voltage']
    _setup_attributes = _gui_attributes + ['assisted_design', 'tf_curve',
                                           'tf_type']
    # main attributes
    dc_gain = FloatProperty(default=1.0, min=-1e10, max=1e10, call_setup=True)
    output_channel = SelectProperty(options=['out1', 'out2',
                                             'pwm0', 'pwm1'])
    unit = SelectProperty(default='V/V',
                          options=lambda inst:
                          [u+"/V" for u in inst.lockbox._output_units],
                          call_setup=True,
                          ignore_errors=True)
    tf_type = SelectProperty(["flat", "filter", "curve"],
                             default="filter",
                             call_setup=True)

    tf_curve = IntProperty(call_setup=True)