From 08eb8ea6a6f63e47065b39daaf5bbe6deb013b93 Mon Sep 17 00:00:00 2001 From: SamuelDeleglise Date: Fri, 2 Dec 2016 01:50:57 +0100 Subject: [PATCH] already a solid skeleton for lockbox (gui for output is almost complete) Finally, the implementation choice is the following Lockbox is a SoftwareModule... Model, Inputs and Outputs are child SoftwareModules of Lockbox (concept just added now). This way, different states for each of these can be saved and loaded independently from each other... + all the gui comes for free. --- pyrpl/attributes.py | 40 +- pyrpl/config/tests_temp.yml | 31 +- pyrpl/modules.py | 47 +- pyrpl/software_modules/lockbox/lockbox.py | 130 ++-- pyrpl/software_modules/lockbox/model.py | 600 +------------- pyrpl/software_modules/lockbox/signal.py | 910 ---------------------- pyrpl/widgets/attribute_widgets.py | 45 +- pyrpl/widgets/module_widgets.py | 62 +- 8 files changed, 254 insertions(+), 1611 deletions(-) delete mode 100644 pyrpl/software_modules/lockbox/signal.py diff --git a/pyrpl/attributes.py b/pyrpl/attributes.py index 682fff516..2b4ed77f5 100644 --- a/pyrpl/attributes.py +++ b/pyrpl/attributes.py @@ -39,6 +39,7 @@ class BaseAttribute(object): - a function get_value(instance, owner) that reads the value from wherever it is stored internally """ widget_class = None + widget = None def __init__(self, default=None, doc=""): """ @@ -107,8 +108,8 @@ def create_widget(self, module, name=None): """ if name is None: name = self.name # attributed by the metaclass of module - return self.widget_class(name, module) - + self.widget = self.widget_class(name, module) + return self.widget class NumberAttribute(BaseAttribute): """ @@ -120,6 +121,7 @@ def create_widget(self, module, name=None): widget.set_increment(self.increment) widget.set_maximum(self.max) widget.set_minimum(self.min) + self.widget = widget return widget def validate_and_normalize(self, value, module): @@ -223,7 +225,13 @@ def options(self, obj): else: return self._options """ - + def change_options(self, new_options): + """ + Replace (dynamically) options by new_options. + """ + self.options = new_options + if self.widget is not None: + self.widget.change_options(new_options) def create_widget(self, module, name=None): """ @@ -255,6 +263,32 @@ def validate_and_normalize(self, value, module): return min([opt for opt in options], key=lambda x: abs(x - value)) +#class DynamicSelectAttribute(BaseAttribute): +# """ +# An attribute for a multiple choice value. +# The options are evaluated at runtime by the function options(self, instance). +# Validation is strict (value should be one of the options()) +# """ +# widget_class = DynamicSelectAttributeWidget +# +# def options(self, instance): +# """ +# options are evaluated at run time. To be reimplemented in base class. +# """ +# raise NotImplementedError("This function should be implemented in derived class.") +# +# def validate_and_normalize(self, value, module): +# """ +# value should evaluate to a string present in self.options(instance) at evaluation time. +# """ +# value = str(value) +# if not (value in self.options(module)): +# raise ValueError("value %s is not an option for SelectAttribute %s of %s" % (value, +# self.name, +# module.name)) +# return value + + class StringAttribute(BaseAttribute): """ An attribute for string (in practice, there is no StringRegister at this stage). diff --git a/pyrpl/config/tests_temp.yml b/pyrpl/config/tests_temp.yml index 2a2843693..8c1c518f9 100644 --- a/pyrpl/config/tests_temp.yml +++ b/pyrpl/config/tests_temp.yml @@ -11,20 +11,20 @@ pyrpl: # General configuration of the software: - NetworkAnalyzer - NetworkAnalyzer window_position: - - -8 - - -8 + - -7 + - 0 - 1916 - - 1017 + - 1008 dock_positions: !!binary | - AAAA/wAAAAD9AAAAAQAAAAIAAAd8AAAD0PwBAAAAAfwAAAAAAAAHfAAABy8A/////AIAAAAD/AAA - ABUAAAHTAAABXwD////8AQAAAAL8AAAAAAAAA5AAAANDAP////oAAAACAQAAAAP7AAAAIgBzAHAA + AAAA/wAAAAD9AAAAAQAAAAIAAAd8AAADx/wBAAAAAfwAAAAAAAAHfAAAA0MA/////AIAAAAD/AAA + ABUAAAPHAAABSgD////8AQAAAAL8AAAAAAAAB3wAAANDAP////oAAAABAQAAAAP7AAAAIgBzAHAA ZQBjAHQAcgB1AG0AXwBhAG4AYQBsAHkAegBlAHIBAAAAAP////8AAAAAAAAAAPsAAAAGAGkAcQBz - AQAAAAD/////AAADQwD////7AAAACABhAHMAZwBzAQAAAAAAAAd8AAACMQD////8AAADlAAAA+gA - AAPoAP////oAAAAAAgAAAAL7AAAABgBuAGEAMgEAAAAA/////wAAAO0A////+wAAAAwAcwBjAG8A - cABlAHMBAAAAFQAAAdMAAADBAP////wAAAHsAAAB+QAAAcMA/////AEAAAAC+wAAAAgAaQBpAHIA - cwEAAAAAAAADjAAAAxQA/////AAAA5AAAAPsAAAD6AD////6AAAAAgIAAAAD+wAAAAYAbgBhADEB - AAAAAP////8AAADtAP////sAAAAEAG4AYQEAAAAA/////wAAAAAAAAAA+wAAAAgAcABpAGQAcwEA - AAJbAAABrgAAAa4A////+wAAAAQAbgBhAAAAA3UAAADoAAAAAAAAAAAAAAd8AAAAAAAAAAQAAAAE + AQAAAAD/////AAADQwD////7AAAACABhAHMAZwBzAAAAAAAAAAd8AAAAFgD////8AAAAAAAAB3wA + AAAAAP////r/////AgAAAAL7AAAABgBuAGEAMgAAAAAA/////wAAAFAA////+wAAAAwAcwBjAG8A + cABlAHMAAAAAFQAAAdMAAABQAP////wAAAAVAAADxwAAAAAA/////AEAAAAC+wAAAAgAaQBpAHIA + cwAAAAAAAAAHfAAAABYA/////AAAAAAAAAd8AAAAAAD////6AAAAAQIAAAAD+wAAAAYAbgBhADEA + AAAAAP////8AAABQAP////sAAAAEAG4AYQEAAAAA/////wAAAAAAAAAA+wAAAAgAcABpAGQAcwAA + AAJbAAABrgAAAFAA////+wAAAAQAbgBhAAAAA3UAAADoAAAAAAAAAAAAAAd8AAAAAAAAAAQAAAAE AAAACAAAAAj8AAAAAA== redpitaya: hostname: 10.214.1.28 @@ -72,8 +72,9 @@ pwms: pwm1: {} pwm2: {} iqs: - iq1: {output_signal: quadrature, bandwidth: [0, 0], input: adc1, frequency: 0, acbandwidth: [ - 0], phase: 180.0, quadrature_factor: 0.0, output_direct: off, gain: 0.0, amplitude: 0.0} + iq1: {output_signal: quadrature, bandwidth: [1214, 2428], input: adc1, frequency: 9999.98883344233, + acbandwidth: [0], phase: 180.0, quadrature_factor: 0.1015625, output_direct: out1, + gain: 0.0, amplitude: 0.09999847412109375} states: {yoyo: {output_direct: off, bandwidth: [0, 0], input: adc1, amplitude: 0.0, gain: 0.0, phase: 0.0, acbandwidth: 0, quadrature_factor: 0.0, output_signal: quadrature, frequency: 0.0}, phase90: {output_signal: quadrature, bandwidth: [0, 0], quadrature_factor: 0.0, @@ -108,7 +109,7 @@ iqs: pids: pid3: {ival: 0.0} pid2: {ival: 0.0} - pid4: {ival: 0.0} + pid4: {ival: 0.0, p: 0.114501953125} pid1: {ival: 0.0, output_direct: off, d: 0.0, setpoint: 0.0, p: 0.0, inputfilter: [ 0, 0, 0, 0], input: adc1, i: 0.0} states: {iui: {output_direct: off, d: 0.0, setpoint: 0.0, p: 0.0, inputfilter: [ @@ -129,3 +130,5 @@ iirs: input: adc1 zeros: - (-100-947.7629626775897j) +lockboxs: + lockbox: {dummy_outputs: {dummy_output: {coucou: 3, p: 0.044885369873046886}}} diff --git a/pyrpl/modules.py b/pyrpl/modules.py index 48cdd455c..725e50de3 100644 --- a/pyrpl/modules.py +++ b/pyrpl/modules.py @@ -50,15 +50,11 @@ def setup(self, **kwds): self._callback_active = True return setup - -class ModuleMetaClass(type): +class NameAttributesMetaClass(type): ''' - 1. Magic to retrieve the name of the attributes in the attributes themselves. + Magic to retrieve the name of the attributes in the attributes themselves. see http://code.activestate.com/recipes/577426-auto-named-decriptors/ - - 2. Builds the setup docstring by aggregating _setup's and setup_attributes's docstrings. ''' - def __new__(cls, classname, bases, classDict): """ Iterate through the new class' __dict__ and update the .name of all recognised BaseAttribute. @@ -70,6 +66,11 @@ def __new__(cls, classname, bases, classDict): attr.name = name return type.__new__(cls, classname, bases, classDict) + +class ModuleMetaClass(NameAttributesMetaClass): + ''' + Builds the setup docstring by aggregating _setup's and setup_attributes's docstrings. + ''' def __init__(self, classname, bases, classDict): """ Takes care of creating the module's 'setup' function. @@ -163,9 +164,9 @@ def c_states(self): """ Returns the config file branch corresponding to the "states" section """ - if not "states" in self.c._parent._keys(): - self.c._parent["states"] = dict() - return self.c._parent.states + if not "states" in self.parent.c._keys(): + self.parent.c["states"] = dict() + return self.parent.c.states def save_state(self, name): """Saves the current state under the name "name" in the config file""" @@ -224,9 +225,9 @@ def c(self): section of the config file. """ manager_section = self.__class__.name + "s" # for instance, iqs - if not manager_section in self.pyrpl_config._keys(): - self.pyrpl_config[manager_section] = dict() - manager_section = getattr(self.pyrpl_config, manager_section) + if not manager_section in self.parent.c._keys(): + self.parent.c[manager_section] = dict() + manager_section = getattr(self.parent.c, manager_section) if not self.name in manager_section._keys(): manager_section[self.name] = dict() return getattr(manager_section, self.name) @@ -375,19 +376,33 @@ class SoftwareModule(BaseModule): ready for acquisition/output with the current setup_attributes' values. """ - def __init__(self, pyrpl, name=None): + def __init__(self, parent, name=None): """ Creates a module with given name. If name is None, uses cls.name. + parent is either a pyrpl instance, or another SoftwareModule. + - First case: config file entry is in self.__class__.name + 's'--> self.name + - Second case: config file entry is in parent_entry-->self.__class__.name + 's'-->self.name """ self._autosave_active = False # attribute values are not overwritten in the config file if name is not None: self.name = name - self.pyrpl = pyrpl - self._parent = pyrpl - self.pyrpl_config = pyrpl.c + self.parent = parent + #self._parent = pyrpl + #self.pyrpl_config = pyrpl.c self.init_module() self._autosave_active = True + @property + def pyrpl(self): + """ + Recursively looks through patent modules untill pyrpl instance is reached. + """ + from pyrpl.pyrpl import Pyrpl + parent = self.parent + while(not isinstance(parent, Pyrpl)): + parent = parent.parent + return parent + def init_module(self): """ To be reimplemented in child class. diff --git a/pyrpl/software_modules/lockbox/lockbox.py b/pyrpl/software_modules/lockbox/lockbox.py index 9b690e82f..3bd7efb9a 100644 --- a/pyrpl/software_modules/lockbox/lockbox.py +++ b/pyrpl/software_modules/lockbox/lockbox.py @@ -1,85 +1,59 @@ -############################################################################### -# pyrpl - DSP servo controller for quantum optics with the RedPitaya -# Copyright (C) 2014-2016 Leonhard Neuhaus (neuhaus@spectro.jussieu.fr) -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -############################################################################### +from pyrpl.modules import SoftwareModule +from pyrpl.attributes import SelectProperty +from .model import Model +from .signals import OutputSignal -# buglist: in lock_opt, it is inconvenient to always specify sof and pdh. unlocks when only pdh is changed -# unspecified parameters should rather be left unchanged instead of being -# set to 0 or 1 +from collections import OrderedDict -from pyrpl.software_modules import SoftwareModule -import logging -from .signal import logger -import os -def getmodel(modeltype): - try: - m = globals()[modeltype] - if type(m) == type: - return m - except KeyError: - pass - # try to find a similar model with lowercase spelling - for k in globals(): - if k.lower() == modeltype.lower(): - m = globals()[k] - if type(m) == type: - return m - logger.error("Model %s not found in model definition file %s", - modeltype, __file__) - -class Lockbox(SoftwareModule): - """generic lockbox object, no implementation-dependent details here - - A lockbox defines one model of the physical system that is controlled.""" +all_models = OrderedDict([(model.name, model) for model in Model.__subclasses__()]) - def __init__(self, rp): - self.logger = logging.getLogger(name=__name__) - self.rp = rp - # make input and output signals - self._makesignals() - # find and setup the model - self.model = getmodel(self.c.model.modeltype)(self) - self.model.setup() - - def _makesignals(self, *args, **kwargs): - """ Instantiates all signals from config file. - Optional arguments are passed to the signal class initialization. """ - signalclasses, signalparameters = self._signalinit - for signaltype, signalclass in signalclasses.items(): - # generalized version of: self.inputs = [reflection, transmission] - signaldict = OrderedDict() - self.__setattr__(signaltype, signaldict) - for k in self.c[signaltype].keys(): - self.logger.debug("Creating %s signal %s...", signaltype, k) - # generalization of: - # self.reflection = Signal(self.c, "inputs.reflection") - signal = signalclass(self.c, - signaltype+"."+k, - **signalparameters) - signaldict[k] = signal - self.__setattr__(k, signal) +class Lockbox(SoftwareModule): + """ + A Module that allows to perform feedback on systems that are well described by a physical model. + """ + name = 'lockbox' + gui_attributes = ["model", "default_sweep_output"] + model = SelectProperty(options=all_models.keys()) + default_sweep_output = SelectProperty(options=["dummy"]) + + def init_module(self): + self.outputs = [] + self._asg = None @property - def signals(self): - """ returns a dictionary containing all signals, i.e. all inputs and - outputs """ - sigdict = dict() - signals, _ = self._signalinit - for s in signals.keys(): - sigdict.update(self.__getattribute__(s)) - return sigdict \ No newline at end of file + def asg(self): + if self._asg==None: + self._asg = self.pyrpl.asgs.pop(self.name) + return self._asg + + def sweep(self, output=None): + """ + Performs a sweep of one of the output. If no output is specified, the default sweep_output is used. + """ + self.unlock + if output is None: + output = self.default_sweep_output + self._asg.output = output + + def add_output(self): + """ + Outputs of the lockbox are added dynamically (for now, inputs are defined by the model). + """ + output = OutputSignal(self) + self.outputs.append(output) + setattr(self, output.name, output) + self.__class__.default_sweep_output.change_options([output.name for output in self.outputs]) + + def unlock(self): + for output in self.outputs: + output.unlock() + + def get_model(self): + return all_models[self.model](self) + + def model_changed(self): + for output in self.outputs: + output.update_for_model() + self.inputs = self.get_model() \ No newline at end of file diff --git a/pyrpl/software_modules/lockbox/model.py b/pyrpl/software_modules/lockbox/model.py index eb0d7d90e..e70322c1d 100644 --- a/pyrpl/software_modules/lockbox/model.py +++ b/pyrpl/software_modules/lockbox/model.py @@ -1,588 +1,20 @@ -import threading +from pyrpl.modules import SoftwareModule +from .signals.input import InputDirect, InputPdh -import scipy -from pyrpl.software_modules.lockbox.signal import * - -from pyrpl import fitting -from pyrpl import pyrpl_utils - -logger = logging.getLogger(name=__name__) - -class Model(object): - """ A generic model object that makes smart use of its inputs and outputs. - This is the baseclass for all other models, such as interferometer, - fabryperot and custom ones. - - Parameters - ---------- - parent: Pyrpl - The pyrpl object that instantiates this model. The model will - retrieve many items from the pyrpl object, such as the redpitaya - instance and various signals. It will also create new attributes in - parent to provide the most important API functions that the model - allows. +class Model(SoftwareModule): """ - gui_buttons = ["unlock", "sweep", "calibrate_all", "lock", - "save_all_gains"] # These are - # used - # to generate - # buttons in the gui - - export_to_parent = ["sweep", "calibrate", "save_current_gain", - "unlock", "islocked", "lock", "help", "calib_lock", - "_lock", "get_offset"] - - # independent variable that specifies the state of the system - _variable = 'x' - - def __init__(self, parent=None): - self.logger = logging.getLogger(__name__) - self.current_stage = 'UNLOCK' - if parent is None: - self._parent = self - else: - self._parent = parent - self.inputs = self._parent.inputs - self.outputs = self._parent.outputs - self.signals = self._parent.signals - self._config = self._parent.c.model - self._make_helpers() - self.state = {'actual': {self._variable: 0}, - 'set': {self._variable: 0}} - - def setup(self): - """ sets up all signals """ - for signal in self.signals.values(): - try: - params = signal._config.setup._dict - except KeyError: - params = dict() - try: - setupfn = self.__getattribute__("setup_"+signal._name) - except AttributeError: - self.logger.debug("No signal setup function setup_%s was " - "found", signal._name) - continue - else: - self.logger.debug("Calling setup_%s!", signal._name) - try: - setupfn(**params) - except TypeError: # means the setup function doesnt take - # params - setupfn() - - def _derivative(self, func, x, n=1, args=()): - return scipy.misc.derivative(func, - x, - dx=1e-9, - n=n, - args=args, - order=3) - - def _inverse(self, func, y, x0, args=()): - """ - Finds a solution x to the equation y = func(x) in the - vicinity of x0. - - Parameters - ---------- - func: function - the function - y: float or np.array(,dtype=float) - the desired value of the function - x0: float - the starting point for the search - args: tuple - optional arguments to pass to func - - Returns - ------- - x: float - the solution. None if no inverse could be found. - """ - try: - inverse = [self._inverse(func, yy, x0, args=args) for yy in y] - if len(inverse) == 1: - return inverse[0] - else: - return inverse - except TypeError: - def myfunc(x, *args): - return func(x, *args) - y - solution, infodict, ier, mesg = scipy.optimize.fsolve( - myfunc, - x0, - args=args, - xtol=1e-6, - epsfcn=1e-8, - fprime=self.__getattribute__(func.__name__+'_slope'), - full_output=True) - if ier == 1: # means solution was found - return solution[0] - else: - return None - - # helpers that create inverse and slope function of the model functions - # corresponding to input signals - def _make_slope(self, fn): - def fn_slope(x, *args): - return self._derivative(fn, x, args=args) - return fn_slope - - def _make_inverse(self, fn): - def fn_inverse(y, x0, *args): - return self._inverse(fn, y, x0, args=args) - return fn_inverse - - def _make_helpers(self): - # create any missing slope and inverse functions - for input in self.inputs.values(): - # test if the slope was defined in the model - if not hasattr(self, input._name+"_slope"): - self.logger.debug("Making slope function for input %s", - input._name) - fn = self.__getattribute__(input._name) - # bug removed a la - # http://stackoverflow.com/questions/3431676/creating-functions-in-a-loop - self.__setattr__(input._name+"_slope", - self._make_slope(fn)) - if not hasattr(self, input._name + "_inverse"): - self.logger.debug("Making inverse function for input %s", - input._name) - fn = self.__getattribute__(input._name) - self.__setattr__(input._name + "_inverse", - self._make_inverse(fn)) - - @property - def variable(self): - """ returns an estimate of the variable defined in _variable """ - inputname, input = self.inputs.items()[0] - input._acquire() # make sure data is fresh - act = input.mean - set = self.state["set"][self._variable] - variable = self.__getattribute__(inputname+'_inverse')(act, set) - # save in state buffer - self.state["actual"][self._variable] = variable - if variable is not None: - return variable - else: - logger.warning("%s could not be estimated. Run a calibration!", - self._variable) - return None - - def islocked(self): - """ returns True if locked, else False """ - if hasattr(self, self._variable): - variable = self.__getattribute__(self._variable) - else: - variable = self.variable - diff = variable - self.state["set"][self._variable] - # first check if parameter error exceeds threshold - if abs(diff) > self._config.lock.error_threshold: - return False - else: - # test for output saturation - for o in self.outputs.values(): - if o.issaturated: - return False - # lock seems ok - return True - - def unlock(self, ival=True): - """ unlocks the system""" - if hasattr(self, '_relocktimer'): # stop relock timer if applicable - self._relocktimer.stop() - for o in self.outputs.values(): - o.unlock(ival=ival) - self.current_stage = "UNLOCK" - - def sweep(self): - """ - Enables the pre-configured sweep on all outputs. - - Returns - ------- - duration: float - The duration of one sweep period, as it is useful to setup the - scope. - """ - - self.unlock() - frequency = None - for o in self.outputs.values(): - frequency = o.sweep() or frequency - self.current_stage = "SWEEP" - return 1.0 / frequency - - def _lock(self, input=None, factor=1.0, offset=None, outputs=None, - _savegain=False, **kwargs): - """ - Locks all outputs to input. - - Parameters - ---------- - input: Signal - the input signal that provides the error signal - factor: float - optional gain multiplier for debugging - offset: float or None - offset to start locking from. Not touched upon if None - outputs: list or None - if None, all outputs with lock configuration are enabled. - if list of RPOutputSignal, only the specified outputs are touched. - _savegain: bool - option for automatic gain configuration, leave False - kwargs must contain a pair _variable = setpoint, where _variable - is the name of the variable of the model, as specified in the - class attribute _variable. - - Returns - ------- - None - """ - - if kwargs: - self.state["set"].update(kwargs) - self.state["set"]["factor"] = factor - if input is None: - input = self.inputs.values()[0] - elif isinstance(input, str): - input = self.inputs[input] - inputname = input._name - variable = kwargs.pop(self._variable) - setpoint = self.__getattribute__(inputname)(variable) - slope = self.__getattribute__(inputname+'_slope')(variable) - # setpoint, slope come in 'units' of the input. Convert to V - input_unit_per_V = input._config[input._config.unit+'_per_V'] - setpoint /= input_unit_per_V - slope /= input_unit_per_V - - # trivial lock algorithm: just enable all gains - if outputs is None: - outputs = self.outputs.values() - # unlock all unused outputs, but leave ival unaffected - for o in [op for op in self.outputs.values() if op not in outputs]: - o.unlock(ival=False) - # engage lock on all desired outputs - for o in outputs: - if not isinstance(o, RPOutputSignal): - o = self.outputs[o] - # get unit of output calibration factor - unit = o._config.calibrationunits.split("_per_V")[0] - # get calibration factor - variable_per_unit = self.__getattribute__(self._variable - + "_per_" + unit) - if not _savegain: - # enable lock of the output - o.lock(slope=slope*variable_per_unit, - setpoint=setpoint, - input=input, - offset=offset, - factor=factor, - **kwargs) - else: # special option: instead of locking, write the gain - o.save_current_gain(slope=slope*variable_per_unit) - - def save_all_gains(self): - """see save_current_gains, no kwds for gui integration""" - self.save_current_gain() - - def save_current_gain(self, outputs=None): - """ saves the current gain setting as default one (for all outputs - unless a list of outputs is given, similar to _lock) """ - self._lock(outputs=outputs, _savegain=True) - - def stage_changed_hook(self, new_stage): - """Overwrite or monkey patch this function for custom action upon - new stage""" - pass - - def lock(self, - factor=None, - firststage=None, - laststage=None, - thread=False, - **kwargs): - ### This function is almost a one-to-one duplicate of FabryPerot.lock ( - # except for the **kwargs that is read online). This is a major - # source of bug !!!! - - # firststage will allow timer-based recursive iteration over stages - # i.e. calling lock(firststage = nexstage) from within this code - stages = self._config.lock.stages._keys() - if firststage: - if not firststage in stages: - self.logger.error("Firststage %s not found in stages: %s", - firstage, stages) - else: - stages = stages[stages.index(firststage):] - for stage in stages: - self.logger.debug("Lock stage: %s", stage) - self.current_stage = stage - self.stage_changed_hook(stage) # Some hook function - if stage.startswith("call_"): - try: - lockfn = self.__getattribute__(stage[len('call_'):]) - except AttributeError: - logger.error("Lock stage %s: model has no function %s.", - stage, stage[len('call_'):]) - raise - else: - # use _lock by default - lockfn = self._lock - parameters = dict(factor=factor) - if self._variable in kwargs: - parameters[self._variable] = kwargs[self._variable] - parameters.update((self._config.lock.stages[stage])) - try: - stime = parameters.pop("time") - except KeyError: - stime = 0 - if stage == laststage or stage == stages[-1]: - if self._variable in kwargs and kwargs[self._variable]: - parameters[self._variable] = kwargs[self._variable] - if factor: - parameters['factor'] = factor - try: - return lockfn(**parameters) - except TypeError: # function doesnt accept kwargs - raise - return lockfn() - else: - if thread: - # immediately execute current step (in another thread) - t0 = threading.Timer(0, - lockfn, - kwargs=parameters) - t0.start() # bug here: lockfn must accept kwargs - # and launch timer for nextstage - nextstage = stages[stages.index(stage) + 1] - parameters = dict(factor=factor, - firststage=nextstage, - laststage=laststage, - thread=thread) - if self._variable in kwargs and kwargs[self._variable]: - parameters[self._variable] = kwargs[self._variable] - t1 = threading.Timer(stime, - self.lock, - kwargs=parameters) - t1.start() - return None - else: - try: - lockfn(**parameters) - except TypeError: # function doesnt accept kwargs - lockfn() - pyrpl_utils.sleep(stime) ## Changed to pyrpl_utils.sleep, - # which basically doesn't freeze the gui - - def relock(self, *args, **kwargs): - """ executes 'lock' until 'islocked' returns true """ - while not self.islocked(): - self.lock(*args, **kwargs) - - def autorelock(self, timeout=1.0): - """ sets up a timer that periodically calls relock() """ - if not hasattr(self, '_relocktimer'): - from PyQt4.QtCore import QTimer - self._relocktimer = QTimer() - try: - self._relocktimer.disconnect() - except TypeError: - pass - self._relocktimer.timeout.connect(self.relock) - self._relocktimer.setSingleShot(False) - self._relocktimer.start(int(timeout*1000)) - - def stop_autolock(self): - self.timer.stop() - - def calibrate_all(self): - """ - When connecting a function to QPushButton.clicked, keyword arguments - are filled with a boolean value. So we need a function with no extra - kwds - :return: - """ - return self.calibrate() - - def calibrate(self, inputs=None, scopeparams={}): - """ - Calibrates by performing a sweep as defined for the outputs and - recording and saving min and max of each input. - - Parameters - ------- - inputs: list - list of input signals to calibrate. All inputs are used if None. - scopeparams: dict - optional parameters for signal acquisition during calibration - that are temporarily written to _config - - Returns - ------- - curves: list - list of all acquired curves - """ - - self.unlock() - duration = self.sweep() - curves = [] - if not inputs: - inputs = self.inputs.values() - for input in inputs: - if not isinstance(input, Signal): - input = self.inputs[input] - try: - input._config._data["trigger_source"] = "asg1" - input._config._data["duration"] = duration - input._config._data.update(scopeparams) - input._acquire() - # when signal: autosave is enabled, each calibration will - # automatically save a curve - curve, ma, mi, mean, rms = input.curve, input.max, input.min, \ - input.mean, input.rms - curves.append(curve) - # add sweep phase at scope trigger to retrieve the scan - # direction afterwards - for o in self.outputs.values(): - if 'sweep' in o._config._keys(): - curve.params[o._name+'.sweep_triggerphase'] = \ - o.sweep_triggerphase - curve.save() - try: - secondsignal = scopeparams["secondsignal"] - input2 = self.signals[secondsignal] - curve2 = input2.curve - curve.add_child(curve2) - self.logger.debug("Secondsignal %s successfully acquired.", - secondsignal) - except KeyError: - self.logger.debug("No secondsignal was specified for %s", - input._name) - finally: - # make sure to reload config file here so that the modified - # scope parameters are not written to config file - self._parent.c._load() - # save all parameters to config - input._config["max"] = ma - input._config["min"] = mi - input._config["mean"] = mean - input._config["rms"] = rms - input._config["curve"] = curve.pk - # turn off sweeps - self.unlock() - return curves - - def calib_lock(self): - """ shortcut to call calibrate(), lock() and return islocked()""" - self.calibrate() - self.lock() - return self.islocked() - - def setup_iq(self, inputsignal='iq', **kwargs): - """ - Sets up an input signal derived from demodultaion of another input. - The config file must contain an input signal named like the the - parameter input with a section 'setup' whose entries are directly - passed to hardware_modules.IQ.setup(). - - Parameters - ---------- - input: str - kwargs: dict - optionally override config files setup section by passing - the arguments as kwargs here - - Returns - ------- - None - """ - if not isinstance(inputsignal, Signal): - input = self.inputs[inputsignal] - else: - input = inputsignal - if not kwargs: - kwargs = input._config.setup._dict - if 'iq' in kwargs: # we can request a particular iq number if needed - input.iq = self._parent.rp.__getattribute__(kwargs.pop('iq')) - elif not hasattr(input , 'iq'): - input.iq = self._parent.rp.iqs.pop() - input.iq.setup(**kwargs) - input._config['redpitaya_input'] = input.iq.name - - def help(self): - """ provides some help to get started. """ - self.logger.info("PyRP Lockbox\n-------------------\n" - + "Usage: \n" - + "Create Pyrpl object: p = Pyrpl('myconfigfile')\n" - + "Turn off the laser and execute: \n" - + "p.get_offset()\n" - + "Turn the laser back on and execute:\n" - + "p.calibrate()\n" - + "(everytime power or alignment has changed). Then: " - + "p.lock(factor=1.0)\n" - + "The device should be locked now. Play \n" - + "with the value of factor until you find a \n" - + "reasonable lock performance and save this as \n" - + "the new default with p.save_current_gain(). \n" - + "Now simply call p.lock() to lock. \n" - + "Assert if locked with p.islocked() and unlock \n" - + "with p.unlock(). ") - - def fit(self, input, manualfit=True, **extra): - """ attempts a fit of input's last calibration curve with the input's - model""" - if not isinstance(input, Signal): - input = self.inputs[input] - signalfn = self.__getattribute__(input._name) - c = CurveDB.get(input._config.curve) - data = c.data - t = c.data.index.values - def fitfn(variable_per_time, t0, offset, scale, **kwds): - variables = (t-t0) * variable_per_time - return np.array(offset + scale * signalfn(variables, **kwds), - dtype=np.double) - # a very naive guess - should be refined with 'input_guess' function - guess = {'variable_per_time': 10.0 / (t.max() - t.min()), - 't0': 0, - 'offset': 0, - 'scale': 1.0} - guess.update(extra) - try: - guessfn = self.__getattribute__(input._name + '_guess') - except AttributeError: - self.logger.warning("No function %s to guess fit " - "parameters is defined. Writing one will " - "improve fit performance. ", - input._name + '_guess') - else: - guess.update(guessfn()) - fitter = fitting.Fit(data, fitfn, manualguess_params=guess, - fixed_params={'offset': 0}, - graphicalfit=manualfit, autofit=True) - fitcurve = CurveDB.create(fitter.fitdata, name='fit_'+input._name) - fitcurve.params.update(fitter.getparams()) - try: - postfn = self.__getattribute__(input._name + '_postfit') - except AttributeError: - self.logger.warning("No function %s to use fit " - "parameters is defined. Writing one will " - "improve calibration results. ", - input._name + '_postfit') - else: - fitcurve.params.update(postfn()) - fitcurve.save() - c.add_child(fitcurve) - return fitcurve + A physical model allowing to relate inputs in V into a physical parameter in *unit*. Several units are actually + available to describe the model parameter (e.g. 'm', 'MHz' for detuning). + inputs is a list of signal.Input objects (or derived classes such as signal.PDH) + """ + parameter_name = "" + units = [] # possible units to describe the physical param# eter to control e.g. ['m', 'MHz'] + inputs = [] # list of input signals that can be implemented - def get_offset(self): - """ Execute this function to record the offsets for all input - signals. If signal.offset_subtraction is true in the config file, - the signal value 0 will from then on correspond to the measured - offset. Before any locking configuration, this function should be - executed in order to take the analog offsets of redpitaya inputs - into account. """ - for input in self.inputs.values(): - input.get_offset() +class DummyModel(Model): + name = "dummy" + units = ['m', 'MHz'] + pdh = InputPdh + reflection = InputDirect + transmission = InputDirect \ No newline at end of file diff --git a/pyrpl/software_modules/lockbox/signal.py b/pyrpl/software_modules/lockbox/signal.py deleted file mode 100644 index d5db2ff0d..000000000 --- a/pyrpl/software_modules/lockbox/signal.py +++ /dev/null @@ -1,910 +0,0 @@ -import time -import logging -import numpy as np - -from pyrpl import bodefit -from pyrpl import CurveDB - -logger = logging.getLogger(__name__) - - -class ExposedConfigParameter(object): - def __init__(self, parameter): - self._parameter = parameter - - def __get__(self, instance, owner): - return instance._config.__getattribute__(self._parameter) - - def __set__(self, instance, value): - instance._config.__setattr__(self._parameter, value) - - -class Signal(object): - """" represention of a physial signal - - A predefined number of samples is aquired when any signal-related property - is requested (except for sample which always gets a simultaneous datapoint. - If several properties are requested simultaneously, all are derived from the - same data trace. This is done by setting a timeout within - which the signals can be requested, roughly corresponding to the required - acquisition time. The recommended syntax for simultaneous acquisition is: - mean, rms = signal.mean, signal.rms - - Parameters - ---------- - config: MemoryBranch - Any memorybranch of the MemoryTree object corresponding to the - config file that defines this signal. - branch: str - The branch name that defines the signal. - Example: "mysignals.myinputs.myinput" - """ - def __init__(self, config, branch): - # get the relevant config branch from config tree - self._config = config._getbranch(branch, defaults=config.signal) - # signal name = branch name - self._name = self._config._branch - self._acquiretime = 0 - - # The units in which the signal will be calibrated - unit = ExposedConfigParameter("unit") - - def __repr__(self): - return str(self.__class__)+"("+self._name+")" - - @property - def unit_per_V(self): - """ The factor to convert this signal from Volts to the units as - specified in the config file. Setting this property will affect the - config file. """ - return self._config[self.unit + "_per_V"] - - @unit_per_V.setter - def unit_per_V(self, value): - self._config[self.unit + "_per_V"] = value - - # placeholder for acquired data implementation, in default units (V) - def _acquire(self): - """ - Acquires new data for the signal. Automatically called - once the buffered data are older than the timeout specified in the - signal configuration, but it can often be useful to manually enforce - a new acquisition through this function. - - Returns - ------- - None - """ - logger.debug("acquire() of signal %s was called! ", self._name) - # get data - self._lastvalues = np.random.normal(size=self._config.points) - self._acquiretime = time.time() - - # placeholder for acquired timetrace implementation - @property - def _times(self): - return np.linspace(0, - self._config.traceduration, - self._config.points, - endpoint=False) - @property - def nyquist_frequency(self): - """ Returns the nyquist frequency of the predefined measurements - such as mean, rms, curve, min, max """ - return self._config.points / self._config.traceduration / 2 - - # placeholder for acquired data sample implementation (faster) - @property - def sample(self): - """ Returns one most recent sample of the signal. Does not rely on - self._acquire. """ - return (np.random.normal() - self.offset) * self.unit_per_V - - # derived quantities from here on, no need to modify in derived class - @property - def _values(self): - # has the timeout expired? - if self._acquiretime + self._config.acquire_timeout < time.time(): - # take new data then - self._acquire() - # return scaled numbers (slower to do it here but nicer code) - return (self._lastvalues - self.offset) * self.unit_per_V - - @property - def mean(self): - """ Returns the mean of the last signal acquisition """ - return self._values.mean() - - @property - def rms(self): - """ Returns the rms of the last signal acquisition """ - return np.sqrt(((self._values - self._values.mean())**2).mean()) - - @property - def max(self): - """ Returns the maximum of the last signal acquisition """ - return self._values.max() - - @property - def min(self): - """ Returns the minimum of the last signal acquisition """ - return self._values.min() - - @property - def curve(self): - """ returns a curve with recent data and a lot of useful parameters""" - # important: call _values first in order to get the _times - # corresponding to the measurement setup of _values - values = self._values - times = self._times - return CurveDB.create(times, values, - name=self._name, - mean=self.mean, - rms=self.rms, - max=self.max, - min=self.min, - average=self._config.average, - unit=self.unit, - unit_per_V=self.unit_per_V, - nyquist_frequency=self.nyquist_frequency, - acquiretime=self._acquiretime, - autosave=self._config.autosave) - - def get_offset(self): - """ acquires and saves the offset of the signal """ - oldoffset = self.offset - # make sure data are fresh - self._acquire() - self._config["offset"] = self._lastvalues.mean() - newoffset = self.offset - logger.debug("Offset for signal %s changed from %s to %s", - self._name, oldoffset, newoffset) - return newoffset - - @property - def offset(self): - if self._config.offset_subtraction: - return self._config.offset or 0 - else: - return 0 - - def get_peak(self): - # make sure data are fresh - self._acquire() - self._config["peak"] = self.mean - - logger.debug("New peak value for signal %s is %s", - self._name, self._config.offset) - - @property - def peak(self): - """ peak signal value, as present when get_peak was last called""" - return self._config.peak - - @property - def transfer_function(self): - try: - pk = self._config.transfer_function.open_loop - except KeyError: - logger.error("No transfer functions available for this output") - return None - return CurveDB.get(pk) - - -class RPSignal(Signal): - """ - A Signal that corresponds to an inputsignal of the DSPModule inside - the RedPitaya - - Parameters - ---------- - config: MemoryBranch - Any memorybranch of the MemoryTree object corresponding to the - config file that defines this signal. - branch: str - The branch name that defines the signal. - Example: "mysignals.myinputs.myinput" - parent: Pyrpl - The Pyrpl object hosting this signal. In principle, any object - containing an attribute '_rp' referring to a RedPitaya object can be - parent. - restartscope: function - The function that the signal calls after acquisition to reset the - scope to its orignal state. - """ - def __init__(self, config, branch, parent, restartscope=lambda: None): - self._parent = parent - self._rp = parent.rp - self._restartscope = restartscope - super(RPSignal, self).__init__(config, branch) - - @property - def redpitaya_input(self): - """ - Returns - ------- - input: str - The DSPModule name of the input signal corresponding to this - signal in the redpitaya """ - return self._config.redpitaya_input - - def _saverawdata(self, data, times): - self._lastvalues = data - self._lasttimes = times - self._acquiretime = time.time() - - def _acquire(self, secondsignal=None): - """ - Acquires new data for the signal. Automatically called - once the buffered data are older than the timeout specified in the - signal configuration, but it can often be useful to manually enforce - a new acquisition through this function. - - Parameters - ---------- - secondsignal: Signal or str - Signal or name of signal that should be recorded simultaneously - with this signal. The result will be directly stored in the second - signal and can be retrieved through properties like curve or - mean of the second signal. - - Returns - ------- - None - """ - logger.debug("acquire() of signal %s was called! ", self._name) - if secondsignal is None: - try: - secondsignal = self._config.secondsignal - except KeyError: - pass - if isinstance(secondsignal, str): - secondsignal = self._parent.signals[secondsignal] - if secondsignal is not None: - input2 = secondsignal.redpitaya_input - logger.debug("Second signal '%s' for acquisition set up.", - secondsignal) - else: - input2 = None - self._rp.scope.setup(duration=self._config.duration, - trigger_source=self._config.trigger_source, - average=self._config.average, - threshold=self._config.threshold, - hysteresis=self._config.hysteresis, - trigger_delay=self._config.trigger_delay, - input1=self.redpitaya_input, - input2=input2) - try: - timeout = self._config.timeout - except KeyError: - timeout = self._rp.scope.duration*5 - self._saverawdata(self._rp.scope.curve(ch=1, timeout=timeout), - self._rp.scope.times) - if secondsignal is not None: - secondsignal._saverawdata(self._rp.scope.curve(ch=2, timeout=-1), - self._rp.scope.times) - self._restartscope() - - @property - def _times(self): - return self._lasttimes#self._rp.scope.times - - @property - def sample(self): - """ Returns a single sample of the signal""" - self._rp.scope.input1 = self.redpitaya_input - return (self._rp.scope.voltage1 - self.offset) * self.unit_per_V - - @property - def nyquist_frequency(self): - """ Returns the nyquist frequency of the predefined measurements - such as mean, rms, curve, min, max """ - return 1.0 / self._rp.scope.sampling_time / 2 - - @property - def curve(self): - """ Returns a CurveDB object with the last result of _acquitision """ - curve = super(RPSignal, self).curve - extraparams = dict( - trigger_timestamp=self._rp.scope.trigger_timestamp, - duration=self._rp.scope.duration) - curve.params.update(extraparams) - curve.save() - return curve - - def fit(self, *args, **kwargs): - """ shortcut to the function fit of the underlying model """ - self._parent.fit(input=self, *args, **kwargs) - - -class RPOutputSignal(RPSignal): - """ - A Signal that drives an output of the RedPitaya. It shares all - properties of RPSignal (an input signal), but furthermore reserves a PID - module to forward its input to an output of the redpitaya. Proper - configuration in the config file leads to almost automatic locking - behaviour: If the output signal knows its transfer function, it can - adjust the PID parameters to provide an ideal proportional or integral - transfer function. - - Parameters - ---------- - config: MemoryBranch - Any memorybranch of the MemoryTree object corresponding to the - config file that defines this signal. - branch: str - The branch name that defines the signal. - Example: "mysignals.myinputs.myinput" - parent: Pyrpl - The Pyrpl object hosting this signal. In principle, any object - containing an attribute '_rp' referring to a RedPitaya object can be - parent. - restartscope: function - The function that the signal calls after acquisition to reset the - scope to its orignal state. - """ - def __init__(self, config, branch, parent, restartscope): - super(RPOutputSignal, self).__init__(config, - branch, - parent, - restartscope) - self.setup() - - def setup(self, **kwargs): - """ - Sets up the output according to the config file specifications. - - Parameters - ---------- - kwargs: dict - Not used here, but present to ease overwriting in derived signal - classes since the API typically passes all setup parameters of the - signal here. - - Returns - ------- - None - """ - # each output gets its own pid - if not hasattr(self, "pid"): - self.pid = self._rp.pids.pop() - - # set voltage limits - try: - self.pid.max_voltage = self._config.max_voltage - except KeyError: - self._config["max_voltage"] = self.pid.max_voltage - try: - self.pid.min_voltage = self._config.min_voltage - except KeyError: - self._config["min_voltage"] = self.pid.min_voltage - - # routing of output - try: - out = self._config.redpitaya_output - except KeyError: - logger.error("Output port for signal signal %s could not be " - + "identified.", self._name) - raise - if out.startswith("pwm"): - out = self._rp.__getattribute__(out) - out.input = self.pid.name - self.pid.output_direct = "off" - else: - self.pid.output_direct = out - - # is a second pid needed? - if self._pid2_filter or self._second_integrator_crossover > 0: - # if we already have a pid2, let's return it to the stack first - if hasattr(self, 'pid2'): - self.pid2.p = 0 - self.pid2.i = 0 - self.pid2.d = 0 - self.pid2.ival = 0 - self._rp.pids.append(self.pid2) - # self.pid has been configured on the output side. Therfore we - # rename it to pid2 and create a new self.pid for the intput - # configuration to be compatible with the single-pid code. - self.pid2 = self.pid - self.pid = self._rp.pids.pop() - logger.debug("Second PID %s acquired for output %s.", - self.pid.name, self._name) - self.pid2.inputfilter = self._pid2_filter - self.pid2.input = self.pid.name - self.pid.max_voltage = 1 - self.pid.min_voltage = -1 - self.pid.p = 1.0 - - # configure inputfilter - try: - self.pid.inputfilter = self._config.lock.inputfilter - except (KeyError, AttributeError): - logger.debug("No inputfilter was defined for output %s. ", - self._name) - - # save the current inputfilter to the config file - try: - self._config["lock"]["inputfilter"] = self._inputfilter - except KeyError: - self._config["lock"] = {"inputfilter": self._inputfilter} - - # set input to off - self.pid.input = "off" - - # configure iir if desired - self.setup_iir() - - # make sure the units of calibration make sense - if not 'calibrationunits' in self._config._keys(): - calibrations = [k for k in self._config._keys() if k.endswith("_per_V")] - if not calibrations or len(calibrations) > 1: - raise ValueError("Too few / too many calibrations for output " - +self._name+" are specified in config file: " - +str(calibrations) - +". Remove all but one to continue.") - else: - self._config['calibrationunits'] = calibrations[0] - - @property - def issaturated(self): - """ - Returns - ------- - - True: if the output has saturated - False: otherwise - """ - ival, max, min = self.pid.ival, self.pid.max_voltage, \ - self.pid.min_voltage - if ival > max or ival < min: - return True - else: - return False - - def off(self, ival=True): - """ Turns off all feedback gains, sweeps and sets the offset to - zero. If ival is false, ival is not included here""" - self.pid.p = 0 - self.pid.i = 0 - self.pid.d = 0 - if ival: - self.pid.ival = 0 - if hasattr(self, 'pid2'): - self.pid2.i = 0 - self.pid2.p = 1 - self.pid2.d = 0 - if ival: - self.pid2.ival = 0 - - def unlock(self, ival=True): - """ Turns the signal lock off if the signal is used for locking """ - if self._skiplock: - return - else: - self.off(ival=ival) - - def lock(self, - slope, - setpoint=None, - input=None, - factor=1.0, - offset=None, - second_integrator=0, - setup_iir=False, - skipskip=False): - """ - Enables feedback with this output. The realized transfer function of - the pid plus specified external analog filters is a pure integrator if - the config file defines unity_gain_frequency for the output, or a pure - proportional gain if the config file defines proportional_gain for the - output. This transfer function can be further refined by - setting the fields 'inputfilter' and 'iir' for the output in the config - file. An incorrect specification of the external analog filter will - result in an imperfect transfer function. - - Parameters - ---------- - slope: float or None - The slope of the input error signal. If the output is specified in - units of m_per_V, the slope must come in units of V_per_m. None - leaves the current slope unchanged (also ignores factor). - setpoint: float or None - The lock setpoint in V. None leaves the setpoint unchanged - input: RPSignal oror None - The input signal of the pid, either as RPSignal object. None - leaves the currend pid input. - factor: float - An extra factor to multiply the gain with for debugging purposes. - offset: float or None - The output offset (V) when the lock is enabled. - second_integrator: float - Factor to multiply a predefined second integrator gain with. Useful - for ramping up the second integrator in a smooth fashion. - setup_iir: bool - If True, no iir filter is set up. Usually, it is enough to - switch on the iir filter only in the final step. This results in a - gain in speed and avoids saturation of internal degrees of - freedom of the IIR. - skipskip: bool - overrides a 'skip' configuration of the output in case it is set - Returns - ------- - None - """ - - # if output is disabled for locking, skip the rest - if self._skiplock and not skipskip: - return - - # normalize slope to our units - slope *= self._config[self._config.calibrationunits] - - # design the loop shape - loopshape = self._config.lock._dict # maybe rename the branch to loopshape - if ("unity_gain_frequency" in loopshape - and "proportional_gain" in loopshape): - raise ValueError("Output " + self._name + " loopshape is " - "overdefined. Defines either unity_gain_frequency or " - "proportional_gain, but not both!") - if slope is None: - if "unity_gain_frequency" in loopshape: - integrator_on = True - gain = self.pid.i - else: # "proportional" in loopshape: - integrator_on = False - gain = self.pid.p - elif slope == 0: - raise ValueError("Cannot lock on a zero slope!") - else: - if "unity_gain_frequency" in loopshape: - integrator_on = True - gain = self._config.lock.unity_gain_frequency - else: # "proportional" in loopshape: - integrator_on = False - gain = self._config.lock.proportional_gain - gain *= factor * -1 / slope - - # if gain is disabled somewhere, return - if gain == 0: - self.off() - logger.warning("Lock called with zero gain! ") - return - - # if analog lowpass filters are present, try to adjust transfer - # function in order to compensate for it with PID - try: - lowpass = sorted(self._config.analogfilter.lowpass) - except KeyError: - self._config['analogfilter']= {'lowpass': []} - lowpass = sorted(self._config.analogfilter.lowpass) - if integrator_on: - integrator_ugf = gain - else: - integrator_ugf = 0 - # pretending there was a 1Hz analog filter will get us the right - # proportional gain with or without integrator in the next block - lowpass = [1.0] + lowpass - - if len(lowpass) >= 0: # i.e. always - # no analog lowpass -> pure integrator lock - proportional = 0 - differentiator_ugf = 0 - if len(lowpass) >= 1: - # set PI corner at first lowpass cutoff - proportional = gain / lowpass[0] - if len(lowpass) >= 2: - # set PD corner at second lowpass cutoff if present - differentiator_ugf = lowpass[1] / proportional - if len(lowpass) >= 3: - logger.warning("Output %s: Don't know how to handle >= 3rd order " - +"analog filter. Consider IIR design. ") - - if input is not None: - if not isinstance(input, RPSignal): - logger.error("Input %s must be a RPSignal instance.", input) - self.pid.input = input.redpitaya_input - # if iir was used, input may be on the iir at the moment - elif (self.pid.input == 'iir') and hasattr(self, "iir"): - self.pid.input = self.iir.input - - # get inputoffset - if input is None: - try: - inputbranch = self._config._root.inputs[self.pid.input] - except KeyError: - inputoffset = 0 - logger.warning("Inputbranch for %s wasn't found. No way to " - "correct for its offset. ", self.pid.input) - else: - if inputbranch.offset_subtraction: - inputoffset = inputbranch.offset - else: - inputoffset = 0 - else: - inputoffset = input.offset - - if offset: - # must turn off gains before setting the offset - self.off() - # offset is internal integral value - self.pid.ival = offset - # sleep for the lowest analog lowpass damping time to let the - # offset settle analogically - if lowpass: - time.sleep(1.0/lowpass[0]) - - # reset inputfilter - allows configuration from configfile in - # near real time - self.pid.inputfilter = self._config.lock.inputfilter - if hasattr(self, 'pid2'): - self.pid2.inputfilter = self._pid2_filter - - # setup iir filter if it is configured - this takes care of input - # signal routing. To be executed, set 'setup_iir: true' in the - # appropriate lock stage in the config file - if setup_iir: - self.setup_iir() - - # rapidly turn on all gains - if setpoint is None: - setpoint = self.pid.setpoint - else: - self.pid.setpoint = setpoint + inputoffset - self.pid.i = integrator_ugf - self.pid.p = proportional - self.pid.d = differentiator_ugf - if second_integrator != 0: - if hasattr(self, 'pid2'): - self.pid2.i = self._second_integrator_crossover\ - * second_integrator - self.pid2.p = 1.0 - - # set the offset once more in case the lack of synchronous gain enabling - # messed up the offset - if offset: - self.pid.ival = offset - if hasattr(self, 'pid2'): - self.pid2.ival = 0 - - # issue a warning if some gain could not be implemented - for act, set, name in [(self.pid.i, integrator_ugf, "integrator"), - (self.pid.p, proportional, "proportional"), - (self.pid.d, differentiator_ugf, "differentiator"), - (self.pid.setpoint, setpoint, "setpoint")]: - if set != 0 and (act / set < 0.9 or act / set > 1.1): - # the next condition is to avoid diverging quotients setpoints - # near zero - if not (name == 'setpoint' and abs(act-set) < 1e-3): - logger.warning("Implemented value for %s of output %s has " - + "saturated more than 10%% away from desired " - + "value. Try to modify analog gains.", - name, self._name) - - def save_current_gain(self, slope=1.0): - """ converts the current transfer function of the output into its - default transfer function by updating relevant settings in the - config file. - - Parameters - ---------- - slope: float - the slope that lock would receive at the current setpoint. - - Returns - ------- - None - """ - # normalize slope to our units - slope *= self._config[self._config.calibrationunits] - # save inputfilters - self._config.lock.inputfilter = self._inputfilter - # save new pid gains - newgains = {"p": self.pid.p, - "i": self.pid.i, - "d": self.pid.d} - # first take care of pid2 if it exists - if hasattr(self, "pid2"): - newgains['p'] *= self.pid2.p - if self.pid2.d != 0: - if newgains["d"] == 0: - newgains["d"] = self.pid2.d - else: - logger.error('Nonzero differential gain in pid2 ' - 'of output %s detected. No method ' - 'implemented to record this gain in the ' - 'config file. Please modify ' - 'RPOutputsignal.save_current_gain ' - 'accordingly! ') - if self.pid2.i != 0: - self._config.lock['second_integrator_crossover'] = \ - self.pid2.i / self.pid2.p - # now we only need to transcribe newgains into the config file - lowpass = [] - if newgains['i'] == 0: # means config file cannot have - # unity_gain_frequency entry - if "unity_gain_frequency" in self._config.lock._keys(): - self._config.lock._data.pop("unity_gain_frequency") - self._config.lock.proportional_gain = newgains["p"] * -1 * slope - else: - # remove possible occurrence of proportional_gain - if "proportional_gain" in self._config.lock._keys(): - self._config.lock._data.pop("proportional_gain") - self._config.lock.unity_gain_frequency = newgains["i"] * -1 * slope - if newgains['p'] != 0: - lowpass.append(newgains['i']/newgains['p']) - elif newgains['d'] != 0: # strange case where d != 0 and p ==0 - lowpass.append(1e20) - if newgains['d'] != 0: - lowpass.append(newgains['d'] * newgains['p']) - # save lowpass setting - self._config['analogfilter']['lowpass'] = lowpass - - - def sweep(self, frequency=None, amplitude=None, waveform=None): - """ If the signal configuration contains a sweep section, this one - is executed here to provide the predefined sweep at the output. """ - try: - kwargs = self._config.sweep._dict - except KeyError: - logger.debug("Sweep for output '%s' is disabled.", self._name) - return None - if frequency: - kwargs["frequency"] = frequency - if waveform: - kwargs["waveform"] = waveform - if amplitude: - kwargs["amplitude"] = amplitude - - # set asg amplitude always to 1.0 and feed sweep through pid instead - amplitude = kwargs["amplitude"] - kwargs["amplitude"] = 1.0 - kwargs["output_direct"] = "off" - if 'asg' in kwargs: - asgname = kwargs.pop("asg") - else: - asgname = 'asg1' - asg = self._rp.__getattribute__(asgname) - self._asg = asg # for future reference - asg.setup(**kwargs) - self.pid.input = asgname - self.pid.setpoint = 0 - self.pid.p = amplitude - return asg.frequency - - @property - def _sweep_triggerphase(self): - """ returns the last scopetriggerphase for the sweeping asg """ - return self._asg.scopetriggerphase - - def _analogfilter(self, frequencies): - tf = np.array(frequencies, dtype=np.complex)*0.0 + 1.0 - try: - lp = self._config.analogfilter.lowpass - except KeyError: - return tf - else: - for p in lp: - tf /= (1.0 + 1j * frequencies / p) - return tf - - @property - def sweep_triggerphase(self): - """ returns the last scopetriggerphase for the sweeping asg, - corrected for phase delay due to analog output filters of the output""" - if hasattr(self, '_asg') and self._asg.amplitude != 0: - f = self._asg.frequency - analogdelay = np.angle(self._analogfilter(f), deg=True) - return (self._sweep_triggerphase + analogdelay) % 360 - else: - return 0 - - @property - def output_offset(self): - """ The output offset of the output signal. At the moment simply a - pointer to self.pid.ival """ - return self.pid.ival - - @output_offset.setter - def output_offset(self, value): - self.pid.ival = value - offset = self.pid.ival - if offset > self._config.max_voltage: - offset = self._config.max_voltage - elif offset < self._config.min_voltage: - offset = self._config.min_voltage - self._config['lastoffset'] = offset - - @property - def _skiplock(self): - if 'lock' not in self._config._keys(): - return True - if 'skip' in self._config.lock._keys(): - if self._config.lock.skip: - return True - return False - - @property - def _pid2_filter(self): - # this is a method to get the pid2 filter coefficients - # future implementation might change where this value is stored - try: - return self._config.lock.inputfilter[len(self.pid.inputfilter):] - except KeyError: - return [] - - @property - def _inputfilter(self): - if hasattr(self, 'pid2'): - return self.pid.inputfilter + self.pid2.inputfilter - else: - return self.pid.inputfilter - - @property - def _second_integrator_crossover(self): - try: - sic = self._config.lock.second_integrator_crossover - except KeyError: - sic = 0 - return sic - - - @property - def redpitaya_input(self): - """ - Returns - ------- - input: str - The DSPModule name of the input signal corresponding to this - signal in the redpitaya """ - return self.pid.name - - def setup_iir(self, **kwargs): - """ - Inserts an iir filter before the output pid. For correct routing, - the pid input must be set correctly, as the iir filter will reuse - the pid input setting as its own input and send its output through - the pid. - - Parameters - ---------- - kwargs: dict - Any kwargs that are accepted by IIR.setup(). By default, - the output's iir section in the config file is used for these - parameters. - - Returns - ------- - None - """ - # load data from config file - try: - iirconfig = self._config.iir._dict - except KeyError: - logger.debug("No iir filter was defined for output %s. ", - self._name) - return - else: - logger.debug("Setting up IIR filter for output %s. ", self._name) - # overwrite defaults with kwargs - iirconfig.update(kwargs) - if 'curve' in iirconfig: - iirconfig.update(bodefit.iirparams_from_curve( - id=iirconfig.pop('curve'))) - else: - # workaround for complex numbers from yaml - iirconfig["zeros"] = [complex(n) for n in iirconfig.pop("zeros")] - iirconfig["poles"] = [complex(n) for n in iirconfig.pop("poles")] - # get module - if not hasattr(self, "iir"): - self.iir = self._rp.iirs.pop() - logger.debug("IIR filter retrieved for output %s. ", self._name) - # output_direct off, since iir goes through pid - iirconfig["output_direct"] = "off" - # input setting -> copy the pid input if it is not erroneously on iir - pidinput = self.pid.input - if pidinput != 'iir': - iirconfig["input"] = pidinput - # setup - self.iir.setup(**iirconfig) - # route iir output through pid - self.pid.input = self.iir.name diff --git a/pyrpl/widgets/attribute_widgets.py b/pyrpl/widgets/attribute_widgets.py index 5f977da7b..d65a14319 100644 --- a/pyrpl/widgets/attribute_widgets.py +++ b/pyrpl/widgets/attribute_widgets.py @@ -925,6 +925,8 @@ def update(self): val = [val] self.widget.set_list(val) + def set_max_cols(self, n_cols): + self.widget.set_max_cols(n_cols) class SelectAttributeWidget(BaseAttributeWidget): """ @@ -964,21 +966,54 @@ def write(self): :return: """ - setattr(self.module, self.name, str(self.widget.currentText())) - if self.acquisition_property: - self.value_changed.emit() + #if self.acquisition_property: + self.value_changed.emit() def update(self): """ Sets the gui value from the current module value - - :return: """ index = list(self.options).index(getattr(self.module, self.name)) self.widget.setCurrentIndex(index) + def change_options(self, new_options): + """ + The options of the combobox can be cahnged dynamically. new_options is a list of strings. + """ + self.widget.blockSignals(True) + self.defaults = new_options + self.widget.clear() + self.widget.addItems(new_options) + self.widget.blockSignals(False) + + + +#class DynamicSelectAttributeWidget(SelectAttributeWidget): +# """ +# Multiple choice property, with optiosn evaluated at run-time: +# the options in the combobox have to be filled upon click. +# """ +# def __init__(self, name, module): +# BaseAttributeWidget.__init__(self, name, module) # don' t do the SelectAttributeWidget initialization. +# +# def set_widget(self): +# """ +# Sets up the widget (here a QComboBox). +# """ +# self.widget = QtGui.QComboBox() +# self.widget.currentIndexChanged.connect(self.write) +# +# @property +# def options(self): +# """ +# All possible options. +# """ +# return getattr(self.module.__class__, self.name).options(self.module) +# +# def + class PhaseAttributeWidget(FloatAttributeWidget): pass diff --git a/pyrpl/widgets/module_widgets.py b/pyrpl/widgets/module_widgets.py index dff67042d..e9f388229 100644 --- a/pyrpl/widgets/module_widgets.py +++ b/pyrpl/widgets/module_widgets.py @@ -1398,4 +1398,64 @@ def restart_averaging(self): """ self.x_data = self.module.freqs() self.y_data = np.zeros(len(self.x_data)) - self.current_average = 0 \ No newline at end of file + self.current_average = 0 + + +#=============Lockbox widgets========================# +class OutputSignalWidget(ModuleWidget): + def init_gui(self): + self.main_layout = QtGui.QVBoxLayout() + self.setLayout(self.main_layout) + self.init_attribute_layout() + for widget in self.attribute_widgets.values(): + self.main_layout.removeWidget(widget) + self.upper_layout = QtGui.QHBoxLayout() + self.main_layout.addLayout(self.upper_layout) + self.col1 = QtGui.QVBoxLayout() + self.col2 = QtGui.QVBoxLayout() + self.col3 = QtGui.QVBoxLayout() + self.col4 = QtGui.QVBoxLayout() + self.upper_layout.addLayout(self.col1) + self.upper_layout.addLayout(self.col2) + self.upper_layout.addLayout(self.col3) + self.upper_layout.addLayout(self.col4) + + aws = self.attribute_widgets + self.col1.addWidget(aws["output_channel"]) + self.col1.addWidget(aws["unit"]) + self.col1.addWidget(aws["dc_gain"]) + self.col1.addWidget(aws["tf_type"]) + self.col1.addWidget(aws["tf_curve"]) + + + self.col2.addWidget(aws["is_sweepable"]) + self.col2.addWidget(aws["sweep_frequency"]) + self.col2.addWidget(aws["sweep_amplitude"]) + self.col2.addWidget(aws["sweep_offset"]) + self.col2.addWidget(aws["sweep_waveform"]) + + self.col3.addWidget(aws["p"]) + self.col3.addWidget(aws["i"]) + + # self.col3.addWidget(aws["tf_filter"]) + self.col3.addWidget(aws["unity_gain_desired"]) + + self.col4.addWidget(aws["additional_filter"]) + aws["additional_filter"].set_max_cols(2) + self.col4.addWidget(aws["extra_module"]) + self.col4.addWidget(aws["extra_module_state"]) + + 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_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.main_layout.addWidget(self.win) + self.main_layout.addWidget(self.win_phase) + + +