From 887eb01dd0deb2c479b8eb2902a68d4dc62e2f96 Mon Sep 17 00:00:00 2001 From: Thomas Descamps Date: Wed, 3 Jan 2018 15:35:38 +0100 Subject: [PATCH 01/35] First version of Solstis 3 laser driver --- .../instrument_drivers/msquared/__init__.py | 0 .../instrument_drivers/msquared/solstis_3.py | 66 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 qcodes/instrument_drivers/msquared/__init__.py create mode 100644 qcodes/instrument_drivers/msquared/solstis_3.py diff --git a/qcodes/instrument_drivers/msquared/__init__.py b/qcodes/instrument_drivers/msquared/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/qcodes/instrument_drivers/msquared/solstis_3.py b/qcodes/instrument_drivers/msquared/solstis_3.py new file mode 100644 index 000000000000..7ffc1ff56def --- /dev/null +++ b/qcodes/instrument_drivers/msquared/solstis_3.py @@ -0,0 +1,66 @@ +import socket +import json +import threading +import time +import random + +from qcodes.instrument.ip import IPInstrument + + +class Solstis3(IPInstrument): + def __init__(self, name, address=None, port=None, timeout=5, controller_address=None, + persistent=True, write_confirmation=True, testing=False, + **kwargs): + super().__init__(name, address=address, port=port, timeout=timeout, + persistent=False, write_confirmation=write_confirmation, testing=testing, terminator='', **kwargs) + + self._controller_address = controller_address + + self.set_persistent(persistent) + + #self.add_parameter('wavelenngth', + #set_cmd) + + @staticmethod + def _generate_transmission_id(): + return random.randint(1, 2 ** 14) + + def _connect(self): + if self._controller_address is None: + raise RuntimeError('No Controller address') + + super()._connect() + answer = self.send_message('start_link', {'ip_address': self._controller_address}) + + if answer['status'] != 'ok': + super()._disconnect() + raise RuntimeError('Connection failed', answer) + + + def send_message(self, op, parameters=None): + transmission_id = self._generate_transmission_id() + + query = {'message': + {'transmission_id': [transmission_id], + 'op': op}} + if parameters: + query['message']['parameters'] = parameters + + query_string = json.dumps(query, separators=(',', ':')) + print('query:', query_string) + answer_string = self.ask_raw(query_string) + print('answer:', answer_string) + answer = json.loads(answer_string) + + if answer['message']['transmission_id'] != [transmission_id]: + raise RuntimeError('Invalid transmission ID', answer) + + return answer['message']['parameters'] + + def snapshot_base(self, update=False): + snapshot = super().snapshot_base(update) + + snapshot['controller_address'] = self._controller_address + + return snapshot + From 6176618af42cec0e8bdd738da3d3d9b33ade7ab2 Mon Sep 17 00:00:00 2001 From: Thomas Descamps Date: Thu, 4 Jan 2018 12:12:44 +0100 Subject: [PATCH 02/35] add methods _get_status and _get_wavelength and _set_wavelength include verbose parameters if one wants to display the JSON strings --- .../instrument_drivers/msquared/solstis_3.py | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/qcodes/instrument_drivers/msquared/solstis_3.py b/qcodes/instrument_drivers/msquared/solstis_3.py index 7ffc1ff56def..7e28d16e6423 100644 --- a/qcodes/instrument_drivers/msquared/solstis_3.py +++ b/qcodes/instrument_drivers/msquared/solstis_3.py @@ -9,10 +9,10 @@ class Solstis3(IPInstrument): def __init__(self, name, address=None, port=None, timeout=5, controller_address=None, - persistent=True, write_confirmation=True, testing=False, - **kwargs): + persistent=True, write_confirmation=True, testing=False,**kwargs): + super().__init__(name, address=address, port=port, timeout=timeout, - persistent=False, write_confirmation=write_confirmation, testing=testing, terminator='', **kwargs) + persistent=False, write_confirmation=write_confirmation, terminator='', **kwargs) self._controller_address = controller_address @@ -34,10 +34,22 @@ def _connect(self): if answer['status'] != 'ok': super()._disconnect() - raise RuntimeError('Connection failed', answer) + raise RuntimeError('Connection to Solstis failed', answer) + else: + print('Connection to Solstis successful') + + def _set_wavelength(self,wavelength): + parameters = {'wavelength':[wavelength]} + self.send_message('set_wave_m',parameters) + + def _get_wavelength(self): + status = self._get_status() + return status['wavelength'][0] + def _get_status(self): + return self.send_message('get_status') - def send_message(self, op, parameters=None): + def send_message(self, op, parameters=None, verbose=False): transmission_id = self._generate_transmission_id() query = {'message': @@ -47,9 +59,11 @@ def send_message(self, op, parameters=None): query['message']['parameters'] = parameters query_string = json.dumps(query, separators=(',', ':')) - print('query:', query_string) + if verbose: + print('query:', query_string) answer_string = self.ask_raw(query_string) - print('answer:', answer_string) + if verbose: + print('answer:', answer_string) answer = json.loads(answer_string) if answer['message']['transmission_id'] != [transmission_id]: From 8df6f8221f8ff0d64989092819f25730ac1adf02 Mon Sep 17 00:00:00 2001 From: Thomas Descamps Date: Thu, 4 Jan 2018 19:26:16 +0100 Subject: [PATCH 03/35] additional methods to control the wavelength tuning (locked or not) functions to get/set lock create parameters wavelength_t/wavelength_m and lock --- .../instrument_drivers/msquared/solstis_3.py | 74 +++++++++++++++++-- 1 file changed, 69 insertions(+), 5 deletions(-) diff --git a/qcodes/instrument_drivers/msquared/solstis_3.py b/qcodes/instrument_drivers/msquared/solstis_3.py index 7e28d16e6423..a2ead874f7a1 100644 --- a/qcodes/instrument_drivers/msquared/solstis_3.py +++ b/qcodes/instrument_drivers/msquared/solstis_3.py @@ -5,6 +5,7 @@ import random from qcodes.instrument.ip import IPInstrument +from qcodes.utils.validators import Numbers, Ints, Enum, MultiType, Anything, Strings class Solstis3(IPInstrument): @@ -18,8 +19,26 @@ def __init__(self, name, address=None, port=None, timeout=5, controller_address= self.set_persistent(persistent) - #self.add_parameter('wavelenngth', - #set_cmd) + self.add_parameter('wavelength_m', + get_cmd = self._get_wavelength, + set_cmd = self._set_wave_m, + label = 'Wavelength', + unit = 'nm', + vals = Numbers(min_value = 700, max_value = 1000), + docstring = 'wavelength locked') + + self.add_parameter('wavelength_t', + get_cmd = self._get_wavelength, + set_cmd = self._move_wave_t, + label= 'Wavelength', + unit = 'nm', + vals = Numbers(min_value = 700, max_value = 1000), + docstring = 'wavelength not locked') + + self.add_parameter('lock', + get_cmd = self._is_wave_locked_m, + set_cmd = self._lock_wave_m, + vals = Enum(True,False)) @staticmethod def _generate_transmission_id(): @@ -38,17 +57,62 @@ def _connect(self): else: print('Connection to Solstis successful') - def _set_wavelength(self,wavelength): + ########## tuning WITHOUT solstis ########## + def _move_wave_t(self,wavelength): + parameters = {'wavelength':[wavelength]} + self.send_message('move_wave_t',parameters) + + def poll_move_wave_t(self): + current_status = self.send_message('poll_move_wave_t') + inProgress = False + if current_status['status'][0] == 1: + inProgress = True + current_wavelength = current_status['current_wavelength'][0] + return inProgress, current_wavelength + + def stop_move_wave_t(self): + self.send_message('stop_move_wave_t') + time.sleep(0.5) #delay between stop cmd sent and effective stop -> read final wavelength + return self._get_wavelength() + + ########## tuning WITH solstis ########## + def _set_wave_m(self, wavelength): parameters = {'wavelength':[wavelength]} self.send_message('set_wave_m',parameters) + def poll_wave_m(self): + current_status = self.send_message('poll_wave_m') + inProgress = False + if current_status['status'][0] == 1: + inProgress = True + current_wavelength = current_status['current_wavelength'][0] + lock_status = current_status['lock_status'][0] + return inProgress, current_wavelength, lock_status + + def stop_wave_m(self): + stop_status = self.send_message('stop_wave_m') + return stop_status['current_wavelength'][0] + + def _lock_wave_m(self,will_be_locked): + if will_be_locked: + parameters = {'operation':'on'} + else: + parameters = {'operation':'off'} + locking_status = self.send_message('lock_wave_m',parameters) + + def _is_wave_locked_m(self): + onProgress, current_wavelength, lock_status = self.poll_wave_m() + return lock_status + + ######### read STATUS ######### def _get_wavelength(self): - status = self._get_status() + status = self.get_status() return status['wavelength'][0] - def _get_status(self): + def get_status(self): return self.send_message('get_status') + ######### ASK command ######### def send_message(self, op, parameters=None, verbose=False): transmission_id = self._generate_transmission_id() From c369cb62029cff6b1af5a978d5c96a58af5cea4b Mon Sep 17 00:00:00 2001 From: Rene Otten Date: Mon, 14 May 2018 12:14:26 +0200 Subject: [PATCH 04/35] Get started on MFLI driver development --- qcodes/instrument_drivers/ZI/ZIMFLI.py | 2148 ++++++++++++++++++++++++ 1 file changed, 2148 insertions(+) create mode 100644 qcodes/instrument_drivers/ZI/ZIMFLI.py diff --git a/qcodes/instrument_drivers/ZI/ZIMFLI.py b/qcodes/instrument_drivers/ZI/ZIMFLI.py new file mode 100644 index 000000000000..855b15829794 --- /dev/null +++ b/qcodes/instrument_drivers/ZI/ZIMFLI.py @@ -0,0 +1,2148 @@ +import time +import logging +import numpy as np +from functools import partial +from math import sqrt + +from typing import Callable, List, Union, cast + +try: + import zhinst.utils +except ImportError: + raise ImportError('''Could not find Zurich Instruments Lab One software. + Please refer to the Zi MFLI User Manual for + download and installation instructions. + ''') + +from qcodes.instrument.parameter import MultiParameter +from qcodes.instrument.base import Instrument +from qcodes.instrument.channel import InstrumentChannel, ChannelList +from qcodes.utils import validators as vals + +log = logging.getLogger(__name__) + +class AUXOutputChannel(InstrumentChannel): + + def __init__(self, parent: 'ZIMFLI', name: str, channum: int) -> None: + super().__init__(parent, name) + + # TODO better validations of parameters + self.add_parameter('scale', + label='scale', + unit='', + get_cmd=partial(self._parent._getter, 'auxouts', + channum - 1, 1, 'scale'), + set_cmd=partial(self._parent._setter, 'auxouts', + channum - 1, 1, 'scale'), + vals=vals.Numbers() + ) + + self.add_parameter('preoffset', + label='preoffset', + unit='', + get_cmd=partial(self._parent._getter, 'auxouts', + channum - 1, 1, 'preoffset'), + set_cmd=partial(self._parent._setter, 'auxouts', + channum - 1, 1, 'preoffset'), + vals=vals.Numbers() + ) + self.add_parameter('offset', + label='offset', + unit='', + get_cmd=partial(self._parent._getter, 'auxouts', + channum - 1, 1, 'offset'), + set_cmd=partial(self._parent._setter, 'auxouts', + channum - 1, 1, 'offset'), + vals=vals.Numbers() + ) + self.add_parameter('limitlower', + label='Lower limit', + unit='', + get_cmd=partial(self._parent._getter, 'auxouts', + channum - 1, 1, 'limitlower'), + set_cmd=partial(self._parent._setter, 'auxouts', + channum - 1, 1, 'limitlower'), + vals=vals.Numbers() + ) + + self.add_parameter('limitupper', + label='Upper limit', + unit='', + get_cmd=partial(self._parent._getter, 'auxouts', + channum - 1, 1, 'limitupper'), + set_cmd=partial(self._parent._setter, 'auxouts', + channum - 1, 1, 'limitupper'), + vals=vals.Numbers() + ) + + # TODO the validator does not catch that there are only + # 2 valid output channels for AU types + self.add_parameter('channel', + label='Channel', + unit='', + get_cmd=partial(self._parent._getter, 'auxouts', + channum - 1, 0, 'demodselect'), + set_cmd=partial(self._parent._setter, 'auxouts', + channum - 1, 0, 'demodselect'), + get_parser=lambda x: x+1, + set_parser=lambda x: x-1, + vals=vals.Ints(0,7) + ) + + outputvalmapping = {'Demod X': 0, + 'Demod Y': 1, + 'Demod R': 2, + 'Demod THETA': 3, + 'AU Cartesian': 7, + 'AU Polar': 8} + + self.add_parameter('output', + label='Output', + unit='', + get_cmd=partial(self._parent._getter, 'auxouts', + channum - 1, 0, 'outputselect'), + set_cmd=partial(self._parent._setter, 'auxouts', + channum - 1, 0, 'outputselect'), + val_mapping=outputvalmapping + ) + +class Sweep(MultiParameter): + """ + Parameter class for the ZIMFLI instrument class for the sweeper. + + The get method returns a tuple of arrays, where each array contains the + values of a signal added to the sweep (e.g. demodulator 4 phase). + + Attributes: + names (tuple): Tuple of strings containing the names of the sweep + signals (to be measured) + units (tuple): Tuple of strings containg the units of the signals + shapes (tuple): Tuple of tuples each containing the Length of a + signal. + setpoints (tuple): Tuple of N copies of the sweep x-axis points, + where N is he number of measured signals + setpoint_names (tuple): Tuple of N identical strings with the name + of the sweep x-axis. + + """ + def __init__(self, name, instrument, **kwargs): + # The __init__ requires that we supply names and shapes, + # but there is no way to know what they could be known at this time. + # They are updated via build_sweep. + super().__init__(name, names=('',), shapes=((1,),), **kwargs) + self._instrument = instrument + + def build_sweep(self): + """ + Build a sweep with the current sweep settings. Must be called + before the sweep can be executed. + + For developers: + This is a general function for updating the sweeper. + Every time a parameter of the sweeper is changed, this function + must be called to update the sweeper. Although such behaviour is only + strictly necessary for parameters that affect the setpoints of the + Sweep parameter, having to call this function for any parameter is + deemed more user friendly (easier to remember; when? -always). + + The function sets all (user specified) settings on the sweeper and + additionally sets names, units, and setpoints for the Sweep + parameter. + + """ + + signals = self._instrument._sweeper_signals + sweepdict = self._instrument._sweepdict + + log.info('Built a sweep') + + sigunits = {'X': 'V', 'Y': 'V', 'R': 'Vrms', 'Xrms': 'Vrms', + 'Yrms': 'Vrms', 'Rrms': 'Vrms', 'phase': 'degrees'} + names = [] + units = [] + for sig in signals: + name = sig.split('/')[-1] + names.append(name) + units.append(sigunits[name]) + self.names = tuple(names) + self.units = tuple(units) + self.labels = tuple(names) # TODO: What are good labels? + + # TODO: what are good set point names? + spnamedict = {'auxouts/0/offset': 'Volts', + 'auxouts/1/offset': 'Volts', + 'auxouts/2/offset': 'Volts', + 'auxouts/3/offset': 'Volts', + 'demods/0/phaseshift': 'degrees', + 'demods/1/phaseshift': 'degrees', + 'demods/2/phaseshift': 'degrees', + 'demods/3/phaseshift': 'degrees', + 'demods/4/phaseshift': 'degrees', + 'demods/5/phaseshift': 'degrees', + 'demods/6/phaseshift': 'degrees', + 'demods/7/phaseshift': 'degrees', + 'oscs/0/freq': 'Hz', + 'oscs/1/freq': 'Hz', + 'sigouts/0/amplitudes/3': 'Volts', + 'sigouts/0/offset': 'Volts', + 'sigouts/1/amplitudes/7': 'Volts', + 'sigouts/1/offset': 'Volts' + } + sp_name = spnamedict[sweepdict['gridnode']] + + self.setpoint_names = ((sp_name,),)*len(signals) + start = sweepdict['start'] + stop = sweepdict['stop'] + npts = sweepdict['samplecount'] + # TODO: make sure that these setpoints are correct, i.e. actually + # matching what the MFLI does + # TODO: support non-sequential sweep mode + if not sweepdict['scan'] == 0: + raise NotImplementedError('Only sequential scanning is supported.') + if sweepdict['xmapping'] == 0: + sw = tuple(np.linspace(start, stop, npts)) + else: + logstart = np.log10(start) + logstop = np.log10(stop) + sw = tuple(np.logspace(logstart, logstop, npts)) + self.setpoints = ((sw,),)*len(signals) + self.shapes = ((npts,),)*len(signals) + + # Now actually send the settings to the instrument + for (setting, value) in sweepdict.items(): + setting = 'sweep/' + setting + self._instrument.sweeper.set(setting, value) + + self._instrument.sweep_correctly_built = True + + def get(self): + """ + Execute the sweeper and return the data corresponding to the + subscribed signals. + + Returns: + + tuple: Tuple containg N numpy arrays where N is the number + of signals added to the sweep. + + Raises: + ValueError: If no signals have been added to the sweep + ValueError: If a sweep setting has been modified since + the last sweep, but Sweep.build_sweep has not been run + """ + daq = self._instrument.daq + signals = self._instrument._sweeper_signals + sweeper = self._instrument.sweeper + + if signals == []: + raise ValueError('No signals selected! Can not perform sweep.') + + if self._instrument.sweep_correctly_built is False: + raise ValueError('The sweep has not been correctly built.' + + ' Please run Sweep.build_sweep.') + + # We must enable the demodulators we use. + # After the sweep, they should be returned to their original state + streamsettings = [] # This list keeps track of the pre-sweep settings + for sigstr in signals: + path = '/'.join(sigstr.split('/')[:-1]) + (_, dev, _, dmnum, _) = path.split('/') + + # If the setting has never changed, get returns an empty dict. + # In that case, we assume that it's zero (factory default) + try: + toget = path.replace('sample', 'enable') + # ZI like nesting inside dicts... + setting = daq.get(toget)[dev]['demods'][dmnum]['enable']['value'][0] + except KeyError: + setting = 0 + streamsettings.append(setting) + daq.setInt(path.replace('sample', 'enable'), 1) + + # We potentially subscribe several times to the same demodulator, + # but that should not be a problem + sweeper.subscribe(path) + + sweeper.execute() + timeout = self._instrument.sweeper_timeout.get() + start = time.time() + while not sweeper.finished(): # Wait until the sweep is done/timeout + time.sleep(0.2) # Check every 200 ms whether the sweep is done + # Here we could read intermediate data via: + # data = sweeper.read(True)... + # and process it while the sweep is completing. + if (time.time() - start) > timeout: + # If for some reason the sweep is blocking, force the end of the + # measurement. + log.error("Sweep still not finished, forcing finish...") + # should exit function with error message instead of returning + sweeper.finish() + + return_flat_dict = True + data = sweeper.read(return_flat_dict) + + sweeper.unsubscribe('*') + for (state, sigstr) in zip(streamsettings, signals): + path = '/'.join(sigstr.split('/')[:-1]) + daq.setInt(path.replace('sample', 'enable'), int(state)) + + return self._parsesweepdata(data) + + def _parsesweepdata(self, sweepresult): + """ + Parse the raw result of a sweep into just the data asked for by the + added sweeper signals. Used by Sweep.get. + + Args: + sweepresult (dict): The dict returned by sweeper.read + + Returns: + tuple: The requested signals in a tuple + """ + trans = {'X': 'x', 'Y': 'y', 'Aux Input 1': 'auxin0', + 'Aux Input 2': 'auxin1', 'R': 'r', 'phase': 'phase', + 'Xrms': 'xpwr', 'Yrms': 'ypwr', 'Rrms': 'rpwr'} + returndata = [] + + for signal in self._instrument._sweeper_signals: + path = '/'.join(signal.split('/')[:-1]) + attr = signal.split('/')[-1] + data = sweepresult[path][0][0][trans[attr]] + returndata.append(data) + + return tuple(returndata) + + +class Scope(MultiParameter): + """ + Parameter class for the ZI UHF-LI Scope Channel 1 + + The .get method launches an acquisition and returns a tuple of two + np.arrays + FFT mode is NOT supported. + + Attributes: + names (tuple): Tuple of strings containing the names of the sweep + signals (to be measured) + units (tuple): Tuple of strings containg the units of the signals + shapes (tuple): Tuple of tuples each containing the Length of a + signal. + setpoints (tuple): Tuple of N copies of the sweep x-axis points, + where N is he number of measured signals + setpoint_names (tuple): Tuple of N identical strings with the name + of the sweep x-axis. + """ + def __init__(self, name, instrument, **kwargs): + # The __init__ requires that we supply names and shapes, + # but there is no way to know what they could be known at this time. + # They are updated via build_scope. + super().__init__(name, names=('',), shapes=((1,),), **kwargs) + self._instrument = instrument + self._scopeactions = [] # list of callables + + def add_post_trigger_action(self, action: Callable) -> None: + """ + Add an action to be performed immediately after the trigger + has been armed. The action must be a callable taking zero + arguments + """ + if action not in self._scopeactions: + self._scopeactions.append(action) + + @property + def post_trigger_actions(self) -> List[Callable]: + return self._scopeactions + + def prepare_scope(self): + """ + Prepare the scope for a measurement. Must immediately preceed a + measurement. + """ + + log.info('Preparing the scope') + + # A convenient reference + params = self._instrument.parameters + + # First figure out what the user has asked for + chans = {1: (True, False), 2: (False, True), 3: (True, True)} + channels = chans[params['scope_channels'].get()] + + npts = params['scope_length'].get() + # Find out whether segments are enabled + if params['scope_segments'].get() == 'ON': + segs = params['scope_segments_count'].get() + else: + segs = 1 + + # TODO Check number of demods + inputunits = {'Signal Input 1': 'V', + 'Signal Input 2': 'V', + 'Trig Input 1': 'V', + 'Trig Input 2': 'V', + 'Aux Output 1': 'V', + 'Aux Output 2': 'V', + 'Aux Output 3': 'V', + 'Aux Output 4': 'V', + 'Aux In 1 Ch 1': 'V', + 'Aux In 1 Ch 2': 'V', + 'Osc phi Demod 4': '°', + 'osc phi Demod 8': '°', + 'AU Cartesian 1': 'arb. un.', + 'AU Cartesian 2': 'arb. un', + 'AU Polar 1': 'arb. un.', + 'AU Polar 2': 'arb. un.', + 'Demod 1 X': 'V', + 'Demod 1 Y': 'V', + 'Demod 1 R': 'V', + 'Demod 1 Phase': '°', + 'Demod 2 X': 'V', + 'Demod 2 Y': 'V', + 'Demod 2 R': 'V', + 'Demod 2 Phase': '°', + 'Demod 3 X': 'V', + 'Demod 3 Y': 'V', + 'Demod 3 R': 'V', + 'Demod 3 Phase': '°', + 'Demod 4 X': 'V', + 'Demod 4 Y': 'V', + 'Demod 4 R': 'V', + 'Demod 4 Phase': '°', + } + + #TODO: what are good names? + inputnames = {'Signal Input 1': 'Sig. In 1', + 'Signal Input 2': 'Sig. In 2', + 'Trig Input 1': 'Trig. In 1', + 'Trig Input 2': 'Trig. In 2', + 'Aux Output 1': 'Aux. Out 1', + 'Aux Output 2': 'Aux. Out 2', + 'Aux Output 3': 'Aux. Out 3', + 'Aux Output 4': 'Aux. Out 4', + 'Aux In 1 Ch 1': 'Aux. In 1 Ch 1', + 'Aux In 1 Ch 2': 'Aux. In 1 Ch 2', + 'Osc phi Demod 4': 'Demod. 4 Phase', # TODO + 'osc phi Demod 8': 'Demod. 8 Phase', # TODO + 'AU Cartesian 1': 'AU Cartesian 1', # TODO + 'AU Cartesian 2': 'AU Cartesian 2', # TODO + 'AU Polar 1': 'AU Polar 1', # TODO + 'AU Polar 2': 'AU Polar 2', # TODO + 'Demod 1 X': 'Demodulator 1 X', + 'Demod 1 Y': 'Demodulator 1 Y', + 'Demod 1 R': 'Demodulator 1 R', + 'Demod 1 Phase': 'Demodulator 1 Phase', + 'Demod 2 X': 'Demodulator 2 X', + 'Demod 2 Y': 'Demodulator 2 Y', + 'Demod 2 R': 'Demodulator 2 R', + 'Demod 2 Phase': 'Demodulator 2 Phase', + 'Demod 3 X': 'Demodulator 3 X', + 'Demod 3 Y': 'Demodulator 3 Y', + 'Demod 3 R': 'Demodulator 3 R', + 'Demod 3 Phase': 'Demodulator 3 Phase', + 'Demod 4 X': 'Demodulator 4 X', + 'Demod 4 Y': 'Demodulator 4 Y', + 'Demod 4 R': 'Demodulator 4 R', + 'Demod 4 Phase': 'Demodulator 4 Phase', + } + + # Make the basic setpoints (the x-axis) + duration = params['scope_duration'].get() + delay = params['scope_trig_delay'].get() + starttime = params['scope_trig_reference'].get()*0.01*duration + delay + stoptime = starttime + duration + + setpointlist = tuple(np.linspace(starttime, stoptime, npts)) # x-axis + spname = 'Time' + namestr = "scope_channel{}_input".format(1) + name1 = inputnames[params[namestr].get()] + unit1 = inputunits[params[namestr].get()] + namestr = "scope_channel{}_input".format(2) + name2 = inputnames[params[namestr].get()] + unit2 = inputunits[params[namestr].get()] + + self.setpoints = ((tuple(range(segs)), (setpointlist,)*segs),)*2 + #self.setpoints = ((setpointlist,)*segs,)*2 + self.setpoint_names = (('Segments', 'Time'), ('Segments', 'Time')) + self.names = (name1, name2) + self.units = (unit1, unit2) + self.labels = ('Scope channel 1', 'Scope channel 2') + self.shapes = ((segs, npts), (segs, npts)) + + self._instrument.daq.sync() + self._instrument.scope_correctly_built = True + + def get(self): + """ + Acquire data from the scope. + + Returns: + tuple: Tuple of two n X m arrays where n is the number of segments + and m is the number of points in the scope trace. + + Raises: + ValueError: If the scope has not been prepared by running the + prepare_scope function. + """ + t_start = time.monotonic() + log.info('Scope get method called') + + if not self._instrument.scope_correctly_built: + raise ValueError('Scope not properly prepared. Please run ' + 'prepare_scope before measuring.') + + # A convenient reference + params = self._instrument.parameters + # + chans = {1: (True, False), 2: (False, True), 3: (True, True)} + channels = chans[params['scope_channels'].get()] + + if params['scope_trig_holdoffmode'].get_latest() == 'events': + raise NotImplementedError('Scope trigger holdoff in number of ' + 'events not supported. Please specify ' + 'holdoff in seconds.') + + ####################################################### + # The following steps SEEM to give the correct result + + # Make sure all settings have taken effect + self._instrument.daq.sync() + + # Calculate the time needed for the measurement. We often have failed + # measurements, so a timeout is needed. + if params['scope_segments'].get() == 'ON': + segs = params['scope_segments_count'].get() + else: + segs = 1 + deadtime = params['scope_trig_holdoffseconds'].get_latest() + # We add one second to account for latencies and random delays + meas_time = segs*(params['scope_duration'].get()+deadtime)+1 + npts = params['scope_length'].get() + + zi_error = True + error_counter = 0 + num_retries = 10 + timedout = False + while (zi_error or timedout) and error_counter < num_retries: + # one shot per trigger. This needs to be set every time + # a the scope is enabled as below using scope_runstop + try: + # we wrap this in try finally to ensure that + # scope.finish is always called even if the + # measurement is interrupted + self._instrument.daq.setInt('/{}/scopes/0/single'.format(self._instrument.device), 1) + + + scope = self._instrument.scope + scope.set('scopeModule/clearhistory', 1) + + # Start the scope triggering/acquiring + # set /dev/scopes/0/enable to 1 + params['scope_runstop'].set('run') + + self._instrument.daq.sync() + + log.debug('Starting ZI scope acquisition.') + # Start something... hauling data from the scopeModule? + scope.execute() + + # Now perform actions that may produce data, e.g. running an AWG + for action in self._scopeactions: + action() + + starttime = time.time() + timedout = False + + progress = scope.progress() + while progress < 1: + log.debug('Scope progress is {}'.format(progress)) + progress = scope.progress() + time.sleep(0.1) # This while+sleep is how ZI engineers do it + if (time.time()-starttime) > 20*meas_time+1: + timedout = True + break + metadata = scope.get("scopeModule/*") + zi_error = bool(metadata['error'][0]) + + # Stop the scope from running + params['scope_runstop'].set('stop') + + if not (timedout or zi_error): + log.info('[+] ZI scope acquisition completed OK') + rawdata = scope.read() + if 'error' in rawdata: + zi_error = bool(rawdata['error'][0]) + data = self._scopedataparser(rawdata, self._instrument.device, + npts, segs, channels) + else: + log.warning('[-] ZI scope acquisition attempt {} ' + 'failed, Timeout: {}, Error: {}, ' + 'retrying'.format(error_counter, timedout, zi_error)) + rawdata = None + data = (None, None) + error_counter += 1 + + if error_counter >= num_retries: + log.error('[+] ZI scope acquisition failed, maximum number' + 'of retries performed. No data returned') + raise RuntimeError('[+] ZI scope acquisition failed, maximum number' + 'of retries performed. No data returned') + finally: + # cleanup and make ready for next scope acquisition + scope.finish() + + t_stop = time.monotonic() + log.info('scope get method returning after {} s'.format(t_stop - + t_start)) + return data + + @staticmethod + def _scopedataparser(rawdata, deviceID, scopelength, segments, channels): + """ + Cast the scope return value dict into a tuple. + + Args: + rawdata (dict): The return of scopeModule.read() + deviceID (str): The device ID string of the instrument. + scopelength (int): The length of each segment + segments (int): The number of segments + channels (tuple): Tuple of two bools controlling what data to return + (True, False) will return data for channel 1 etc. + + Returns: + tuple: A 2-tuple of either None or np.array with dimensions + segments x scopelength. + """ + + data = rawdata['{}'.format(deviceID)]['scopes']['0']['wave'][0][0] + if channels[0]: + ch1data = data['wave'][0].reshape(segments, scopelength) + else: + ch1data = None + if channels[1]: + ch2data = data['wave'][1].reshape(segments, scopelength) + else: + ch2data = None + + return (ch1data, ch2data) + +class ZIMFLI(Instrument): + """ + QCoDeS driver for ZI MFLI. + + Currently implementing demodulator settings and the sweeper functionality. + + Requires ZI Lab One software to be installed on the computer running QCoDeS. + Furthermore, the Data Server and Web Server must be running and a connection + between the two must be made. + + TODOs: + * Add zoom-FFT + """ + + def __init__(self, name: str, device_ID: str, **kwargs) -> None: + """ + Create an instance of the instrument. + + Args: + name (str): The internal QCoDeS name of the instrument + device_ID (str): The device name as listed in the web server. + """ + + super().__init__(name, **kwargs) + self.api_level = 5 + zisession = zhinst.utils.create_api_session(device_ID, self.api_level) + (self.daq, self.device, self.props) = zisession + + self.daq.setDebugLevel(3) + # create (instantiate) an instance of each module we will use + self.sweeper = self.daq.sweep() + self.sweeper.set('sweep/device', self.device) + self.scope = self.daq.scopeModule() + self.scope.subscribe('/{}/scopes/0/wave'.format(self.device)) + ######################################## + # INSTRUMENT PARAMETERS + + ######################################## + # Oscillators + for oscs in range(1,3): # TODO + self.add_parameter('oscillator{}_freq'.format(oscs), + label='Frequency of oscillator {}'.format(oscs), + unit='Hz', + set_cmd=partial(self._setter, 'oscs', + oscs-1, 1, 'freq'), + get_cmd=partial(self._getter, 'oscs', + oscs-1, 1, 'freq'), + vals=vals.Numbers(0, 600e6)) + + ######################################## + # DEMODULATOR PARAMETERS + + for demod in range(1, 9): # TODO + self.add_parameter('demod{}_order'.format(demod), + label='Filter order', + get_cmd=partial(self._getter, 'demods', + demod-1, 0, 'order'), + set_cmd=partial(self._setter, 'demods', + demod-1, 0, 'order'), + vals=vals.Ints(1, 8) + ) + + self.add_parameter('demod{}_harmonic'.format(demod), + label=('Reference frequency multiplication' + + ' factor'), + get_cmd=partial(self._getter, 'demods', + demod-1, 1, 'harmonic'), + set_cmd=partial(self._setter, 'demods', + demod-1, 1, 'harmonic'), + vals=vals.Ints(1, 999) + ) + + self.add_parameter('demod{}_timeconstant'.format(demod), + label='Filter time constant', + get_cmd=partial(self._getter, 'demods', + demod-1, 1, 'timeconstant'), + set_cmd=partial(self._setter, 'demods', + demod-1, 1, 'timeconstant'), + unit='s' + ) + + self.add_parameter('demod{}_samplerate'.format(demod), + label='Sample rate', + get_cmd=partial(self._getter, 'demods', + demod-1, 1, 'rate'), + set_cmd=partial(self._setter, 'demods', + demod-1, 1, 'rate'), + unit='Sa/s', + docstring=""" + Note: the value inserted by the user + may be approximated to the + nearest value supported by the + instrument. + """) + + self.add_parameter('demod{}_phaseshift'.format(demod), + label='Phase shift', + unit='degrees', + get_cmd=partial(self._getter, 'demods', + demod-1, 1, 'phaseshift'), + set_cmd=partial(self._setter, 'demods', + demod-1, 1, 'phaseshift') + ) + + # val_mapping for the demodX_signalin parameter + dmsigins = {'Sig In 1': 0, + 'Sig In 2': 1, + 'Trigger 1': 2, + 'Trigger 2': 3, + 'Aux Out 1': 4, + 'Aux Out 2': 5, + 'Aux Out 3': 6, + 'Aux Out 4': 7, + 'Aux In 1': 8, + 'Aux In 2': 9, + 'Phi Demod 4': 10, + 'Phi Demod 8': 11} + + self.add_parameter('demod{}_signalin'.format(demod), + label='Signal input', + get_cmd=partial(self._getter, 'demods', + demod-1, 0,'adcselect'), + set_cmd=partial(self._setter, 'demods', + demod-1, 0, 'adcselect'), + val_mapping=dmsigins, + vals=vals.Enum(*list(dmsigins.keys())) + ) + + self.add_parameter('demod{}_sinc'.format(demod), + label='Sinc filter', + get_cmd=partial(self._getter, 'demods', + demod-1, 0, 'sinc'), + set_cmd=partial(self._setter, 'demods', + demod-1, 0, 'sinc'), + val_mapping={'ON': 1, 'OFF': 0}, + vals=vals.Enum('ON', 'OFF') + ) + + self.add_parameter('demod{}_streaming'.format(demod), + label='Data streaming', + get_cmd=partial(self._getter, 'demods', + demod-1, 0, 'enable'), + set_cmd=partial(self._setter, 'demods', + demod-1, 0, 'enable'), + val_mapping={'ON': 1, 'OFF': 0}, + vals=vals.Enum('ON', 'OFF') + ) + + dmtrigs = {'Continuous': 0, + 'Trigger in 3 Rise': 1, + 'Trigger in 3 Fall': 2, + 'Trigger in 3 Both': 3, + 'Trigger in 3 High': 32, + 'Trigger in 3 Low': 16, + 'Trigger in 4 Rise': 4, + 'Trigger in 4 Fall': 8, + 'Trigger in 4 Both': 12, + 'Trigger in 4 High': 128, + 'Trigger in 4 Low': 64, + 'Trigger in 3|4 Rise': 5, + 'Trigger in 3|4 Fall': 10, + 'Trigger in 3|4 Both': 15, + 'Trigger in 3|4 High': 160, + 'Trigger in 3|4 Low': 80} + + self.add_parameter('demod{}_trigger'.format(demod), + label='Trigger', + get_cmd=partial(self._getter, 'demods', + demod-1, 0, 'trigger'), + set_cmd=partial(self._setter, 'demods', + demod-1, 0, 'trigger'), + val_mapping=dmtrigs, + vals=vals.Enum(*list(dmtrigs.keys())) + ) + + self.add_parameter('demod{}_sample'.format(demod), + label='Demod sample', + get_cmd=partial(self._getter, 'demods', + demod - 1, 2, 'sample'), + snapshot_value=False + ) + + for demod_param in ['x', 'y', 'R', 'phi']: + if demod_param in ('x', 'y', 'R'): + unit = 'V' + else: + unit = 'deg' + self.add_parameter('demod{}_{}'.format(demod, demod_param), + label='Demod {} {}'.format(demod, demod_param), + get_cmd=partial(self._get_demod_sample, + demod - 1, demod_param), + snapshot_value=False, + unit=unit + ) + + ######################################## + # SIGNAL INPUTS + + for sigin in range(1, 3): # TODO + + self.add_parameter('signal_input{}_range'.format(sigin), + label='Input range', + set_cmd=partial(self._setter, 'sigins', + sigin-1, 1, 'range'), + get_cmd=partial(self._getter, 'sigins', + sigin-1, 1, 'range'), + unit='V') + + self.add_parameter('signal_input{}_scaling'.format(sigin), + label='Input scaling', + set_cmd=partial(self._setter, 'sigins', + sigin-1, 1, 'scaling'), + get_cmd=partial(self._getter, 'sigins', + sigin-1, 1, 'scaling'), + ) + + self.add_parameter('signal_input{}_AC'.format(sigin), + label='AC coupling', + set_cmd=partial(self._setter,'sigins', + sigin-1, 0, 'ac'), + get_cmd=partial(self._getter, 'sigins', + sigin-1, 0, 'ac'), + val_mapping={'ON': 1, 'OFF': 0}, + vals=vals.Enum('ON', 'OFF') + ) + + self.add_parameter('signal_input{}_impedance'.format(sigin), + label='Input impedance', + set_cmd=partial(self._setter, 'sigins', + sigin-1, 0, 'imp50'), + get_cmd=partial(self._getter, 'sigins', + sigin-1, 0, 'imp50'), + val_mapping={50: 1, 1000: 0}, + vals=vals.Enum(50, 1000) + ) + + sigindiffs = {'Off': 0, 'Inverted': 1, 'Input 1 - Input 2': 2, + 'Input 2 - Input 1': 3} + self.add_parameter('signal_input{}_diff'.format(sigin), + label='Input signal subtraction', + set_cmd=partial(self._setter, 'sigins', + sigin-1, 0, 'diff'), + get_cmd=partial(self._getter, 'sigins', + sigin-1, 0, 'diff'), + val_mapping=sigindiffs, + vals=vals.Enum(*list(sigindiffs.keys()))) + + ######################################## + # SIGNAL OUTPUTS + outputamps = {1: 'amplitudes/3', 2: 'amplitudes/7'} + outputampenable = {1: 'enables/3', 2: 'enables/7'} + + for sigout in range(1,3): # TODO + + self.add_parameter('signal_output{}_on'.format(sigout), + label='Turn signal output on and off.', + set_cmd=partial(self._sigout_setter, + sigout-1, 0, 'on'), + get_cmd=partial(self._sigout_getter, + sigout-1, 0, 'on'), + val_mapping={'ON': 1, 'OFF': 0}, + vals=vals.Enum('ON', 'OFF') ) + + self.add_parameter('signal_output{}_imp50'.format(sigout), + label='Switch to turn on 50 Ohm impedance', + set_cmd=partial(self._sigout_setter, + sigout-1, 0, 'imp50'), + get_cmd=partial(self._sigout_getter, + sigout-1, 0, 'imp50'), + val_mapping={'ON': 1, 'OFF': 0}, + vals=vals.Enum('ON', 'OFF') ) + + self.add_parameter('signal_output{}_amplitude'.format(sigout), + label='Signal output amplitude', + set_cmd=partial(self._sigout_setter, + sigout-1, 1, outputamps[sigout]), + get_cmd=partial(self._sigout_getter, + sigout-1, 1, outputamps[sigout]), + unit='V') + + self.add_parameter('signal_output{}_ampdef'.format(sigout), + get_cmd=None, set_cmd=None, + initial_value='Vpk', + label="Signal output amplitude's definition", + unit='V', + vals=vals.Enum('Vpk','Vrms', 'dBm')) + + self.add_parameter('signal_output{}_range'.format(sigout), + label='Signal output range', + set_cmd=partial(self._sigout_setter, + sigout-1, 1, 'range'), + get_cmd=partial(self._sigout_getter, + sigout-1, 1, 'range'), + vals=vals.Enum(0.075, 0.15, 0.75, 1.5)) + + self.add_parameter('signal_output{}_offset'.format(sigout), + label='Signal output offset', + set_cmd=partial(self._sigout_setter, + sigout-1, 1, 'offset'), + get_cmd=partial(self._sigout_getter, + sigout-1, 1, 'offset'), + vals=vals.Numbers(-1.5, 1.5), + unit='V') + + self.add_parameter('signal_output{}_autorange'.format(sigout), + label='Enable signal output range.', + set_cmd=partial(self._sigout_setter, + sigout-1, 0, 'autorange'), + get_cmd=partial(self._sigout_getter, + sigout-1, 0, 'autorange'), + val_mapping={'ON': 1, 'OFF': 0}, + vals=vals.Enum('ON', 'OFF') ) + + self.add_parameter('signal_output{}_enable'.format(sigout), + label="Enable signal output's amplitude.", + set_cmd=partial(self._sigout_setter, + sigout-1, 0, + outputampenable[sigout]), + get_cmd=partial(self._sigout_getter, + sigout-1, 0, + outputampenable[sigout]), + val_mapping={'ON': 1, 'OFF': 0}, + vals=vals.Enum('ON', 'OFF') ) + + auxoutputchannels = ChannelList(self, "AUXOutputChannels", AUXOutputChannel, + snapshotable=False) + + for auxchannum in range(1,5): # TODO + name = 'aux_out{}'.format(auxchannum) + auxchannel = AUXOutputChannel(self, name, auxchannum) + auxoutputchannels.append(auxchannel) + self.add_submodule(name, auxchannel) + auxoutputchannels.lock() + self.add_submodule('aux_out_channels', auxoutputchannels) + ######################################## + # SWEEPER PARAMETERS + + self.add_parameter('sweeper_BWmode', + label='Sweeper bandwidth control mode', + set_cmd=partial(self._sweep_setter, + 'sweep/bandwidthcontrol'), + get_cmd=partial(self._sweep_getter, + 'sweep/bandwidthcontrol'), + val_mapping={'auto': 2, 'fixed': 1, 'current': 0}, + docstring=""" + For each sweep point, the demodulator + filter bandwidth (time constant) may + be either set automatically, be the + current demodulator bandwidth or be + a fixed number; the sweeper_BW + parameter. + """ + ) + + self.add_parameter('sweeper_BW', + label='Fixed bandwidth sweeper bandwidth (NEP)', + set_cmd=partial(self._sweep_setter, + 'sweep/bandwidth'), + get_cmd=partial(self._sweep_getter, + 'sweep/bandwidth'), + docstring=""" + This is the NEP bandwidth used by the + sweeper if sweeper_BWmode is set to + 'fixed'. If sweeper_BWmode is either + 'auto' or 'current', this value is + ignored. + """ + ) + + self.add_parameter('sweeper_start', + label='Start value of the sweep', + set_cmd=partial(self._sweep_setter, + 'sweep/start'), + get_cmd=partial(self._sweep_getter, + 'sweep/start'), + vals=vals.Numbers(0, 600e6)) + + self.add_parameter('sweeper_stop', + label='Stop value of the sweep', + set_cmd=partial(self._sweep_setter, + 'sweep/stop'), + get_cmd=partial(self._sweep_getter, + 'sweep/stop'), + vals=vals.Numbers(0, 600e6)) + + self.add_parameter('sweeper_samplecount', + label='Length of the sweep (pts)', + set_cmd=partial(self._sweep_setter, + 'sweep/samplecount'), + get_cmd=partial(self._sweep_getter, + 'sweep/samplecount'), + vals=vals.Ints(0, 100000)) + + # val_mapping for sweeper_param parameter + sweepparams = {'Aux Out 1 Offset': 'auxouts/0/offset', # TODO + 'Aux Out 2 Offset': 'auxouts/1/offset', + 'Aux Out 3 Offset': 'auxouts/2/offset', + 'Aux Out 4 Offset': 'auxouts/3/offset', + 'Demod 1 Phase Shift': 'demods/0/phaseshift', + 'Demod 2 Phase Shift': 'demods/1/phaseshift', + 'Demod 3 Phase Shift': 'demods/2/phaseshift', + 'Demod 4 Phase Shift': 'demods/3/phaseshift', + 'Demod 5 Phase Shift': 'demods/4/phaseshift', + 'Demod 6 Phase Shift': 'demods/5/phaseshift', + 'Demod 7 Phase Shift': 'demods/6/phaseshift', + 'Demod 8 Phase Shift': 'demods/7/phaseshift', + 'Osc 1 Frequency': 'oscs/0/freq', + 'Osc 2 Frequency': 'oscs/1/freq', + 'Output 1 Amplitude 4': 'sigouts/0/amplitudes/3', + 'Output 1 Offset': 'sigouts/0/offset', + 'Output 2 Amplitude 8': 'sigouts/1/amplitudes/7', + 'Output 2 Offset': 'sigouts/1/offset' + } + + self.add_parameter('sweeper_param', + label='Parameter to sweep (sweep x-axis)', + set_cmd=partial(self._sweep_setter, + 'sweep/gridnode'), + val_mapping=sweepparams, + get_cmd=partial(self._sweep_getter, + 'sweep/gridnode'), + vals=vals.Enum(*list(sweepparams.keys())) + ) + + # val_mapping for sweeper_units parameter + sweepunits = {'Aux Out 1 Offset': 'V', # TODO + 'Aux Out 2 Offset': 'V', + 'Aux Out 3 Offset': 'V', + 'Aux Out 4 Offset': 'V', + 'Demod 1 Phase Shift': 'degrees', + 'Demod 2 Phase Shift': 'degrees', + 'Demod 3 Phase Shift': 'degrees', + 'Demod 4 Phase Shift': 'degrees', + 'Demod 5 Phase Shift': 'degrees', + 'Demod 6 Phase Shift': 'degrees', + 'Demod 7 Phase Shift': 'degrees', + 'Demod 8 Phase Shift': 'degrees', + 'Osc 1 Frequency': 'Hz', + 'Osc 2 Frequency': 'Hz', + 'Output 1 Amplitude 4': 'V', + 'Output 1 Offset': 'V', + 'Output 2 Amplitude 8': 'V', + 'Output 2 Offset': 'V' + } + + self.add_parameter('sweeper_units', + label='Units of sweep x-axis', + get_cmd=self.sweeper_param.get, + get_parser=lambda x:sweepunits[x]) + + # val_mapping for sweeper_mode parameter + sweepmodes = {'Sequential': 0, + 'Binary': 1, + 'Biderectional': 2, + 'Reverse': 3} + + self.add_parameter('sweeper_mode', + label='Sweep mode', + set_cmd=partial(self._sweep_setter, + 'sweep/scan'), + get_cmd=partial(self._sweep_getter, 'sweep/scan'), + val_mapping=sweepmodes, + vals=vals.Enum(*list(sweepmodes)) + ) + + self.add_parameter('sweeper_order', + label='Sweeper filter order', + set_cmd=partial(self._sweep_setter, + 'sweep/order'), + get_cmd=partial(self._sweep_getter, + 'sweep/order'), + vals=vals.Ints(1, 8), + docstring=""" + This value is invoked only when the + sweeper_BWmode is set to 'fixed'. + """) + + self.add_parameter('sweeper_settlingtime', + label=('Minimal settling time for the ' + + 'sweeper'), + set_cmd=partial(self._sweep_setter, + 'sweep/settling/time'), + get_cmd=partial(self._sweep_getter, + 'sweep/settling/time'), + vals=vals.Numbers(0), + unit='s', + docstring=""" + This is the minimal waiting time + at each point during a sweep before the + data acquisition starts. Note that the + filter settings may result in a longer + actual waiting/settling time. + """ + ) + + self.add_parameter('sweeper_inaccuracy', + label='Demodulator filter settling inaccuracy', + set_cmd=partial(self._sweep_setter, + 'sweep/settling/inaccuracy'), + docstring=""" + Demodulator filter settling inaccuracy + defining the wait time between a sweep + parameter change and recording of the + next sweep point. The settling time is + calculated as the time required to attain + the specified remaining proportion [1e-13, + 0.1] of an incoming step function. Typical + inaccuracy values: 10m for highest sweep + speed for large signals, 100u for precise + amplitude measurements, 100n for precise + noise measurements. Depending on the + order of the demodulator filter the settling + inaccuracy will define the number of filter + time constants the sweeper has to wait. The + maximum between this value and the settling + time is taken as wait time until the next + sweep point is recorded. + """ + ) + + self.add_parameter('sweeper_settlingtc', + label='Sweep filter settling time', + get_cmd=partial(self._sweep_getter, + 'sweep/settling/tc'), + unit='', + docstring="""This settling time is in units of + the filter time constant.""" + ) + + self.add_parameter('sweeper_averaging_samples', + label=('Minimal no. of samples to average at ' + + 'each sweep point'), + set_cmd=partial(self._sweep_setter, + 'sweep/averaging/sample'), + get_cmd=partial(self._sweep_getter, + 'sweep/averaging/sample'), + vals=vals.Ints(1), + docstring=""" + The actual number of samples is the + maximum of this value and the + sweeper_averaging_time times the + relevant sample rate. + """ + ) + + self.add_parameter('sweeper_averaging_time', + label=('Minimal averaging time'), + set_cmd=partial(self._sweep_setter, + 'sweep/averaging/tc'), + get_cmd=partial(self._sweep_getter, + 'sweep/averaging/tc'), + unit='s', + docstring=""" + The actual number of samples is the + maximum of this value times the + relevant sample rate and the + sweeper_averaging_samples.""" + ) + + self.add_parameter('sweeper_xmapping', + label='Sweeper x mapping', + set_cmd=partial(self._sweep_setter, + 'sweep/xmapping'), + get_cmd=partial(self._sweep_getter, + 'sweep/xmapping'), + val_mapping={'lin': 0, 'log': 1} + ) + + self.add_parameter('sweeper_sweeptime', + label='Expected sweep time', + unit='s', + get_cmd=self._get_sweep_time) + + self.add_parameter('sweeper_timeout', + label='Sweep timeout', + unit='s', + initial_value=600, + get_cmd=None, set_cmd=None) + + ######################################## + # THE SWEEP ITSELF + self.add_parameter('Sweep', + parameter_class=Sweep, + ) + + # A "manual" parameter: a list of the signals for the sweeper + # to subscribe to + self._sweeper_signals = [] # type: List[str] + + # This is the dictionary keeping track of the sweeper settings + # These are the default settings + self._sweepdict = {'start': 1e6, + 'stop': 10e6, + 'samplecount': 25, + 'bandwidthcontrol': 1, # fixed mode + 'bandwidth': 50, + 'gridnode': 'oscs/0/freq', + 'scan': 0, # sequential scan + 'order': 1, + 'settling/time': 1e-6, + 'settling/inaccuracy': 10e-3, + 'averaging/sample': 25, + 'averaging/tc': 100e-3, + 'xmapping': 0, # linear + } + # Set up the sweeper with the above settings + self.Sweep.build_sweep() + + ######################################## + # SCOPE PARAMETERS + # default parameters: + + # This parameter corresponds to the Run/Stop button in the GUI + self.add_parameter('scope_runstop', + label='Scope run state', + set_cmd=partial(self._setter, 'scopes', 0, 0, + 'enable'), + get_cmd=partial(self._getter, 'scopes', 0, 0, + 'enable'), + val_mapping={'run': 1, 'stop': 0}, + vals=vals.Enum('run', 'stop'), + docstring=('This parameter corresponds to the ' + 'run/stop button in the GUI.')) + + self.add_parameter('scope_mode', + label="Scope's mode: time or frequency domain.", + set_cmd=partial(self._scope_setter, 1, 0, + 'mode'), + get_cmd=partial(self._scope_getter, 'mode'), + val_mapping={'Time Domain': 1, + 'Freq Domain FFT': 3}, + vals=vals.Enum('Time Domain', 'Freq Domain FFT') + ) + + # 1: Channel 1 on, Channel 2 off. + # 2: Channel 1 off, Channel 2 on, + # 3: Channel 1 on, Channel 2 on. + self.add_parameter('scope_channels', + label='Recorded scope channels', + set_cmd=partial(self._scope_setter, 0, 0, + 'channel'), + get_cmd=partial(self._getter, 'scopes', 0, + 0, 'channel'), + vals=vals.Enum(1, 2, 3) + ) + + self._samplingrate_codes = {'1.80 GHz': 0, # TODO + '900 MHz': 1, + '450 MHz': 2, + '225 MHz': 3, + '113 MHz': 4, + '56.2 MHz': 5, + '28.1 MHz': 6, + '14.0 MHz': 7, + '7.03 MHz': 8, + '3.50 MHz': 9, + '1.75 MHz': 10, + '880 kHz': 11, + '440 kHz': 12, + '220 kHz': 13, + '110 kHz': 14, + '54.9 kHz': 15, + '27.5 kHz': 16} + + self.add_parameter('scope_samplingrate', + label="Scope's sampling rate", + set_cmd=partial(self._scope_setter, 0, 0, + 'time'), + get_cmd=partial(self._getter, 'scopes', 0, + 0, 'time'), + val_mapping=self._samplingrate_codes, + vals=vals.Enum(*list(self._samplingrate_codes.keys())) + ) + + self.add_parameter('scope_length', + label="Length of scope trace (pts)", + set_cmd=partial(self._scope_setter, 0, 1, + 'length'), + get_cmd=partial(self._getter, 'scopes', 0, + 1, 'length'), + vals=vals.Numbers(4096, 128000000), + get_parser=int + ) + + self.add_parameter('scope_duration', + label="Scope trace duration", + set_cmd=partial(self._scope_setter, 0, 0, + 'duration'), + get_cmd=partial(self._scope_getter, + 'duration'), + vals=vals.Numbers(2.27e-6,4.660e3), + unit='s' + ) + + # Map the possible input sources to LabOne's IDs. + # The IDs can be seen in log file of LabOne UI + inputselect = {'Signal Input 1': 0, # TODO + 'Signal Input 2': 1, + 'Trig Input 1': 2, + 'Trig Input 2': 3, + 'Aux Output 1': 4, + 'Aux Output 2': 5, + 'Aux Output 3': 6, + 'Aux Output 4': 7, + 'Aux In 1 Ch 1': 8, + 'Aux In 1 Ch 2': 9, + 'Osc phi Demod 4': 10, + 'Osc phi Demod 8': 11, + 'AU Cartesian 1': 112, + 'AU Cartesian 2': 113, + 'AU Polar 1': 128, + 'AU Polar 2': 129, + } + # Add all 8 demodulators and their respective parameters + # to inputselect as well. + # Numbers correspond to LabOne IDs, taken from UI log. + for demod in range(1,9): + inputselect['Demod {} X'.format(demod)] = 15+demod + inputselect['Demod {} Y'.format(demod)] = 31+demod + inputselect['Demod {} R'.format(demod)] = 47+demod + inputselect['Demod {} Phase'.format(demod)] = 63+demod + + for channel in range(1,3): + self.add_parameter('scope_channel{}_input'.format(channel), + label=("Scope's channel {}".format(channel) + + " input source"), + set_cmd=partial(self._scope_setter, 0, 0, + ('channels/{}/'.format(channel-1) + + 'inputselect')), + get_cmd=partial(self._getter, 'scopes', 0, 0, + ('channels/{}/'.format(channel-1) + + 'inputselect')), + val_mapping=inputselect, + vals=vals.Enum(*list(inputselect.keys())) + ) + + self.add_parameter('scope_average_weight', + label="Scope Averages", + set_cmd=partial(self._scope_setter, 1, 0, + 'averager/weight'), + get_cmd=partial(self._scope_getter, + 'averager/weight'), + vals=vals.Numbers(min_value=1) + ) + + self.add_parameter('scope_trig_enable', + label="Enable triggering for scope readout", + set_cmd=partial(self._setter, 'scopes', 0, + 0, 'trigenable'), + get_cmd=partial(self._getter, 'scopes', 0, + 0, 'trigenable'), + val_mapping={'ON': 1, 'OFF': 0}, + vals=vals.Enum('ON', 'OFF') + ) + + self.add_parameter('scope_trig_signal', + label="Trigger signal source", + set_cmd=partial(self._setter, 'scopes', 0, + 0, 'trigchannel'), + get_cmd=partial(self._getter, 'scopes', 0, + 0, 'trigchannel'), + val_mapping=inputselect, + vals=vals.Enum(*list(inputselect.keys())) + ) + + slopes = {'None': 0, 'Rise': 1, 'Fall': 2, 'Both': 3} + + self.add_parameter('scope_trig_slope', + label="Scope's triggering slope", + set_cmd=partial(self._setter, 'scopes', 0, + 0, 'trigslope'), + get_cmd=partial(self._getter, 'scopes', 0, + 0, 'trigslope'), + val_mapping=slopes, + vals=vals.Enum(*list(slopes.keys())) + ) + + # TODO: figure out how value/percent works for the trigger level + self.add_parameter('scope_trig_level', + label="Scope trigger level", + set_cmd=partial(self._setter, 'scopes', 0, + 1, 'triglevel'), + get_cmd=partial(self._getter, 'scopes', 0, + 1, 'triglevel'), + vals=vals.Numbers() + ) + + self.add_parameter('scope_trig_hystmode', + label="Enable triggering for scope readout.", + set_cmd=partial(self._setter, 'scopes', 0, + 0, 'trighysteresis/mode'), + get_cmd=partial(self._getter, 'scopes', 0, + 0, 'trighysteresis/mode'), + val_mapping={'absolute': 0, 'relative': 1}, + vals=vals.Enum('absolute', 'relative') + ) + + self.add_parameter('scope_trig_hystrelative', + label="Trigger hysteresis, relative value in %", + set_cmd=partial(self._setter, 'scopes', 0, + 1, 'trighysteresis/relative'), + get_cmd=partial(self._getter, 'scopes', 0, + 1, 'trighysteresis/relative'), + # val_mapping= lambda x: 0.01*x, + vals=vals.Numbers(0) + ) + + self.add_parameter('scope_trig_hystabsolute', + label="Trigger hysteresis, absolute value", + set_cmd=partial(self._setter, 'scopes', 0, + 1, 'trighysteresis/absolute'), + get_cmd=partial(self._getter, 'scopes', 0, + 1, 'trighysteresis/absolute'), + vals=vals.Numbers(0, 20) + ) + + triggates = {'Trigger In 3 High': 0, 'Trigger In 3 Low': 1, + 'Trigger In 4 High': 2, 'Trigger In 4 Low': 3} + self.add_parameter('scope_trig_gating_source', + label='Scope trigger gating source', + set_cmd=partial(self._setter, 'scopes', 0, 0, + 'triggate/inputselect'), + get_cmd=partial(self._getter, 'scopes', 0, 0, + 'triggate/inputselect'), + val_mapping=triggates, + vals=vals.Enum(*list(triggates.keys())) + ) + + self.add_parameter('scope_trig_gating_enable', + label='Scope trigger gating ON/OFF', + set_cmd=partial(self._setter, 'scopes', 0, 0, + 'triggate/enable'), + get_cmd=partial(self._getter, 'scopes', 0, 0, + 'triggate/enable'), + val_mapping = {'ON': 1, 'OFF': 0}, + vals=vals.Enum('ON', 'OFF')) + + # make this a slave parameter off scope_holdoff_seconds + # and scope_holdoff_events + self.add_parameter('scope_trig_holdoffmode', + label="Scope trigger holdoff mode", + set_cmd=partial(self._setter, 'scopes', 0, + 0, 'trigholdoffmode'), + get_cmd=partial(self._getter, 'scopes', 0, + 0, 'trigholdoffmode'), + val_mapping={'s': 0, 'events': 1}, + vals=vals.Enum('s', 'events') + ) + + self.add_parameter('scope_trig_holdoffseconds', + label='Scope trigger holdoff', + set_cmd=partial(self._scope_setter, 0, 1, + 'trigholdoff'), + get_cmd=partial(self._getter, 'scopes', 0, + 1, 'trigholdoff'), + unit='s', + vals=vals.Numbers(20e-6, 10) + ) + + self.add_parameter('scope_trig_reference', + label='Scope trigger reference', + set_cmd=partial(self._scope_setter, 0, 1, + 'trigreference'), + get_cmd=partial(self._getter, 'scopes', 0, + 1, 'trigreference'), + vals=vals.Numbers(0, 100) + ) + + # TODO: add validation. What's the minimal/maximal delay? + self.add_parameter('scope_trig_delay', + label='Scope trigger delay', + set_cmd=partial(self._scope_setter, 0, 1, + 'trigdelay'), + get_cmd=partial(self._getter, 'scopes', 0, 1, + 'trigdelay'), + unit='s') + + self.add_parameter('scope_segments', + label='Enable/disable segments', + set_cmd=partial(self._scope_setter, 0, 0, + 'segments/enable'), + get_cmd=partial(self._getter, 'scopes', 0, + 0, 'segments/enable'), + val_mapping={'OFF': 0, 'ON': 1}, + vals=vals.Enum('ON', 'OFF') + ) + + self.add_parameter('scope_segments_count', + label='No. of segments returned by scope', + set_cmd=partial(self._setter, 'scopes', 0, 1, + 'segments/count'), + get_cmd=partial(self._getter, 'scopes', 0, 1, + 'segments/count'), + vals=vals.Ints(1, 32768), + get_parser=int + ) + + self.add_function('scope_reset_avg', + call_cmd=partial(self.scope.set, + 'scopeModule/averager/restart', 1), + ) + + ######################################## + # THE SCOPE ITSELF + self.add_parameter('Scope', + parameter_class=Scope, + ) + + + def _setter(self, module, number, mode, setting, value): + """ + General function to set/send settings to the device. + + The module (e.g demodulator, input, output,..) number is counted in a + zero indexed fashion. + + Args: + module (str): The module (eg. demodulator, input, output, ..) + to set. + number (int): Module's index + mode (bool): Indicating whether we are setting an int or double + setting (str): The module's setting to set. + value (int/double): The value to set. + """ + + setstr = '/{}/{}/{}/{}'.format(self.device, module, number, setting) + + if mode == 0: + self.daq.setInt(setstr, value) + if mode == 1: + self.daq.setDouble(setstr, value) + + def _getter(self, module: str, number: int, + mode: int, setting: str) -> Union[float, int, str, dict]: + """ + General get function for generic parameters. Note that some parameters + use more specialised setter/getters. + + The module (e.g demodulator, input, output,..) number is counted in a + zero indexed fashion. + + Args: + module (str): The module (eg. demodulator, input, output, ..) + we want to know the value of. + number (int): Module's index + mode (int): Indicating whether we are asking for an int or double. + 0: Int, 1: double, 2: Sample + setting (str): The module's setting to set. + returns: + inquered value + + """ + + querystr = '/{}/{}/{}/{}'.format(self.device, module, number, setting) + log.debug("getting %s", querystr) + if mode == 0: + value = self.daq.getInt(querystr) + elif mode == 1: + value = self.daq.getDouble(querystr) + elif mode == 2: + value = self.daq.getSample(querystr) + else: + raise RuntimeError("Invalid mode supplied") + # Weird exception, samplingrate returns a string + return value + + def _get_demod_sample(self, number: int, demod_param: str) -> float: + log.debug("getting demod %s param %s", number, demod_param) + mode = 2 + module = 'demods' + setting = 'sample' + if demod_param not in ['x', 'y', 'R', 'phi']: + raise RuntimeError("Invalid demodulator parameter") + datadict = cast(dict, self._getter(module, number, mode, setting)) + datadict['R'] = np.abs(datadict['x'] + 1j * datadict['y']) + datadict['phi'] = np.angle(datadict['x'] + 1j * datadict['y'], deg=True) + return datadict[demod_param] + + def _sigout_setter(self, number, mode, setting, value): + """ + Function to set signal output's settings. A specific setter function is + needed as parameters depend on each other and need to be checked and + updated accordingly. + + Args: + number (int): + mode (bool): Indicating whether we are asking for an int or double + setting (str): The module's setting to set. + value (Union[int, float]): The value to set the setting to. + """ + + # convenient reference + params = self.parameters + + def amp_valid(): + nonlocal value + toget = params['signal_output{}_ampdef'.format(number+1)] + ampdef_val = toget.get() + toget = params['signal_output{}_autorange'.format(number+1)] + autorange_val = toget.get() + + if autorange_val == 'ON': + toget = params['signal_output{}_imp50'.format(number+1)] + imp50_val = toget.get() + imp50_dic = {'OFF': 1.5, 'ON': 0.75} + range_val = imp50_dic[imp50_val] + + else: + so_range = params['signal_output{}_range'.format(number+1)].get() + range_val = round(so_range, 3) + + amp_val_dict={'Vpk': lambda value: value, + 'Vrms': lambda value: value*sqrt(2), + 'dBm': lambda value: 10**((value-10)/20) + } + + if -range_val < amp_val_dict[ampdef_val](value) > range_val: + raise ValueError('Signal Output:' + + ' Amplitude too high for chosen range.') + value = amp_val_dict[ampdef_val](value) + + def offset_valid(): + nonlocal value + nonlocal number + range_val = params['signal_output{}_range'.format(number+1)].get() + range_val = round(range_val, 3) + amp_val = params['signal_output{}_amplitude'.format(number+1)].get() + amp_val = round(amp_val, 3) + if -range_val< value+amp_val > range_val: + raise ValueError('Signal Output: Offset too high for ' + 'chosen range.') + + def range_valid(): + nonlocal value + nonlocal number + toget = params['signal_output{}_autorange'.format(number+1)] + autorange_val = toget.get() + imp50_val = params['signal_output{}_imp50'.format(number+1)].get() + imp50_dic = {'OFF': [1.5, 0.15], 'ON': [0.75, 0.075]} + + if autorange_val == "ON": + raise ValueError('Signal Output :' + ' Cannot set range as autorange is turned on.') + + if value not in imp50_dic[imp50_val]: + raise ValueError('Signal Output: Choose a valid range:' + '[0.75, 0.075] if imp50 is on, [1.5, 0.15]' + ' otherwise.') + + def ampdef_valid(): + # check which amplitude definition you can use. + # dBm is only possible with 50 Ohm imp ON + imp50_val = params['signal_output{}_imp50'.format(number+1)].get() + imp50_ampdef_dict = {'ON': ['Vpk','Vrms', 'dBm'], + 'OFF': ['Vpk','Vrms']} + if value not in imp50_ampdef_dict[imp50_val]: + raise ValueError("Signal Output: Choose a valid amplitude " + "definition; ['Vpk','Vrms', 'dBm'] if imp50 is" + " on, ['Vpk','Vrms'] otherwise.") + + dynamic_validation = {'range': range_valid, + 'ampdef': ampdef_valid, + 'amplitudes/3': amp_valid, + 'amplitudes/7': amp_valid, + 'offset': offset_valid} + + def update_range_offset_amp(): + range_val = params['signal_output{}_range'.format(number+1)].get() + offset_val = params['signal_output{}_offset'.format(number+1)].get() + amp_val = params['signal_output{}_amplitude'.format(number+1)].get() + if -range_val < offset_val + amp_val > range_val: + #The GUI would allow higher values but it would clip the signal. + raise ValueError('Signal Output: Amplitude and/or ' + 'offset out of range.') + + def update_offset(): + self.parameters['signal_output{}_offset'.format(number+1)].get() + + def update_amp(): + self.parameters['signal_output{}_amplitude'.format(number+1)].get() + + def update_range(): + self.parameters['signal_output{}_autorange'.format(number+1)].get() + + # parameters which will potentially change other parameters + changing_param = {'imp50': [update_range_offset_amp, update_range], + 'autorange': [update_range], + 'range': [update_offset, update_amp], + 'amplitudes/3': [update_range, update_amp], + 'amplitudes/7': [update_range, update_amp], + 'offset': [update_range] + } + + setstr = '/{}/sigouts/{}/{}'.format(self.device, number, setting) + + if setting in dynamic_validation: + dynamic_validation[setting]() + + if mode == 0: + self.daq.setInt(setstr, value) + if mode == 1: + self.daq.setDouble(setstr, value) + + if setting in changing_param: + [f() for f in changing_param[setting]] + + def _sigout_getter(self, number, mode, setting): + """ + Function to query the settings of signal outputs. Specific setter + function is needed as parameters depend on each other and need to be + checked and updated accordingly. + + Args: + number (int): + mode (bool): Indicating whether we are asking for an int or double + setting (str): The module's setting to set. + """ + + querystr = '/{}/sigouts/{}/{}'.format(self.device, number, setting) + if mode == 0: + value = self.daq.getInt(querystr) + if mode == 1: + value = self.daq.getDouble(querystr) + + return value + + def _list_nodes(self, node): + """ + Returns a list with all nodes in the sub-tree below the specified node. + + Args: + node (str): Module of which you want to know the parameters. + return: + list of sub-nodes + """ + node_list = self.daq.getList('/{}/{}/'.format(self.device, node)) + return node_list + + @staticmethod + def NEPBW_to_timeconstant(NEPBW, order): # TODO + """ + Helper function to translate a NEP BW and a filter order + to a filter time constant. Meant to be used when calculating + sweeper sweep times. + + Note: precise only to within a few percent. + + Args: + NEPBW (float): The NEP bandwidth in Hz + order (int): The filter order + + Returns: + float: The filter time constant in s. + """ + const = {1: 0.249, 2: 0.124, 3: 0.093, 4: 0.078, 5: 0.068, + 6: 0.061, 7: 0.056, 8: 0.052} + tau_c = const[order]/NEPBW + + return tau_c + + def _get_sweep_time(self): + """ + get_cmd for the sweeper_sweeptime parameter. + + Note: this calculation is only an estimate and not precise to more + than a few percent. + + Returns: + Union[float, None]: None if the bandwidthcontrol setting is + 'auto' (then all bets are off), otherwise a time in seconds. + + Raises: + ValueError: if no signals are added to the sweep + """ + + # Possible TODO: cut down on the number of instrument + # queries. + + if self._sweeper_signals == []: + raise ValueError('No signals selected! Can not find sweep time.') + + mode = self.sweeper_BWmode.get() + + # The effective time constant of the demodulator depends on the + # sweeper/bandwidthcontrol setting. + # + # If this setting is 'current', the largest current + # time constant of the involved demodulators is used + # + # If the setting is 'fixed', the NEP BW specified under + # sweep/bandwidth is used. The filter order is needed to convert + # the NEP BW to a time constant + + demods = set([sig.split('/')[3] for sig in self._sweeper_signals]) + rates = [] + for demod in demods: + rates.append(self._getter('demods', demod, 1, 'rate')) + rate = min(rates) + + if mode == 'current': + tcs = [] + for demod in demods: + tcs.append(self._getter('demods', demod, 1, 'timeconstant')) + + tau_c = max(tcs) + + elif mode == 'fixed': + order = self.sweeper_order() + BW = self.sweeper_BW() + + tau_c = self.NEPBW_to_timeconstant(BW, order) + + elif mode == 'auto': + return None + + settlingtime = max(self.sweeper_settlingtc.get()*tau_c, + self.sweeper_settlingtime.get()) + averagingtime = max(self.sweeper_averaging_time.get()*tau_c*rate, + self.sweeper_averaging_samples.get())/rate + + time_est = (settlingtime+averagingtime)*self.sweeper_samplecount.get() + return time_est + + def _sweep_setter(self, setting, value): + """ + set_cmd for all sweeper parameters. The value and setting are saved in + a dictionary which is read by the Sweep parameter's build_sweep method + and only then sent to the instrument. + """ + key = '/'.join(setting.split('/')[1:]) + self._sweepdict[key] = value + self.sweep_correctly_built = False + + def _sweep_getter(self, setting): + """ + General get_cmd for sweeper parameters + + The built-in sweeper.get command returns a dictionary, but we want + single values. + + Args: + setting (str): the path used by ZI to describe the setting, + e.g. 'sweep/settling/time' + """ + # TODO: Should this look up in _sweepdict rather than query the + # instrument? + returndict = self.sweeper.get(setting) # this is a dict + + # The dict may have different 'depths' depending on the parameter. + # The depth is encoded in the setting string (number of '/') + keys = setting.split('/')[1:] + + while keys != []: + key = keys.pop(0) + returndict = returndict[key] + rawvalue = returndict + + if isinstance(rawvalue, np.ndarray) and len(rawvalue) == 1: + value = rawvalue[0] + elif isinstance(rawvalue, list) and len(rawvalue) == 1: + value = rawvalue[0] + else: + value = rawvalue + + return value + + def add_signal_to_sweeper(self, demodulator, attribute): + """ + Add a signal to the output of the sweeper. When the sweeper sweeps, + the signals added to the sweeper are returned. + + Args: + demodulator (int): A number from 1-8 choosing the demodulator. + The same demodulator can be chosen several times for + different attributes, e.g. demod1 X, demod1 phase + attribute (str): The attribute to record, e.g. phase or Y + + Raises: + ValueError: if a demodulator outside the allowed range is + selected + ValueError: if an attribute not in the list of allowed attributes + is selected + """ + + # TODO: implement all possibly returned attributes + valid_attributes = ['X', 'Y', 'R', 'phase', 'Xrms', 'Yrms', 'Rrms'] + + # Validation + if demodulator not in range(1, 9): + raise ValueError('Can not select demodulator' + + ' {}. Only '.format(demodulator) + + 'demodulators 1-8 are available.') + if attribute not in valid_attributes: + raise ValueError('Can not select attribute:'+ + '{}. Only the following attributes are' + + ' available: ' + + ('{}, '*len(valid_attributes)).format(*valid_attributes)) + + # internally, we use strings very similar to the ones used by the + # instrument, but with the attribute added, e.g. + # '/dev2189/demods/0/sample/X' means X of demodulator 1. + signalstring = ('/' + self.device + + '/demods/{}/sample/{}'.format(demodulator-1, + attribute)) + if signalstring not in self._sweeper_signals: + self._sweeper_signals.append(signalstring) + + def remove_signal_from_sweeper(self, demodulator, attribute): + """ + Remove a signal from the output of the sweeper. If the signal + has not previously been added, a warning is logged. + + Args: + demodulator (int): A number from 1-8 choosing the demodulator. + The same demodulator can be chosen several times for + different attributes, e.g. demod1 X, demod1 phase + attribute (str): The attribute to record, e.g. phase or Y + """ + + signalstring = ('/' + self.device + + '/demods/{}/sample/{}'.format(demodulator-1, + attribute)) + if signalstring not in self._sweeper_signals: + log.warning('Can not remove signal with {} of'.format(attribute) + + ' demodulator {}, since it was'.format(demodulator) + + ' not previously added.') + else: + self._sweeper_signals.remove(signalstring) + + def print_sweeper_settings(self): + """ + Pretty-print the current settings of the sweeper. + If Sweep.build_sweep and Sweep.get are called, the sweep described + here will be performed. + """ + print('ACQUISITION') + toprint = ['sweeper_BWmode', 'sweeper_BW', 'sweeper_order', + 'sweeper_averaging_samples', 'sweeper_averaging_time', + 'sweeper_settlingtime', 'sweeper_settlingtc'] + for paramname in toprint: + parameter = self.parameters[paramname] + print(' {}: {} ({})'.format(parameter.label, parameter.get(), + parameter.unit)) + + print('HORISONTAL') + toprint = ['sweeper_start', 'sweeper_stop', + 'sweeper_units', + 'sweeper_samplecount', + 'sweeper_param', 'sweeper_mode', + 'sweeper_timeout'] + for paramname in toprint: + parameter = self.parameters[paramname] + print(' {}: {}'.format(parameter.label, parameter.get())) + + print('VERTICAL') + count = 1 + for signal in self._sweeper_signals: + (_, _, _, dm, _, attr) = signal.split('/') + fmt = (count, int(dm)+1, attr) + print(' Signal {}: Demodulator {}: {}'.format(*fmt)) + count += 1 + + features = ['timeconstant', 'order', 'samplerate'] + print('DEMODULATORS') + demods = [] + for signal in self._sweeper_signals: + demods.append(int(signal.split('/')[3])) + demods = set(demods) + for dm in demods: + for feat in features: + parameter = self.parameters['demod{:d}_{}'.format(dm+1, feat)] + fmt = (dm+1, parameter.label, parameter.get(), parameter.unit) + print(' Demodulator {}: {}: {:.6f} ({})'.format(*fmt)) + print('META') + swptime = self.sweeper_sweeptime() + if swptime is not None: + print(' Expected sweep time: {:.1f} (s)'.format(swptime)) + else: + print(' Expected sweep time: N/A in auto mode') + print(' Sweep timeout: {} ({})'.format(self.sweeper_timeout.get(), + 's')) + ready = self.sweep_correctly_built + print(' Sweep built and ready to execute: {}'.format(ready)) + + def _scope_setter(self, scopemodule, mode, setting, value): + """ + set_cmd for all scope parameters. The value and setting are saved in + a dictionary which is read by the Scope parameter's build_scope method + and only then sent to the instrument. + + Args: + scopemodule (int): Indicates whether this is a setting of the + scopeModule or not. 1: it is a scopeModule setting, + 0: it is not. + mode (int): Indicates whether we are setting an int or a float. + 0: int, 1: float. NOTE: Ignored if scopemodule==1. + setting (str): The setting, e.g. 'length'. + value (Union[int, float, str]): The value to set. + """ + # Because setpoints need to be built + self.scope_correctly_built = False + + # Some parameters are linked to each other in specific ways + # Therefore, we need special actions for setting these parameters + + SRtranslation = {'kHz': 1e3, 'MHz': 1e6, 'GHz': 1e9, # TODO + 'khz': 1e3, 'Mhz': 1e6, 'Ghz': 1e9} + + def setlength(value): + # TODO: add validation. The GUI seems to correect this value + self.daq.setDouble('/{}/scopes/0/length'.format(self.device), + value) + SR_str = self.parameters['scope_samplingrate'].get() + (number, unit) = SR_str.split(' ') + SR = float(number)*SRtranslation[unit] + self.parameters['scope_duration']._save_val(value/SR) + self.daq.setInt('/{}/scopes/0/length'.format(self.device), value) + + def setduration(value): + # TODO: validation? + SR_str = self.parameters['scope_samplingrate'].get() + (number, unit) = SR_str.split(' ') + SR = float(number)*SRtranslation[unit] + N = int(np.round(value*SR)) + self.parameters['scope_length']._save_val(N) + self.parameters['scope_duration']._save_val(value) + self.daq.setInt('/{}/scopes/0/length'.format(self.device), N) + + def setholdoffseconds(value): + self.parameters['scope_trig_holdoffmode'].set('s') + self.daq.setDouble('/{}/scopes/0/trigholdoff'.format(self.device), + value) + + def setsamplingrate(value): + # When the sample rate is changed, the number of points of the trace + # remains unchanged and the duration changes accordingly + newSR_str = dict(zip(self._samplingrate_codes.values(), + self._samplingrate_codes.keys()))[value] + (number, unit) = newSR_str.split(' ') + newSR = float(number)*SRtranslation[unit] + oldSR_str = self.parameters['scope_samplingrate'].get() + (number, unit) = oldSR_str.split(' ') + oldSR = float(number)*SRtranslation[unit] + oldduration = self.parameters['scope_duration'].get() + newduration = oldduration*oldSR/newSR + self.parameters['scope_duration']._save_val(newduration) + self.daq.setInt('/{}/scopes/0/time'.format(self.device), value) + + specialcases = {'length': setlength, + 'duration': setduration, + 'scope_trig_holdoffseconds': setholdoffseconds, + 'time': setsamplingrate} + + if setting in specialcases: + specialcases[setting](value) + self.daq.sync() + return + else: + # We have two different parameter types: those under + # /scopes/0/ and those under scopeModule/ + if scopemodule: + self.scope.set('scopeModule/{}'.format(setting), value) + elif mode == 0: + self.daq.setInt('/{}/scopes/0/{}'.format(self.device, + setting), value) + elif mode == 1: + self.daq.setDouble('/{}/scopes/0/{}'.format(self.device, + setting), value) + return + + def _scope_getter(self, setting): + """ + get_cmd for scopeModule parameters + + """ + # There are a few special cases + SRtranslation = {'kHz': 1e3, 'MHz': 1e6, 'GHz': 1e9, # TODO + 'khz': 1e3, 'Mhz': 1e6, 'Ghz': 1e9} + + def getduration(): + SR_str = self.parameters['scope_samplingrate'].get() + (number, unit) = SR_str.split(' ') + SR = float(number)*SRtranslation[unit] + N = self.parameters['scope_length'].get() + duration = N/SR + return duration + + specialcases = {'duration': getduration} + + if setting in specialcases: + value = specialcases[setting]() + else: + querystr = 'scopeModule/' + setting + returndict = self.scope.get(querystr) + # The dict may have different 'depths' depending on the parameter. + # The depth is encoded in the setting string (number of '/') + keys = setting.split('/')[1:] + + while keys != []: + key = keys.pop(0) + returndict = returndict[key] + rawvalue = returndict + + if isinstance(rawvalue, np.ndarray) and len(rawvalue) == 1: + value = rawvalue[0] + elif isinstance(rawvalue, list) and len(rawvalue) == 1: + value = rawvalue[0] + else: + value = rawvalue + + return value + + def close(self): + """ + Override of the base class' close function + """ + self.scope.unsubscribe('/{}/scopes/0/wave'.format(self.device)) + self.scope.clear() + self.sweeper.clear() + self.daq.disconnect() + super().close() From 8922738067f794738ab164ac2ebce13db808a771 Mon Sep 17 00:00:00 2001 From: Rene Otten Date: Fri, 27 Jul 2018 10:02:39 +0200 Subject: [PATCH 05/35] Add initial version of FZJ DecaDAC driver --- .../instrument_drivers/Harvard/FZJ_Decadac.py | 757 ++++++++++++++++++ 1 file changed, 757 insertions(+) create mode 100644 qcodes/instrument_drivers/Harvard/FZJ_Decadac.py diff --git a/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py b/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py new file mode 100644 index 000000000000..eda6da94362f --- /dev/null +++ b/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py @@ -0,0 +1,757 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Author: Johannes Dresen (jo.dressen@fz-juelich.de) +# Michael Wagener (m.wagener@fz-juelich.de) +# Lukas Lankes (l.lankes@fz-juelich.de) +# all ZEA-2 at Forschungszentrum Jülich GmbH +# Date: July 2018 + + +import serial +from qcodes import InstrumentChannel, ChannelList +from qcodes.utils import validators as vals +from qcodes.instrument.base import InstrumentBase + + +class DACException(Exception): + pass + + +class DacBase(object): + + # Switch position values + SWITCH_LEFT = -1 # -10 <= U <= 0 [V] + SWITCH_MID = 0 # 0 <= U <= 10 [V] + SWITCH_RIGHT = 1 # -10 <= U <= 10 [V] + + # Slot mode values + SLOT_MODE_OFF = 0 # Channel outputs are disconnected from the input, grounded with 10MOhm. + SLOT_MODE_FINE = 1 # 2-channel mode. Channels 0 and 1 are output, use 2 and 3 for fine adjustment of Channels 0 and 1 respectively + SLOT_MODE_COARSE = 2 # All 4 channels are used as output + + # Trigger mode values + TRIG_UPDATE_ALWAYS = 0 + TRIG_UPDATE_NEVER = 8 + TRIG_1_UPDATE_IF_LOW = 2 + TRIG_1_UPDATE_IF_HIGH = 10 + TRIG_1_UPDATE_UNTIL_RISING_EDGE = 4 + TRIG_1_UPDATE_UNTIL_FALLING_EDGE = 6 + TRIG_1_UPDATE_AFTER_RISING_EDGE = 12 + TRIG_1_UDPATE_AFTER_FALLING_EDGE = 14 + TRIG_2_UPDATE_IF_LOW = 3 + TRIG_2_UPDATE_IF_HIGH = 11 + TRIG_2_UPDATE_UNTIL_RISING_EDGE = 5 + TRIG_2_UPDATE_UNTIL_FALLING_EDGE = 7 + TRIG_2_UPDATE_AFTER_RISING_EDGE = 13 + TRIG_2_UDPATE_AFTER_FALLING_EDGE = 15 + + # Validators + _CHANNEL_VAL = vals.Ints(0, 3) + _SWITCH_VAL = vals.Ints(SWITCH_LEFT, SWITCH_RIGHT) + _MODE_VAL = vals.Ints(SLOT_MODE_OFF, SLOT_MODE_COARSE) + _SLOT_VAL = vals.Ints(0, 4) + _TRIG_MODE_VAL = vals.Enum(0, 2,3,4,5,6,7,8, 10,11,12,13,14,15) # The trigger modes 1 and 9 are undefined + + # Default buffer size for reading the device parameters + _BUF_SIZE=10 + + # Default values of the parameters + _DEFAULT_VOLT = 0 + _DEFAULT_SWITCH_POS = SWITCH_MID + _DEFAULT_LOWER_LIMIT = 0 + _DEFAULT_UPPER_LIMIT = 65536 + _DEFAULT_UPDATE_PERIOD = 1000 + _DEFAULT_SLOPE = 0 + _DEFAULT_TRIG_MODE = TRIG_UPDATE_ALWAYS + _DEFAULT_SLOT_MODE = SLOT_MODE_COARSE + + # Commands to send to the device + _COMMAND_SET_SLOT = "B{};" + _COMMAND_SET_CHANNEL = "C{};" + _COMMAND_SET_VOLT = "D{};" + _COMMAND_SET_UPPER_LIMIT = "U{};" + _COMMAND_SET_LOWER_LIMIT = "L{};" + _COMMAND_SET_TRIG_MODE = "G{};" + _COMMAND_SET_SLOPE = "S{};" + _COMMAND_SET_UPDATE_PERIOD = "T{};" + _COMMAND_SET_SLOT_MODE = "M{};" + _COMMAND_GET_VOLT = "d;" + + + @staticmethod + def _dac_v_to_code(volt, min_volt, max_volt): + """ + Convert a voltage to the internal dac code (number between 0-65536) + based on the minimum/maximum values of a given channel. + Midrange is 32768. + + Arguments: + volt (float): voltage in V to convert + min_volt (float): minimum voltage + max_volt (float): maximum voltage + + Returns: + (int): dac-code + """ + if volt < min_volt or volt >= max_volt: + raise ValueError("Cannot convert voltage {} V to a voltage code, value out of range ({} V - {} V).".format(volt, min_volt, max_volt)) + + frac = (volt - min_volt) / (max_volt - min_volt) + val = int(round(frac * 65536)) + + # extra check to be absolutely sure that the instrument does nothing + # receive an out-of-bounds value + if val > 65535 or val < 0: + raise ValueError("Voltage ({} V) resulted in the voltage code {}, which is not within the allowed range.".format(volt, val)) + + return val + + + @staticmethod + def _dac_code_to_v(code, min_volt, max_volt): + """ + Convert the internal dac code (number between 0-65536) to the voltage value + based on the minimum/maximum values of a given channel. + + Arguments: + code (int): dac-code to convert + min_volt (float): minimum voltage + max_volt (float): maximum voltage + + Returns: + (float): voltage in V + """ + frac = code / 65536.0 + + return (frac * (max_volt - min_volt)) + min_volt + + + @staticmethod + def _evaluate_switchpos(pos): + """ + Returns the minimum and maximum voltages by the switch position + + Arguments: + pos (int): switch position + {SWITCH_LEFT = -1, SWITCH_MID = 0, SWITCH_RIGHT = 1} + + Returns: + (float, float): minimum and maximum voltage in V + """ + min_volt = 0. + max_volt = 0. + + if(pos == DacBase.SWITCH_LEFT): + min_volt = -10. + elif(pos == DacBase.SWITCH_MID): + max_volt = 10. + elif(pos == DacBase.SWITCH_RIGHT): + min_volt = -10. + max_volt = 10. + else: + raise ValueError("No valid switch position given.") + + return min_volt, max_volt + + +class DacChannel(InstrumentChannel, DacBase): + + def __init__(self, parent, name, channel): + """ + Initialize the channel + + Arguments: + parent (DacSlot): DacSlot object, the channel belongs to + name (String): name of the channel + channel (int): number of the channel + switch_pos (int): switch position of the channel + + Attributes: + name (str): name of the channel + volt (float): voltage of the channel + lower_ramp_limit (float): used to sweep dac voltages + upper_ramp_limit (float): used to sweep dac voltages + update_period (int): time between two refreshes in us (mircoseconds) + slope (int): ramp slope + switch_pos (int): position of the channel switch for the voltage range {SWITCH_LEFT, SWITCH_MID, SWITCH_RIGHT} + trig_mode (int): trigger mode {TRIG_UPDATE_*, TRIG_1_UPDATE_*, TRIG_2_UPDATE_*} + """ + InstrumentChannel.__init__(self, parent, name) + + DacChannel._CHANNEL_VAL.validate(channel) + self.number = channel + + # Validators + self._volt_val = DacVoltValidator(self) + self._volt_raw_val = vals.Ints(0, 65535) + self._ramp_val = vals.Numbers(0, 10) + + # Channel parameters + # Voltage + self.add_parameter("volt", get_cmd=self._get_volt, get_parser=self._dac_code_to_v, set_cmd=self._set_volt, set_parser=self._dac_v_to_code, vals=self._volt_val, label="Voltage", unit="V") + self.add_parameter("volt_raw", get_cmd=self._get_volt, set_cmd=self._set_volt, vals=self._volt_raw_val, label="Voltage (raw data)") + + # The limit commands are used to sweep dac voltages. They are not safety features. + self.add_parameter("lower_ramp_limit", get_cmd=self._get_lower_limit, get_parser=self._dac_code_to_v, set_cmd=self._set_lower_limit, set_parser=self._dac_v_to_code, vals=self._volt_val, label="Lower Ramp Limit", unit="V") + self.add_parameter("upper_ramp_limit", get_cmd=self._get_upper_limit, get_parser=self._dac_code_to_v, set_cmd=self._set_upper_limit, set_parser=self._dac_v_to_code, vals=self._volt_val, label="Upper Ramp Limit", unit="V") + self.add_parameter("lower_ramp_limit_raw", get_cmd=self._get_lower_limit, set_cmd=self._set_lower_limit, vals=self._volt_raw_val, label="Lower Ramp Limit (raw data)") + self.add_parameter("upper_ramp_limit_raw", get_cmd=self._get_upper_limit, set_cmd=self._set_upper_limit, vals=self._volt_raw_val, label="Upper Ramp Limit (raw data)") + + # Ramping parameters + self.add_parameter("update_period", get_cmd=self._get_update_period, get_parser=int, set_cmd=self._set_update_period, set_parser=int, vals=vals.Ints(50, 65535), label="Update Period", unit="us") + self.add_parameter("slope", get_cmd=self._get_slope, get_parser=int, set_cmd=self._set_slope, set_parser=int, vals=vals.Ints(-(2**32), 2**32), label="Ramp Slope") + + self.add_parameter("switch_pos", get_cmd=self._get_switch_pos, set_cmd=self._set_switch_pos, vals=self._SWITCH_VAL, label="Switch Position") + self.add_parameter("trig_mode", get_cmd=self._get_trig_mode, set_cmd=self._set_trig_mode, vals=self._TRIG_MODE_VAL, label="Trigger Mode") + + # Add ramp function to the list of functions + self.add_function("ramp", call_cmd=self._ramp, args=(self._volt_val, self._ramp_val)) + self.add_function("ramp_wait", call_cmd=self._ramp_wait, args=(self._volt_val, self._ramp_val)) + + + def reset(self): + """ + Resets all parameters to default + """ + self._volt = DacBase._DEFAULT_VOLT + self._switch_pos = DacBase._DEFAULT_SWITCH_POS + self._lower_limit = DacBase._DEFAULT_LOWER_LIMIT + self._upper_limit = DacBase._DEFAULT_UPPER_LIMIT + self._update_period = DacBase._DEFAULT_UPDATE_PERIOD + self._slope = DacBase._DEFAULT_SLOPE + self._trig_mode = DacBase._DEFAULT_TRIG_MODE + + return (DacBase._COMMAND_SET_UPPER_LIMIT.format(self._upper_limit) + + DacBase._COMMAND_SET_LOWER_LIMIT.format(self._lower_limit) + + DacBase._COMMAND_SET_VOLT.format(self._volt) + + DacBase._COMMAND_SET_TRIG_MODE.format(self._trig_mode) + + DacBase._COMMAND_SET_SLOPE.format(self._slope) + + DacBase._COMMAND_SET_UPDATE_PERIOD.format(self._update_period)) + + def _get_volt(self): + """ + Reads out the voltage of the channel as dac-code + """ + self._parent._write(self, DacBase._COMMAND_GET_VOLT) + buf = self._parent._read(self, self._BUF_SIZE) + self._volt = int(buf[-self._BUF_SIZE+1:-1]) + + return self._volt + + + def _set_volt(self, volt): + """ + Sets the voltage for the channel as dac-code + """ + self._parent._write(self, DacBase._COMMAND_SET_VOLT.format(volt)) + self._volt = volt + + + def _get_switch_pos(self): + """ + Gets the switch_pos + """ + return self._switch_pos + + + def _set_switch_pos(self, pos): + """ + Stores the set switch position of the channel + {SWITCH_LEFT = -1, SWITCH_MID = 0, SWITCH_RIGHT = 1} + + This does not change the physical switch position; synchronisation has to be made manually + """ + self._min_volt, self._max_volt = DacBase._evaluate_switchpos(pos) + self._switch_pos = pos + + + def _get_lower_limit(self): + """ + Gets the lower ramp limit as dac-code + """ + return self._lower_limit + + + def _set_lower_limit(self, lower_limit): + """ + Sets the lower ramp limit as dac-code + """ + self._parent._write(self, DacBase._COMMAND_SET_LOWER_LIMIT.format(lower_limit)) + self._lower_limit = lower_limit + + + def _get_upper_limit(self): + """ + Gets the upper ramp limit as dac-code + """ + return self._upper_limit + + + def _set_upper_limit(self, upper_limit): + """ + Sets the upper ramp limit as dac-code + """ + self._parent._write(self, DacBase._COMMAND_SET_UPPER_LIMIT.format(upper_limit)) + self._upper_limit = upper_limit + + + def _get_trig_mode(self): + """ + Gets the update trigger mode + """ + return self._trig_mode + + + def _set_trig_mode(self, trig_mode): + """ + Sets the update trigger mode + """ + self._parent._write(self, DacBase._COMMAND_SET_TRIG_MODE.format(trig_mode)) + self._trig_mode = trig_mode + + + def _get_update_period(self): + """ + Gets the update period (time between to refreshs in us) + """ + return self._update_period + + + def _set_update_period(self, update_period): + """ + Sets the update period (time between to refreshs in us) + """ + self._parent._write(self, DacBase._COMMAND_SET_UPDATE_PERIOD.format(update_period)) + self._update_period = update_period + + + def _get_slope(self): + """ + Gets the ramp slope + """ + return self._slope + + + def _set_slope(self, slope): + """ + Sets the ramp slope + """ + self._parent._write(self, DacBase._COMMAND_SET_SLOPE.format(slope)) + self._slope = slope + + + def _ramp_help(self, val, rate, wait): + """ + Ramp the DAC to a given voltage. And eventually wait until it's done. + + Params: + val (float): The voltage to ramp to in V + rate (float): The ramp rate in units of V/s + wait (bool): True if the function should wait until the ramp is done + """ + # We need to know the current dac value (in raw units), as well as the update rate + c_volt = self.volt.get() # Current Voltage + + if c_volt == val: # If we are already at the right voltage, we don't need to ramp + return + + c_val = self._dac_v_to_code(c_volt) # Current voltage in DAC units + e_val = self._dac_v_to_code(val) # Endpoint in DAC units + + t_rate = 1000000.0 / (self.update_period.get()) # Number of refreshes per second + secs = abs((c_volt - val) / rate) # Number of seconds to ramp + + # The formula to calculate the slope is: Number of DAC steps divided by the number of time steps in the ramp multiplied by 65536 + slope = int(((e_val - c_val) / (t_rate * secs)) * 65536) + + # Now let's set up our limits and ramo slope + if slope > 0: + self.upper_ramp_limit.set(val) + else: + self.lower_ramp_limit.set(val) + + self.slope.set(slope) + + # Block until the ramp is complete is block is True + if wait: + while self.volt_raw.get() != e_val: + pass + + + def _ramp(self, val, rate): + """ + Ramp the DAC to a given voltage. + + Params: + val (float): The voltage to ramp to in V + rate (float): The ramp rate in units of V/s + """ + self._ramp_help(val, rate, False) + + + def _ramp_wait(self, val, rate): + """ + Ramp the DAC to a given voltage and wait until it's done + + Params: + val (float): The voltage to ramp to in V + rate (float): The ramp rate in units of V/s + """ + self._ramp_help(val, rate, True) + + + def _dac_v_to_code(self, volt): + """ + Convert a voltage to the internal dac code (number between 0-65536) + based on the minimum/maximum values of a given channel. + Midrange is 32768. + + Arguments: + volt (float): voltage in V to convert + + Returns: + (int): dac-code + """ + return DacBase._dac_v_to_code(volt, self._min_volt, self._max_volt) + + + def _dac_code_to_v(self, code): + """ + Convert the internal dac code (number between 0-65536) to the voltage value + based on the minimum/maximum values of a given channel. + + Arguments: + code (int): dac-code to convert + + Returns: + (float): voltage in V + """ + return DacBase._dac_code_to_v(code, self._min_volt, self._max_volt) + + +class DacVoltValidator(vals.Validator): + + is_numeric = True + + + def __init__(self, parent: DacChannel) -> None: + """ + Initialize the voltage validator + + Arguments: + parent (DacChannel): channel this validator belongs to + """ + self._parent = parent + + + def validate(self, value, context=""): + """ + Checking if the voltage is in a valid range (_min_volt and _max_volt of the parent channel) + + Arguments: + value (float): voltage to check + context (str): context of the function call for error handling + """ + if not isinstance(value, vals.Numbers.validtypes): + raise TypeError("{} is not an int or float.\n{}".format(repr(value), context)) + + if not (self._parent._min_volt <= value <= self._parent._max_volt): + raise ValueError("DacVoltValidator is invalid: must be between {} and {} inclusive.\n{}".format(self._parent._min_volt, self._parent._max_volt, context)) + + +class DacSlot(InstrumentChannel, DacBase): + + def __init__(self, parent, name, slot): + """ + Initialize the slot + + Arguments: + parent (Decadac): Decadac object this slot belongs to + name (str): name of the slot + slot (int): number of the slot + switch (int): switch position of all channels of this slot + mode (int): slot mode (MODE_OFF, MODE_FINE, MODE_COARSE) + + Attributes: + name (str): name of the slot + channels (ChannelList): list of channels in this slot + mode (int): slot mode (MODE_OFF, MODE_FINE, MODE_COARSE) + """ + InstrumentChannel.__init__(self, parent, name) + + DacSlot._SLOT_VAL.validate(slot) + self.number = slot + + channels = ChannelList(self, "Slot_Chans", DacChannel) + + for channel in range(4): + channels.append(DacChannel(self, "Chan{}".format(slot, channel), channel)) + + self.add_submodule("channels", channels) + + self.add_parameter("mode", get_cmd=self._get_mode, set_cmd=self._set_mode, vals=self._MODE_VAL, label="Slot Mode") + + + def reset(self): + """ + Resets all parameters to default + """ + self._mode = DacBase._DEFAULT_SLOT_MODE + + return (DacBase._COMMAND_SET_SLOT_MODE).format(self._mode) + + + def _get_mode(self): + """ + Gets the slot mode + """ + return self._mode + + + def _set_mode(self, mode): + """ + Sets the slot mode + """ + self._parent._write(self, DacBase._COMMAND_SET_SLOT_MODE.format(mode)) + self._mode = mode + + + def _write(self, obj, cmd): + """ + Forward the write method of the Decadac class + """ + return self._parent._write(obj, cmd) + + + def _read(self, obj, buf_size): + """ + Forward the read method of the Decadac class + """ + return self._parent._read(obj, buf_size) + + +class Decadac(InstrumentBase): + + _DEFAULT_RESET = False + _DEFAULT_BAUDRATE = 9600 + _DEFAULT_TIMEOUT = 5 + + + _device_connected = False + enable_output = False + + def __init__(self, name, address, reset=_DEFAULT_RESET, baudrate=_DEFAULT_BAUDRATE, timeout=_DEFAULT_TIMEOUT, **kwargs): + """ + Initialize the device + + Args: + name (str): name of the device + address (str): address of the device (e.g. /dev/ttyUSB0) + reset (bool): if "True", set all voltages to zero, set trigger mode to "always update" and stop ramps. If "False" only the upper and lower limit is reset. + baudrate (int): baud rate of ASCII protocol + timeout (int): seconds to allow for responses. Default 5 + + Attributes: + name (str): name + slots (ChannelList): list of all slots + channels (ChannelList): list of all channels + """ + + super().__init__(name, **kwargs) + + self._address = address + self._reset = reset + self._baudrate = baudrate + self._timeout = timeout + + self._device = None + + self.current_slot = None + self.current_channel = None + + self.open() + + + def __enter__(self): + """ + Entering a with-section + """ + if self._device == None: + self.open() + + return self + + + def __exit__(self, type, value, traceback): + """ + Leaving a with-section + """ + if self._device != None: + self.close() + + return False + + + def open(self, address=None, reset=None, baudrate=None, timeout=None): + """ + Open the device connection + + Arguments: + address (str): address of the device (e.g. /dev/ttyUSB0) + reset (bool): if "True", set all voltages to zero, set trigger mode to "always update" and stop ramps. If "False" only the upper and lower limit is reset. + baudrate (int): baud rate of ASCII protocol + timeout (int): seconds to allow for responses. Default 5 + + Not given arguments are replaced by the last used value + """ + if address != None: + self._address = address + if reset != None: + self._reset = reset + if baudrate != None: + self._baudrate = baudrate + if timeout != None: + self._timeout = timeout + + if Decadac._device_connected: + raise DACException("Device \"{}\" ({}) is already in use.".format(self.name, self._address)) + + try: + print("Connecting to \"{}\" ({}) ...".format(self.name, self._address)) + self._device = serial.Serial(self._address, self._baudrate, timeout=self._timeout) + Decadac._device_connected = True + print("Connected successfully.") + except: + raise DACException("Faild to connect to \"{}\" ({}).".format(self.name, self._address)) + + self.current_slot = None + self.current_channel = None + + channels = ChannelList(self, "Channels", DacChannel) + slots = ChannelList(self, "Slots", DacSlot) + + for slot in range(5): + slots.append(DacSlot(self, "Slot{}".format(slot), slot)) + channels.extend(slots[slot].channels) + + slots.lock() + channels.lock() + + self.add_submodule("slots", slots) + self.add_submodule("channels", channels) + + if self._reset: + self.reset() + + + def close(self): + """ + Close the device connection + """ + self.current_slot = None + self.current_channel = None + + self.submodules.clear() + + print("Closing the device \"{}\" ...".format(self.name)) + self._device.close() + self._device = None + Decadac._device_connected = False + print("Closed successfully.") + + + def reset(self): + """ + Reset all parameters to default + """ + cmd = "" + + for slot in self.slots: + cmd += DacBase._COMMAND_SET_SLOT.format(slot.number) + cmd += slot.reset() + + for channel in self.channels: + cmd += DacBase._COMMAND_SET_CHANNEL.format(channel.number) + cmd += channel.reset() + + cmd += DacBase._COMMAND_SET_SLOT.format(0) + DacBase._COMMAND_SET_CHANNEL.format(0) # Select first slot and channel + + self._write(self, cmd) + + + def _set_slot(self, slot: DacSlot): + """ + Sets the current used slot + + Arguments: + slot (DacSlot): number of the current slot + """ + if self.current_slot == None or self.current_slot.number != slot.number: + self._write(self, DacBase._COMMAND_SET_SLOT.format(slot.number)) + self.current_slot = slot + + return True + + return False + + + def _set_channel(self, slot: DacSlot, channel: DacChannel): + """ + Sets the current used channel + + Arguments: + slot (DacSlot): number of the current slot + channel (DacChannel): number of the current channel in this slot + """ + + if self._set_slot(slot) or self.current_channel == None or self.current_channel.number != channel.number: + self._write(self, DacBase._COMMAND_SET_CHANNEL.format(channel.number)) + self.current_channel = channel + + return True + + return False + + + def _write(self, obj, cmd): + """ + Send a command to the device + + Arguments: + obj (object): object that wants to write something (needed to set the current slot and channel) + cmd (str): command + """ + if obj != None: + if isinstance(obj, DacSlot): + self._set_slot(obj) + elif isinstance(obj, DacChannel): + self._set_channel(obj._parent, obj) + + self._device.write(str.encode(cmd)) + + if Decadac.enable_output: + print("Decadac._write(\"{}\")".format(cmd)) + + return self._read(self, len(cmd)) + + + def _read(self, obj, buf_size): + """ + Read the buffer of the device + + Arguments: + obj (object): object that wants to read something (needed to set the current slot and channel) + buf_size (int): size of the buffer to read + """ + if obj != None: + if isinstance(obj, DacSlot): + self._set_slot(obj) + elif isinstance(obj, DacChannel): + self._set_channel(obj._parent, obj) + + result = self._device.read(buf_size).decode() + + if Decadac.enable_output: + print("Decadac._read({}) = \"{}\"".format(buf_size, result)) + + return result \ No newline at end of file From 32bd93ad60aef07a434bf16cff2d543fbd390e1c Mon Sep 17 00:00:00 2001 From: "Lukas.Lankes" Date: Wed, 5 Sep 2018 14:25:11 +0200 Subject: [PATCH 06/35] Decadac class now derives from VisaInstrument instead of using serial for the hardware communication. This should fix issue #2. --- .../instrument_drivers/Harvard/FZJ_Decadac.py | 81 ++----------------- 1 file changed, 8 insertions(+), 73 deletions(-) diff --git a/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py b/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py index eda6da94362f..8259d92069f6 100644 --- a/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py +++ b/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py @@ -7,10 +7,9 @@ # Date: July 2018 -import serial from qcodes import InstrumentChannel, ChannelList from qcodes.utils import validators as vals -from qcodes.instrument.base import InstrumentBase +from qcodes.instrument.visa import VisaInstrument class DACException(Exception): @@ -530,7 +529,7 @@ def _read(self, obj, buf_size): return self._parent._read(obj, buf_size) -class Decadac(InstrumentBase): +class Decadac(VisaInstrument): _DEFAULT_RESET = False _DEFAULT_BAUDRATE = 9600 @@ -557,73 +556,13 @@ def __init__(self, name, address, reset=_DEFAULT_RESET, baudrate=_DEFAULT_BAUDRA channels (ChannelList): list of all channels """ - super().__init__(name, **kwargs) - - self._address = address - self._reset = reset - self._baudrate = baudrate - self._timeout = timeout - - self._device = None + super().__init__(name, address=address, timeout=timeout, **kwargs) self.current_slot = None self.current_channel = None self.open() - - def __enter__(self): - """ - Entering a with-section - """ - if self._device == None: - self.open() - - return self - - - def __exit__(self, type, value, traceback): - """ - Leaving a with-section - """ - if self._device != None: - self.close() - - return False - - - def open(self, address=None, reset=None, baudrate=None, timeout=None): - """ - Open the device connection - - Arguments: - address (str): address of the device (e.g. /dev/ttyUSB0) - reset (bool): if "True", set all voltages to zero, set trigger mode to "always update" and stop ramps. If "False" only the upper and lower limit is reset. - baudrate (int): baud rate of ASCII protocol - timeout (int): seconds to allow for responses. Default 5 - - Not given arguments are replaced by the last used value - """ - if address != None: - self._address = address - if reset != None: - self._reset = reset - if baudrate != None: - self._baudrate = baudrate - if timeout != None: - self._timeout = timeout - - if Decadac._device_connected: - raise DACException("Device \"{}\" ({}) is already in use.".format(self.name, self._address)) - - try: - print("Connecting to \"{}\" ({}) ...".format(self.name, self._address)) - self._device = serial.Serial(self._address, self._baudrate, timeout=self._timeout) - Decadac._device_connected = True - print("Connected successfully.") - except: - raise DACException("Faild to connect to \"{}\" ({}).".format(self.name, self._address)) - self.current_slot = None self.current_channel = None @@ -640,9 +579,9 @@ def open(self, address=None, reset=None, baudrate=None, timeout=None): self.add_submodule("slots", slots) self.add_submodule("channels", channels) - if self._reset: + if reset: self.reset() - + def close(self): """ @@ -653,11 +592,7 @@ def close(self): self.submodules.clear() - print("Closing the device \"{}\" ...".format(self.name)) - self._device.close() - self._device = None - Decadac._device_connected = False - print("Closed successfully.") + super().close() def reset(self): @@ -727,7 +662,7 @@ def _write(self, obj, cmd): elif isinstance(obj, DacChannel): self._set_channel(obj._parent, obj) - self._device.write(str.encode(cmd)) + super().write_raw(cmd) if Decadac.enable_output: print("Decadac._write(\"{}\")".format(cmd)) @@ -749,7 +684,7 @@ def _read(self, obj, buf_size): elif isinstance(obj, DacChannel): self._set_channel(obj._parent, obj) - result = self._device.read(buf_size).decode() + result = super().ask_raw(buf_size) if Decadac.enable_output: print("Decadac._read({}) = \"{}\"".format(buf_size, result)) From 8b0332f72ffc5dd0f61a2099ebc8ac5331eb3fad Mon Sep 17 00:00:00 2001 From: 4K User Date: Wed, 26 Sep 2018 14:34:37 +0200 Subject: [PATCH 07/35] Remove unnessesary open statement --- qcodes/instrument_drivers/Harvard/FZJ_Decadac.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py b/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py index 8259d92069f6..af90b2cb5f1a 100644 --- a/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py +++ b/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py @@ -556,13 +556,11 @@ def __init__(self, name, address, reset=_DEFAULT_RESET, baudrate=_DEFAULT_BAUDRA channels (ChannelList): list of all channels """ - super().__init__(name, address=address, timeout=timeout, **kwargs) + super().__init__(name, address, timeout=timeout, **kwargs) self.current_slot = None self.current_channel = None - self.open() - self.current_slot = None self.current_channel = None From d474c63c298e792a152c9ba1d37d553e990834e3 Mon Sep 17 00:00:00 2001 From: "Lukas.Lankes" Date: Mon, 15 Oct 2018 11:29:25 +0200 Subject: [PATCH 08/35] Initial Commit. First test on hardware --- qcodes/__init__.py | 3 +- qcodes/instrument/parameter.py | 20 +++ qcodes/instrument/sweep_values.py | 3 + qcodes/loops.py | 241 +++++++++++++++++++++++++++++- qcodes/utils/validators.py | 20 +++ 5 files changed, 284 insertions(+), 3 deletions(-) diff --git a/qcodes/__init__.py b/qcodes/__init__.py index 5901a1a2d984..16f926b1f37c 100644 --- a/qcodes/__init__.py +++ b/qcodes/__init__.py @@ -33,7 +33,7 @@ from qcodes.station import Station -from qcodes.loops import Loop, active_loop, active_data_set +from qcodes.loops import Loop, BufferedLoop, active_loop, active_data_set from qcodes.measure import Measure from qcodes.actions import Task, Wait, BreakIf haswebsockets = True @@ -60,6 +60,7 @@ from qcodes.instrument.function import Function from qcodes.instrument.parameter import ( Parameter, + BufferedParameter, ArrayParameter, MultiParameter, StandardParameter, diff --git a/qcodes/instrument/parameter.py b/qcodes/instrument/parameter.py index 0012a893f1ed..48de6f4eccb3 100644 --- a/qcodes/instrument/parameter.py +++ b/qcodes/instrument/parameter.py @@ -932,6 +932,26 @@ def sweep(self, start, stop, step=None, num=None): """ return SweepFixedValues(self, start=start, stop=stop, step=step, num=num) + + +class BufferedParameter(Parameter): + + def __init__(self, name: str, instrument: 'Instrument', + sweep_parameter_cmd: Optional[Callable]=None, + send_buffer_cmd: Optional[Callable]=None, + *args, **kwargs): + super().__init__(name, instrument=instrument, *args, **kwargs) + + self._sweep_parameter_cmd = sweep_parameter_cmd + self._send_buffer_cmd = send_buffer_cmd + + def set_buffered(self, sweep_values): + if self._sweep_parameter_cmd is not None: + self._sweep_parameter_cmd(self, sweep_values) + + def send_buffer(self): + if self._send_buffer_cmd is not None: + self._send_buffer_cmd(self) class ArrayParameter(_BaseParameter): diff --git a/qcodes/instrument/sweep_values.py b/qcodes/instrument/sweep_values.py index 9301ab49f486..07f0df5c3530 100644 --- a/qcodes/instrument/sweep_values.py +++ b/qcodes/instrument/sweep_values.py @@ -62,6 +62,9 @@ def __init__(self, parameter, **kwargs): raise TypeError('parameter {} is not settable'.format(parameter)) self.set = parameter.set + + if (getattr(parameter, 'set_buffered', None) and getattr(parameter, 'has_set_buffered', True)): + self.set_buffered = parameter.set_buffered def validate(self, values): """ diff --git a/qcodes/loops.py b/qcodes/loops.py index be087ea2cede..717bcbe81956 100644 --- a/qcodes/loops.py +++ b/qcodes/loops.py @@ -56,6 +56,7 @@ from qcodes.data.data_array import DataArray from qcodes.utils.helpers import wait_secs, full_class, tprint from qcodes.utils.metadata import Metadatable +from qcodes.instrument.parameter import BufferedParameter from .actions import (_actions_snapshot, Task, Wait, _Measure, _Nest, BreakIf, _QcodesBreak) @@ -156,7 +157,34 @@ def loop(self, sweep_values, delay=0): out.nested_loop = Loop(sweep_values, delay) return out + + def buffered_loop(self, sweep_values, delay=0): + """ + Nest another loop inside this one. + + Args: + sweep_values (): + delay (int): + + Examples: + >>> Loop(sv1, d1).buffered_loop(sv2, d2).each(*a) + + is equivalent to: + + >>> Loop(sv1, d1).each(BufferedLoop(sv2, d2).each(*a)) + + Returns: a new Loop object - the original is untouched + """ + out = self._copy() + + if out.nested_loop: + # nest this new loop inside the deepest level + out.nested_loop = out.nested_loop.buffered_loop(sweep_values, delay) + else: + out.nested_loop = BufferedLoop(sweep_values, delay) + return out + def _copy(self): out = Loop(self.sweep_values, self.delay, progress_interval=self.progress_interval) @@ -304,6 +332,64 @@ def snapshot_base(self, update=False): } +class BufferedLoop(Loop): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def _copy(self): + out = BufferedLoop(self.sweep_values, self.delay, + progress_interval=self.progress_interval) + out.nested_loop = self.nested_loop + out.then_actions = self.then_actions + out.station = self.station + return out + + def loop(self, *args, **kwargs): + raise AttributeError('It is not supported to nest a \'normal\' Loop in a BufferedLoop.') + + def each(self, *actions): + return self._each(True, *actions) + + def _each(self, is_most_outer_loop: bool, *actions): + """ + Perform a set of actions at each setting of this loop. + TODO(setting vs setpoints) ? better be verbose. + + Args: + *actions (Any): actions to perform at each setting of the loop + + Each action can be: + + - a Parameter to measure + - a Task to execute + - a Wait + - another Loop or ActiveLoop + + """ + actions = list(actions) + + # check for nested Loops, and activate them with default measurement + for i, action in enumerate(actions): + if isinstance(action, BufferedLoop): + default = Station.default.default_measurement + actions[i] = action.each(*default) + elif isinstance(action, Loop): + raise AttributeError('It is not supported to nest a \'normal\' Loop in a BufferedLoop.') + + self.validate_actions(*actions) + + if self.nested_loop: + # recurse into the innermost loop and apply these actions there + actions = [self.nested_loop._each(False, *actions)] + + return BufferedActiveLoop(is_most_outer_loop, + self.sweep_values, self.delay, *actions, + then_actions=self.then_actions, station=self.station, + progress_interval=self.progress_interval, + bg_task=self.bg_task, bg_final_task=self.bg_final_task, bg_min_delay=self.bg_min_delay) + + def _attach_then_actions(loop, actions, overwrite): """Inner code for both Loop.then and ActiveLoop.then.""" for action in actions: @@ -773,7 +859,7 @@ def _compile_actions(self, actions, action_indices=()): measurement_group[:] = [] return callables - + def _compile_one(self, action, new_action_indices): if isinstance(action, Wait): return Task(self._wait, action.delay) @@ -880,7 +966,7 @@ def _run_loop(self, first_delay=0, action_indices=(), delay = 0 except _QcodesBreak: break - + # after the first setpoint, delay reverts to the loop delay delay = self.delay @@ -924,3 +1010,154 @@ def _wait(self, delay): finish_clock = time.perf_counter() + delay t = wait_secs(finish_clock) time.sleep(t) + + +class BufferedActiveLoop(ActiveLoop): + + def __init__(self, is_most_outer_loop: bool, *args, **kwargs): + super().__init__(*args, **kwargs) + + self._is_most_outer_loop = is_most_outer_loop + + def _set_buffered_sweep(self): + self.sweep_values.parameter.set_buffered(list(self.sweep_values)) + + if len(self.actions) == 1 and isinstance(self.actions[0], BufferedActiveLoop): + self.actions[0]._set_buffered_sweep() + + def _send_buffer(self): + self.sweep_values.parameter.send_buffer() + + def _run_loop(self, first_delay=0, action_indices=(), + loop_indices=(), current_values=(), + **ignore_kwargs): + """ + the routine that actually executes the loop, and can be called + from one loop to execute a nested loop + + first_delay: any delay carried over from an outer loop + action_indices: where we are in any outer loop action arrays + loop_indices: setpoint indices in any outer loops + current_values: setpoint values in any outer loops + signal_queue: queue to communicate with main process directly + ignore_kwargs: for compatibility with other loop tasks + """ + if self._is_most_outer_loop: + self._set_buffered_sweep() + self._send_buffer() + + # at the beginning of the loop, the time to wait after setting + # the loop parameter may be increased if an outer loop requested longer + delay = max(self.delay, first_delay) + + callables = self._compile_actions(self.actions, action_indices) + n_callables = 0 + for item in callables: + if hasattr(item, 'param_ids'): + n_callables += len(item.param_ids) + else: + n_callables += 1 + t0 = time.time() + last_task = t0 + imax = len(self.sweep_values) + + self.last_task_failed = False + + for i, value in enumerate(self.sweep_values): + if self.progress_interval is not None: + tprint('loop %s: %d/%d (%.1f [s])' % ( + self.sweep_values.name, i, imax, time.time() - t0), + dt=self.progress_interval, tag='outerloop') + if i: + tprint("Estimated finish time: %s" % ( + time.asctime(time.localtime(t0 + ((time.time() - t0) * imax / i)))), + dt=self.progress_interval, tag="finish") + + set_val = self.sweep_values.set(value) + + new_indices = loop_indices + (i,) + new_values = current_values + (value,) + data_to_store = {} + + if hasattr(self.sweep_values, "parameters"): # combined parameter + set_name = self.data_set.action_id_map[action_indices] + if hasattr(self.sweep_values, 'aggregate'): + value = self.sweep_values.aggregate(*set_val) + # below is useful but too verbose even at debug + # log.debug('Calling .store method of DataSet because ' + # 'sweep_values.parameters exist') + self.data_set.store(new_indices, {set_name: value}) + # set_val list of values to set [param1_setpoint, param2_setpoint ..] + for j, val in enumerate(set_val): + set_index = action_indices + (j+n_callables, ) + set_name = (self.data_set.action_id_map[set_index]) + data_to_store[set_name] = val + else: + set_name = self.data_set.action_id_map[action_indices] + data_to_store[set_name] = value + # below is useful but too verbose even at debug + # log.debug('Calling .store method of DataSet because a sweep step' + # ' was taken') + self.data_set.store(new_indices, data_to_store) + + if not self._nest_first: + # only wait the delay time if an inner loop will not inherit it + self._wait(delay) + + try: + for f in callables: + # below is useful but too verbose even at debug + # log.debug('Going through callables at this sweep step.' + # ' Calling {}'.format(f)) + f(first_delay=delay, + loop_indices=new_indices, + current_values=new_values) + + # after the first action, no delay is inherited + delay = 0 + except _QcodesBreak: + break + + # after the first setpoint, delay reverts to the loop delay + delay = self.delay + + # now check for a background task and execute it if it's + # been long enough since the last time + # don't let exceptions in the background task interrupt + # the loop + # if the background task fails twice consecutively, stop + # executing it + if self.bg_task is not None: + t = time.time() + if t - last_task >= self.bg_min_delay: + try: + self.bg_task() + except Exception: + if self.last_task_failed: + self.bg_task = None + self.last_task_failed = True + log.exception("Failed to execute bg task") + + last_task = t + + # run the background task one last time to catch the last setpoint(s) + if self.bg_task is not None: + log.debug('Running the background task one last time.') + self.bg_task() + + # the loop is finished - run the .then actions + #log.debug('Finishing loop, running the .then actions...') + for f in self._compile_actions(self.then_actions, ()): + #log.debug('...running .then action {}'.format(f)) + f() + + # run the bg_final_task from the bg_task: + if self.bg_final_task is not None: + log.debug('Running the bg_final_task') + self.bg_final_task() + + def _wait(self, delay): + if delay: + finish_clock = time.perf_counter() + delay + t = wait_secs(finish_clock) + time.sleep(t) \ No newline at end of file diff --git a/qcodes/utils/validators.py b/qcodes/utils/validators.py index 872e1b11b568..4c157ed60b3f 100644 --- a/qcodes/utils/validators.py +++ b/qcodes/utils/validators.py @@ -621,3 +621,23 @@ def __repr__(self): return '' else: return ''.format(self.allowed_keys) + + +class ObjectTypeValidator(Validator): + + def __init__(self, allowed_type=object): + """ + Validator for objects + """ + self._valid_values = [0] + self._allowed_type = allowed_type + + def validate(self, value, context=''): + if not isinstance(value, self._allowed_type): + raise TypeError('{} is a forbidden type; {}'.format(type(value), context)) + + def __repr__(self): + if self._allowed_type is None: + return '' + else: + return ''.format(self._allowed_type) \ No newline at end of file From 55eea9b3d269d5713303a18552856bbace6bec86 Mon Sep 17 00:00:00 2001 From: "Lukas.Lankes" Date: Mon, 15 Oct 2018 11:31:27 +0200 Subject: [PATCH 09/35] QuPulseInstrument added --- .../QuTech/qupulse_instrument.py | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 qcodes/instrument_drivers/QuTech/qupulse_instrument.py diff --git a/qcodes/instrument_drivers/QuTech/qupulse_instrument.py b/qcodes/instrument_drivers/QuTech/qupulse_instrument.py new file mode 100644 index 000000000000..36571689257e --- /dev/null +++ b/qcodes/instrument_drivers/QuTech/qupulse_instrument.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Wed Sep 19 10:17:25 2018 + +@author: l.lankes +""" + +from typing import Callable +import numpy as np + +from qcodes.instrument.base import Instrument +from qcodes.instrument.parameter import ArrayParameter, BufferedParameter +import qcodes.utils.validators as vals +from qupulse.pulses.pulse_template import PulseTemplate +from qupulse.pulses import ForLoopPT, MappingPT, plotting +from qupulse.expressions import Expression +from qupulse.hardware.setup import HardwareSetup + + +class QuPulseArrayParameter(ArrayParameter): + + def __init__(self, name: str, + get_cmd: Callable, **kwargs): + """ + """ + super().__init__(name, **kwargs) + + self.get_cmd = get_cmd + + def get_raw(self): + """ + """ + return self.get_cmd() + +class QuPulseParameters(Instrument): + + def __init__(self, name: str, **kwargs): + """ + """ + super().__init__(name, **kwargs) + + self.sweep_values = None + + def set_parameters(self, pulse_parameters, + sweep_parameter_cmd=None, + send_buffer_cmd=None): + """ + """ + self.parameters.clear() + + if pulse_parameters is not None: + for p in pulse_parameters: + self.add_parameter(p, parameter_class=BufferedParameter, + vals=vals.Numbers(), set_cmd=None, + sweep_parameter_cmd=sweep_parameter_cmd, + send_buffer_cmd=send_buffer_cmd) + + def to_dict(self): + result = {} + + for p in self.parameters: + result[p] = self.parameters[p].get() + + return result + + +class QuPulseInstrument(Instrument): + + def __init__(self, + name: str, + pt: PulseTemplate, + hardware_setup: HardwareSetup, + **kwargs) -> None: + """ + """ + super().__init__(name, **kwargs); + + self._hardware_setup = hardware_setup + + self.add_parameter("template", + get_cmd=self._get_template, + set_cmd=self._set_template, + vals=vals.ObjectTypeValidator(PulseTemplate), + label="Pulse template") + self.add_parameter("output", + parameter_class=QuPulseArrayParameter, + shape=(1,), + get_cmd=self._get_output, + label="Output data") + self.add_parameter("channel_mapping", + ) + self.add_submodule("template_parameters", + QuPulseParameters(self.name + "_template_parameters")) + + self.template.set(pt) + + self._output_counter = 0 # TODO Test + self._send_counter = 0 # TODO Test + self._build_counter = 0 # TODO Test + self._reset_counter = 0 # TODO Test + + + def _get_template(self): + """ + Gets the PulseTemplate + """ + return self._template + + + def _set_template(self, value): + """ + Sets the PulseTemplate and extracts its parameters + + template: PulseTemplate + """ + self._template = value + self._loops = [] + + self.template_parameters.set_parameters(self._template.parameter_names, + self._sweep_parameter, + self._send_buffer) + + + def _sweep_parameter(self, parameter, sweep_values): + """ + + """ + self._build_counter += 1 # TODO Test + print("QuPulseTemplate._build_buffer({}, {}) called ({})".format(parameter, sweep_values, self._build_counter)) + + for l in self._loops: + if l['parameter'] == parameter.name: + raise AttributeError('It is not supported to sweep the same parameter ({}) twice.'.format(parameter)) + + loop = { + "parameter" : parameter.name, + "iterator" : "__" + parameter.name + "_it", + "sweep_values" : sweep_values + } + self._loops.append(loop) + + + def _send_buffer(self, parameter): + """ + Sends the buffer to the device and the device waits for the trigger + """ + self._send_counter += 1 # TODO Test + print("QuPulseTemplate._send_buffer({}) called ({})".format(parameter, self._send_counter)) # TODO Test + + parameter_mapping = {} + params = self.template_parameters.to_dict() + + for p in self.template.get().parameter_names: + parameter_mapping[p] = p + + for loop in self._loops: + parameter_mapping[loop['parameter']] = "{}[{}]".format(loop['parameter'], loop['iterator']) + params[loop['parameter']] = loop['sweep_values'] + + pt = MappingPT(self.template.get(), parameter_mapping=parameter_mapping) + + for loop in reversed(self._loops): + pt = ForLoopPT(pt, loop['iterator'], range(len(loop['sweep_values']))) + + program = pt.create_program(parameters=params) + + print(program.get_measurement_windows()) + + # Test + plotting.plot(pt, params, sample_rate=100) + + program_name = self.name + "_program" + + self._hardware_setup.register_program(program_name, program, update=True) + self._hardware_setup.run_program(program_name) + + # resetting loops because the buffer is already at the hardware + self._loops.clear() + print(" --> Reset") # TODO Test + + + def _get_output(self): + """ + """ + self._output_counter += 1 # TODO Test + print("QuPulseTemplate._get_output() called ({}); parameters: {}.".format(self._output_counter, self.template_parameters.to_dict())) # TODO Test + + # TODO + # first call: measure all values and resturn the first + # next calls: return next values until the list of return values is empty + # empty list: -> first call because it has to be a new measure + + return 0 #np.array([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]]) From 2ffebf7343bbf2fb9a70307f44ea97abd05633a0 Mon Sep 17 00:00:00 2001 From: Lab User Date: Mon, 15 Oct 2018 15:31:30 +0200 Subject: [PATCH 10/35] Interim status after testing on hardware --- .../QuTech/qupulse_instrument.py | 78 ++++++++++++++----- qcodes/loops.py | 5 +- 2 files changed, 63 insertions(+), 20 deletions(-) diff --git a/qcodes/instrument_drivers/QuTech/qupulse_instrument.py b/qcodes/instrument_drivers/QuTech/qupulse_instrument.py index 36571689257e..c6d3eaf01e48 100644 --- a/qcodes/instrument_drivers/QuTech/qupulse_instrument.py +++ b/qcodes/instrument_drivers/QuTech/qupulse_instrument.py @@ -6,7 +6,7 @@ @author: l.lankes """ -from typing import Callable +from typing import Callable, Union, Iterable import numpy as np from qcodes.instrument.base import Instrument @@ -14,8 +14,7 @@ import qcodes.utils.validators as vals from qupulse.pulses.pulse_template import PulseTemplate from qupulse.pulses import ForLoopPT, MappingPT, plotting -from qupulse.expressions import Expression -from qupulse.hardware.setup import HardwareSetup +from qupulse.hardware.setup import HardwareSetup, ChannelID, _SingleChannel, MeasurementMask class QuPulseArrayParameter(ArrayParameter): @@ -40,8 +39,6 @@ def __init__(self, name: str, **kwargs): """ super().__init__(name, **kwargs) - self.sweep_values = None - def set_parameters(self, pulse_parameters, sweep_parameter_cmd=None, send_buffer_cmd=None): @@ -63,20 +60,20 @@ def to_dict(self): result[p] = self.parameters[p].get() return result - + class QuPulseInstrument(Instrument): def __init__(self, name: str, - pt: PulseTemplate, - hardware_setup: HardwareSetup, + program_name: str, + pulse_template: PulseTemplate=None, **kwargs) -> None: """ """ super().__init__(name, **kwargs); - self._hardware_setup = hardware_setup + self.program_name = program_name self.add_parameter("template", get_cmd=self._get_template, @@ -88,19 +85,49 @@ def __init__(self, shape=(1,), get_cmd=self._get_output, label="Output data") - self.add_parameter("channel_mapping", - ) self.add_submodule("template_parameters", QuPulseParameters(self.name + "_template_parameters")) - self.template.set(pt) + if pulse_template: + self.template.set(pulse_template) + + self._hardware_setup = HardwareSetup() self._output_counter = 0 # TODO Test self._send_counter = 0 # TODO Test self._build_counter = 0 # TODO Test self._reset_counter = 0 # TODO Test + + def set_channel(self, identifier: ChannelID, + single_channel: Union[_SingleChannel, Iterable[_SingleChannel]], + allow_multiple_registration: bool=False) -> None: + """ + HardwareSetup.set_channel + """ + return self._hardware_setup.set_channel(identifier, + single_channel, + allow_multiple_registration=allow_multiple_registration) + + def set_measurement(self, measurement_name: str, + measurement_mask: Union[MeasurementMask, Iterable[MeasurementMask]], + allow_multiple_registration: bool=False): + """ + HardwareSetup.set_measurement + """ + return self._hardware_setup.set_measurement(measurement_name, + measurement_mask, + allow_multiple_registration=allow_multiple_registration) + + + def rm_channel(self, identifier: ChannelID) -> None: + """ + HardwareSetup.rm_channel + """ + return self._hardware_setup.rm_channel(identifier) + + def _get_template(self): """ Gets the PulseTemplate @@ -165,15 +192,28 @@ def _send_buffer(self, parameter): program = pt.create_program(parameters=params) - print(program.get_measurement_windows()) - # Test - plotting.plot(pt, params, sample_rate=100) - program_name = self.name + "_program" + self._hardware_setup.register_program(self.program_name, program, update=True) + + awg = next(iter(self._hardware_setup.known_awgs)) # TODO + dac = next(iter(self._hardware_setup.known_dacs)) # TODO + + awg._device.set_chan_state([False, False, True, True]) + + self._hardware_setup.arm_program(self.program_name) + + awg.run_current_program() + + self.scanline_data = dac.card.extractNextScanline() + + for measurement in self._hardware_setup._measurement_map: + + self.DS_C = self.scanline_data.operationResults['DS_C'].getAsVoltage(dac.config.inputConfiguration[2].inputRange) + self.DS_D = self.scanline_data.operationResults['DS_D'].getAsVoltage(dac.config.inputConfiguration[2].inputRange) - self._hardware_setup.register_program(program_name, program, update=True) - self._hardware_setup.run_program(program_name) + self.DS_C = self.DS_C.reshape((40, 40)) + self.DS_D = self.DS_D.reshape((40, 40)) # resetting loops because the buffer is already at the hardware self._loops.clear() @@ -184,7 +224,7 @@ def _get_output(self): """ """ self._output_counter += 1 # TODO Test - print("QuPulseTemplate._get_output() called ({}); parameters: {}.".format(self._output_counter, self.template_parameters.to_dict())) # TODO Test + #print("QuPulseTemplate._get_output() called ({}); parameters: {}.".format(self._output_counter, self.template_parameters.to_dict())) # TODO Test # TODO # first call: measure all values and resturn the first diff --git a/qcodes/loops.py b/qcodes/loops.py index 717bcbe81956..36e66d0ce2eb 100644 --- a/qcodes/loops.py +++ b/qcodes/loops.py @@ -1044,7 +1044,10 @@ def _run_loop(self, first_delay=0, action_indices=(), """ if self._is_most_outer_loop: self._set_buffered_sweep() - self._send_buffer() + self._send_buffer() # TODO Measurement windows + # TODO send meas.win. to measurement-instrument + # TODO arm measurement-inst + # at the beginning of the loop, the time to wait after setting # the loop parameter may be increased if an outer loop requested longer From b31fa9c3f8914d25e02339ccdee4b223c6f19f68 Mon Sep 17 00:00:00 2001 From: "Lukas.Lankes" Date: Thu, 18 Oct 2018 15:30:11 +0200 Subject: [PATCH 11/35] Interim status --- qcodes/__init__.py | 3 +- qcodes/instrument/parameter.py | 80 +++- .../QuTech/qupulse_instrument.py | 396 +++++++++++++----- qcodes/loops.py | 83 +++- qcodes/utils/validators.py | 15 +- 5 files changed, 432 insertions(+), 145 deletions(-) diff --git a/qcodes/__init__.py b/qcodes/__init__.py index 16f926b1f37c..0564bde4b536 100644 --- a/qcodes/__init__.py +++ b/qcodes/__init__.py @@ -60,7 +60,8 @@ from qcodes.instrument.function import Function from qcodes.instrument.parameter import ( Parameter, - BufferedParameter, + BufferedSweepableParameter, + BufferedReadableParameter, ArrayParameter, MultiParameter, StandardParameter, diff --git a/qcodes/instrument/parameter.py b/qcodes/instrument/parameter.py index 48de6f4eccb3..259d1cb19139 100644 --- a/qcodes/instrument/parameter.py +++ b/qcodes/instrument/parameter.py @@ -932,26 +932,6 @@ def sweep(self, start, stop, step=None, num=None): """ return SweepFixedValues(self, start=start, stop=stop, step=step, num=num) - - -class BufferedParameter(Parameter): - - def __init__(self, name: str, instrument: 'Instrument', - sweep_parameter_cmd: Optional[Callable]=None, - send_buffer_cmd: Optional[Callable]=None, - *args, **kwargs): - super().__init__(name, instrument=instrument, *args, **kwargs) - - self._sweep_parameter_cmd = sweep_parameter_cmd - self._send_buffer_cmd = send_buffer_cmd - - def set_buffered(self, sweep_values): - if self._sweep_parameter_cmd is not None: - self._sweep_parameter_cmd(self, sweep_values) - - def send_buffer(self): - if self._send_buffer_cmd is not None: - self._send_buffer_cmd(self) class ArrayParameter(_BaseParameter): @@ -1758,3 +1738,63 @@ def set_raw(self, value: Union[int, float]) -> None: self._save_val(value) self._wrapped_parameter.set(instrument_value) + + +class BufferedSweepableParameter(Parameter): + + def __init__(self, name: str, instrument: 'Instrument', + sweep_parameter_cmd: Optional[Callable]=None, + send_buffer_cmd: Optional[Callable]=None, + *args, **kwargs): + super().__init__(name, instrument=instrument, *args, **kwargs) + + self._sweep_parameter_cmd = sweep_parameter_cmd + self._send_buffer_cmd = send_buffer_cmd + + def set_buffered(self, sweep_values): + if self._sweep_parameter_cmd is not None: + return self._sweep_parameter_cmd(self, sweep_values) + else: + return None + + def send_buffer(self): + if self._send_buffer_cmd is not None: + return self._send_buffer_cmd(self) + else: + return None + + +class BufferedReadableParameter(ArrayParameter): + + def __init__(self, name: str, + get_buffered_cmd: Callable, + config_meas_cmd: Optional[Callable]=None, + arm_meas_cmd: Optional[Callable]=None, + **kwargs): + """ + """ + super().__init__(name, (1,), **kwargs) + + self._get_buffered_cmd = get_buffered_cmd + self._config_meas_cmd = config_meas_cmd + + def get_raw(self): + """ + """ + return self._get_buffered_cmd(self) + + def configure_measurement(self, measurement_windows): + """ + """ + if self._config_meas_cmd is not None and self.name in measurement_windows: + self._config_meas_cmd(self, measurement_windows.pop(self.name)) + + return measurement_windows + + def arm_measurement(self, measurement_windows): + """ + """ + if self._arm_meas_cmd is not None: + self._arm_meas_cmd(self) + + return measurement_windows \ No newline at end of file diff --git a/qcodes/instrument_drivers/QuTech/qupulse_instrument.py b/qcodes/instrument_drivers/QuTech/qupulse_instrument.py index c6d3eaf01e48..d31d1e54701b 100644 --- a/qcodes/instrument_drivers/QuTech/qupulse_instrument.py +++ b/qcodes/instrument_drivers/QuTech/qupulse_instrument.py @@ -7,38 +7,29 @@ """ from typing import Callable, Union, Iterable +from collections import defaultdict import numpy as np from qcodes.instrument.base import Instrument -from qcodes.instrument.parameter import ArrayParameter, BufferedParameter +from qcodes.instrument.parameter import ArrayParameter, BufferedSweepableParameter, BufferedReadableParameter import qcodes.utils.validators as vals from qupulse.pulses.pulse_template import PulseTemplate from qupulse.pulses import ForLoopPT, MappingPT, plotting -from qupulse.hardware.setup import HardwareSetup, ChannelID, _SingleChannel, MeasurementMask +from qupulse.hardware.awgs.base import AWG +from qupulse.hardware.dacs import DAC +from qupulse.hardware.setup import HardwareSetup, ChannelID, _SingleChannel, PlaybackChannel, MarkerChannel, MeasurementMask +from qupulse._program._loop import MultiChannelProgram, Loop +import warnings -class QuPulseArrayParameter(ArrayParameter): - - def __init__(self, name: str, - get_cmd: Callable, **kwargs): - """ - """ - super().__init__(name, **kwargs) - - self.get_cmd = get_cmd - - def get_raw(self): - """ - """ - return self.get_cmd() +class QuPulseTemplateParameters(Instrument): -class QuPulseParameters(Instrument): - def __init__(self, name: str, **kwargs): """ """ super().__init__(name, **kwargs) - + + def set_parameters(self, pulse_parameters, sweep_parameter_cmd=None, send_buffer_cmd=None): @@ -48,11 +39,12 @@ def set_parameters(self, pulse_parameters, if pulse_parameters is not None: for p in pulse_parameters: - self.add_parameter(p, parameter_class=BufferedParameter, + self.add_parameter(p, parameter_class=BufferedSweepableParameter, vals=vals.Numbers(), set_cmd=None, sweep_parameter_cmd=sweep_parameter_cmd, send_buffer_cmd=send_buffer_cmd) - + + def to_dict(self): result = {} @@ -60,38 +52,146 @@ def to_dict(self): result[p] = self.parameters[p].get() return result + + +class QuPulseDACChannel(BufferedReadableParameter): + + def __init__(self, name: str, get_buffered_cmd: Callable, config_meas_cmd: Callable, + measurement_mask: Union[MeasurementMask, Iterable[MeasurementMask]], + **kwargs): + super().__init__(name, get_buffered_cmd=get_buffered_cmd, config_meas_cmd=config_meas_cmd, **kwargs) + + self.measurement_mask = measurement_mask + self.ready = False + + +class QuPulseDACInstrument(Instrument): + + def __init__(self, name: str, + **kwargs): + """ + """ + super().__init__(name, **kwargs) + + self.add_parameter("program_name", + set_cmd=None, + vals=vals.Strings(), + label="Program name") + + self.program_name.set(self.name + "_program") + self._measurements = defaultdict(dict) + self._data = None -class QuPulseInstrument(Instrument): + + def add_measurement(self, name: str, + measurement_mask: Union[MeasurementMask, Iterable[MeasurementMask]]): + + if isinstance(measurement_mask, MeasurementMask): + if name in self.parameters: + warnings.warn( + "You add a measurement mask to an already registered measurement name. This is deprecated and will be removed in a future version. Please add all measurement masks at once.", + DeprecationWarning) + measurement_mask = self.parameters[name].measurement_mask | {measurement_mask} + else: + measurement_mask = {measurement_mask} + else: + try: + measurement_mask = set(measurement_mask) + except TypeError: + raise TypeError('Mask must be (a list) of type MeasurementMask') + + for old_name, parameter in self.parameters.items(): + if isinstance(parameter, QuPulseDACChannel) and (measurement_mask & parameter.measurement_mask): + raise ValueError('Measurement mask already registered for measurement "{}"'.format(old_name)) + + if name in self.parameters: + self.parameters[name].measurement_mask = measurement_mask + else: + self.add_parameter(name, parameter_class=QuPulseDACChannel, get_buffered_cmd=self._get_buffered, config_meas_cmd=self._configure_measurement, arm_meas_cmd=self._arm_measurement, measurement_mask=measurement_mask) + + + def _configure_measurement(self, parameter, measurement_window): + print("QuPulseDACInstrument._configure_measurement({}, {})".format(parameter, measurement_window)) + + if not parameter.ready: + self._measurements[parameter.measurement_mask.dac][parameter.name] = measurement_window + else: + raise AttributeError("It is not possible to define multiple measurement windows on one measurement channel ({}).".format(parameter.name)) + + + def _arm_measurement(self, parameter): + parameter.ready = True + all_ready = True + + for p in self.parameters: + if not p.ready: + all_ready = False + break + + if all_ready: + for dac, dac_windows in self._measurements.items(): + dac.register_window_measurement(self.program_name, dac_windows) + + for dac in self._measurements.keys(): + dac.arm_program(self.program_name) + + + def _get_buffered(self, parameter): + if not self._data: + self._data = {} + for dac, dac_windows in self._measurements.items(): + data = dac.measure_program() + if data: + self._data = {**self._data, **data} + + # TODO + n = 1 # Count of measurement values, depends on Operation (e.g. Downsampling only produces one measurement per measurement-window) + + values = self._data[parameter.measurement_mask.mask_name][:n] + del self._data[parameter.measurement_mask.mask_name][:n] + + return values + + +class QuPulseAWGInstrument(Instrument): - def __init__(self, - name: str, - program_name: str, + def __init__(self, name: str, pulse_template: PulseTemplate=None, **kwargs) -> None: """ """ super().__init__(name, **kwargs); - self.program_name = program_name - self.add_parameter("template", get_cmd=self._get_template, set_cmd=self._set_template, - vals=vals.ObjectTypeValidator(PulseTemplate), + vals=vals.ObjectTypeValidator(PulseTemplate, type(None)), label="Pulse template") - self.add_parameter("output", - parameter_class=QuPulseArrayParameter, - shape=(1,), - get_cmd=self._get_output, - label="Output data") + self.add_parameter("program_name", + set_cmd=None, + vals=vals.Strings(), + label="Program name") + self.add_parameter("channel_mapping", + set_cmd=None, + vals=vals.ObjectTypeValidator(dict, type(None)), + label="Channel mapping") + self.add_parameter("measurement_mapping", + set_cmd=None, + vals=vals.ObjectTypeValidator(dict, type(None)), + label="Measurement mapping") + self.add_parameter("registered_channels", + get_cmd=self._get_registered_channels, + label="Registered channels") self.add_submodule("template_parameters", - QuPulseParameters(self.name + "_template_parameters")) + QuPulseTemplateParameters(self.name + "_template_parameters")) if pulse_template: self.template.set(pulse_template) - self._hardware_setup = HardwareSetup() + self.program_name.set(self.name + "_program") + + self._channel_map = {} self._output_counter = 0 # TODO Test self._send_counter = 0 # TODO Test @@ -99,35 +199,6 @@ def __init__(self, self._reset_counter = 0 # TODO Test - def set_channel(self, identifier: ChannelID, - single_channel: Union[_SingleChannel, Iterable[_SingleChannel]], - allow_multiple_registration: bool=False) -> None: - """ - HardwareSetup.set_channel - """ - return self._hardware_setup.set_channel(identifier, - single_channel, - allow_multiple_registration=allow_multiple_registration) - - - def set_measurement(self, measurement_name: str, - measurement_mask: Union[MeasurementMask, Iterable[MeasurementMask]], - allow_multiple_registration: bool=False): - """ - HardwareSetup.set_measurement - """ - return self._hardware_setup.set_measurement(measurement_name, - measurement_mask, - allow_multiple_registration=allow_multiple_registration) - - - def rm_channel(self, identifier: ChannelID) -> None: - """ - HardwareSetup.rm_channel - """ - return self._hardware_setup.rm_channel(identifier) - - def _get_template(self): """ Gets the PulseTemplate @@ -149,6 +220,50 @@ def _set_template(self, value): self._send_buffer) + def set_channel(self, identifier: ChannelID, + single_channel: Union[_SingleChannel, Iterable[_SingleChannel]], + allow_multiple_registration: bool=False) -> None: + """ + """ + if isinstance(single_channel, (PlaybackChannel, MarkerChannel)): + if identifier in self._channel_map: + warnings.warn( + "You add a single hardware channel to an already existing channel id. This is deprecated and will be removed in a future version. Please add all channels at once.", + DeprecationWarning) + single_channel = self._channel_map[identifier] | {single_channel} + else: + single_channel = {single_channel} + else: + try: + single_channel = set(single_channel) + except TypeError: + raise TypeError('Channel must be (a list of) either a playback or a marker channel') + + if not allow_multiple_registration: + for ch_id, channel_set in self._channel_map.items(): + if single_channel & channel_set: + raise ValueError('Channel already registered as {} for channel {}'.format( + type(self._channel_map[ch_id]).__name__, ch_id)) + + for s_channel in single_channel: + if not isinstance(s_channel, (PlaybackChannel, MarkerChannel)): + raise TypeError('Channel must be (a list of) either a playback or a marker channel') + + self._channel_map[identifier] = single_channel + + + def remove_channel(self, identifier: ChannelID): + """ + """ + self._channel_map.pop(identifier) + + + def _get_registered_channels(self): + """ + """ + return self._channel_map + + def _sweep_parameter(self, parameter, sweep_values): """ @@ -158,12 +273,13 @@ def _sweep_parameter(self, parameter, sweep_values): for l in self._loops: if l['parameter'] == parameter.name: - raise AttributeError('It is not supported to sweep the same parameter ({}) twice.'.format(parameter)) + raise AttributeError('It is not supported to sweep the same parameter ({}) more than once.'.format(parameter)) loop = { - "parameter" : parameter.name, - "iterator" : "__" + parameter.name + "_it", - "sweep_values" : sweep_values + 'parameter' : parameter.name, + 'iterator' : '__' + parameter.name + '_it', + 'sweep_values' : sweep_values, + 'is_sent' : False } self._loops.append(loop) @@ -175,6 +291,24 @@ def _send_buffer(self, parameter): self._send_counter += 1 # TODO Test print("QuPulseTemplate._send_buffer({}) called ({})".format(parameter, self._send_counter)) # TODO Test + send = True + + for loop in self._loops: + if loop['parameter'] == parameter.name: + loop['is_sent'] = True + elif not loop['is_sent']: + send = False + + if send: + return self._really_send_buffer() + else: + return None + + def _really_send_buffer(self): + """ + """ + print(" --> Send") # TODO Test + parameter_mapping = {} params = self.template_parameters.to_dict() @@ -190,35 +324,103 @@ def _send_buffer(self, parameter): for loop in reversed(self._loops): pt = ForLoopPT(pt, loop['iterator'], range(len(loop['sweep_values']))) - program = pt.create_program(parameters=params) - - - - self._hardware_setup.register_program(self.program_name, program, update=True) - - awg = next(iter(self._hardware_setup.known_awgs)) # TODO - dac = next(iter(self._hardware_setup.known_dacs)) # TODO - - awg._device.set_chan_state([False, False, True, True]) - - self._hardware_setup.arm_program(self.program_name) - - awg.run_current_program() - - self.scanline_data = dac.card.extractNextScanline() - - for measurement in self._hardware_setup._measurement_map: - - self.DS_C = self.scanline_data.operationResults['DS_C'].getAsVoltage(dac.config.inputConfiguration[2].inputRange) - self.DS_D = self.scanline_data.operationResults['DS_D'].getAsVoltage(dac.config.inputConfiguration[2].inputRange) - - self.DS_C = self.DS_C.reshape((40, 40)) - self.DS_D = self.DS_D.reshape((40, 40)) - - # resetting loops because the buffer is already at the hardware - self._loops.clear() - print(" --> Reset") # TODO Test - + program = pt.create_program(parameters=params, channel_mapping=self.channel_mapping.get()) + + measurement_windows = self._run_program(program) +# self._hardware_setup.register_program(self.program_name.get(), program, update=True) +# +# awg = next(iter(self._hardware_setup.known_awgs)) # TODO +# dac = next(iter(self._hardware_setup.known_dacs)) # TODO +# +# awg._device.set_chan_state([False, False, True, True]) +# +# self._hardware_setup.arm_program(self.program_name.get()) +# +# awg.run_current_program() +# +# self.scanline_data = dac.card.extractNextScanline() +# +# for measurement in self._hardware_setup._measurement_map: +# +# self.DS_C = self.scanline_data.operationResults['DS_C'].getAsVoltage(dac.config.inputConfiguration[2].inputRange) +# self.DS_D = self.scanline_data.operationResults['DS_D'].getAsVoltage(dac.config.inputConfiguration[2].inputRange) +# +# self.DS_C = self.DS_C.reshape((40, 40)) +# self.DS_D = self.DS_D.reshape((40, 40)) +# +# # resetting loops because the buffer is already at the hardware +# self._loops.clear() +# print(" --> Reset") # TODO Test + return measurement_windows + + + def _run_program(self, program, update=False): + """ + """ + mcp = MultiChannelProgram(program) + if mcp.channels - set(self._channel_map.keys()): + raise KeyError('The following channels are unknown to the HardwareSetup: {}'.format( + mcp.channels - set(self._channel_map.keys()))) + + temp_measurement_windows = defaultdict(list) + for program in mcp.programs.values(): + for mw_name, begins_lengths in program.get_measurement_windows().items(): + temp_measurement_windows[mw_name].append(begins_lengths) + + measurement_windows = dict() + while temp_measurement_windows: + mw_name, begins_lengths_deque = temp_measurement_windows.popitem() + + begins, lengths = zip(*begins_lengths_deque) + measurement_windows[mw_name] = ( + np.concatenate(begins), + np.concatenate(lengths) + ) + + handled_awgs = set() + for channels, program in mcp.programs.items(): + awgs_to_channel_info = dict() + + def get_default_info(awg): + return ([None] * awg.num_channels, + [None] * awg.num_channels, + [None] * awg.num_markers) + + for channel_id in channels: + for single_channel in self._channel_map[channel_id]: + playback_ids, voltage_trafos, marker_ids = \ + awgs_to_channel_info.setdefault(single_channel.awg, get_default_info(single_channel.awg)) + + if isinstance(single_channel, PlaybackChannel): + playback_ids[single_channel.channel_on_awg] = channel_id + voltage_trafos[single_channel.channel_on_awg] = single_channel.voltage_transformation + elif isinstance(single_channel, MarkerChannel): + marker_ids[single_channel.channel_on_awg] = channel_id + + for awg, (playback_ids, voltage_trafos, marker_ids) in awgs_to_channel_info.items(): + if awg in handled_awgs: + raise ValueError('AWG has two programs') + else: + handled_awgs.add(awg) + awg.upload(self.program_name.get(), + program=program, + channels=tuple(playback_ids), + markers=tuple(marker_ids), + force=update, + voltage_transformation=tuple(voltage_trafos)) + + known_awgs = {single_channel.awg + for single_channel_set in self._channel_map.values() + for single_channel in single_channel_set} + + for awg in known_awgs: + if awg in handled_awgs: + awg.arm(self.program_name.get()) + else: + # The other AWGs should ignore the trigger + awg.arm(None) + + return measurement_windows def _get_output(self): """ diff --git a/qcodes/loops.py b/qcodes/loops.py index 36e66d0ce2eb..64f863b7b830 100644 --- a/qcodes/loops.py +++ b/qcodes/loops.py @@ -56,7 +56,7 @@ from qcodes.data.data_array import DataArray from qcodes.utils.helpers import wait_secs, full_class, tprint from qcodes.utils.metadata import Metadatable -from qcodes.instrument.parameter import BufferedParameter +from qcodes.instrument.parameter import _BaseParameter, BufferedSweepableParameter, BufferedReadableParameter from .actions import (_actions_snapshot, Task, Wait, _Measure, _Nest, BreakIf, _QcodesBreak) @@ -158,7 +158,7 @@ def loop(self, sweep_values, delay=0): return out - def buffered_loop(self, sweep_values, delay=0): + def buffered_loop(self, sweep_values): """ Nest another loop inside this one. @@ -179,9 +179,9 @@ def buffered_loop(self, sweep_values, delay=0): if out.nested_loop: # nest this new loop inside the deepest level - out.nested_loop = out.nested_loop.buffered_loop(sweep_values, delay) + out.nested_loop = out.nested_loop.buffered_loop(sweep_values) else: - out.nested_loop = BufferedLoop(sweep_values, delay) + out.nested_loop = BufferedLoop(sweep_values) return out @@ -334,12 +334,14 @@ def snapshot_base(self, update=False): class BufferedLoop(Loop): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, sweep_values, station=None): + super().__init__(sweep_values, station=station) + + if not isinstance(sweep_values.parameter, BufferedSweepableParameter): + raise TypeError("The sweep values of a buffered loop must sweep a buffered parameter.") def _copy(self): - out = BufferedLoop(self.sweep_values, self.delay, - progress_interval=self.progress_interval) + out = BufferedLoop(self.sweep_values) out.nested_loop = self.nested_loop out.then_actions = self.then_actions out.station = self.station @@ -349,9 +351,6 @@ def loop(self, *args, **kwargs): raise AttributeError('It is not supported to nest a \'normal\' Loop in a BufferedLoop.') def each(self, *actions): - return self._each(True, *actions) - - def _each(self, is_most_outer_loop: bool, *actions): """ Perform a set of actions at each setting of this loop. TODO(setting vs setpoints) ? better be verbose. @@ -371,23 +370,26 @@ def _each(self, is_most_outer_loop: bool, *actions): # check for nested Loops, and activate them with default measurement for i, action in enumerate(actions): + print("XXX: {}".format(type(action))) if isinstance(action, BufferedLoop): default = Station.default.default_measurement actions[i] = action.each(*default) elif isinstance(action, Loop): raise AttributeError('It is not supported to nest a \'normal\' Loop in a BufferedLoop.') + elif isinstance(action, _BaseParameter) and not isinstance(action, BufferedReadableParameter): + raise AttributeError("The measuring parameters in a buffered loop must be BufferedReadableParameters.") self.validate_actions(*actions) if self.nested_loop: # recurse into the innermost loop and apply these actions there - actions = [self.nested_loop._each(False, *actions)] + actions = [self.nested_loop.each(*actions)] - return BufferedActiveLoop(is_most_outer_loop, - self.sweep_values, self.delay, *actions, + return BufferedActiveLoop(self.sweep_values, *actions, then_actions=self.then_actions, station=self.station, progress_interval=self.progress_interval, - bg_task=self.bg_task, bg_final_task=self.bg_final_task, bg_min_delay=self.bg_min_delay) + bg_task=self.bg_task, bg_final_task=self.bg_final_task, + bg_min_delay=self.bg_min_delay) def _attach_then_actions(loop, actions, overwrite): @@ -1014,10 +1016,12 @@ def _wait(self, delay): class BufferedActiveLoop(ActiveLoop): - def __init__(self, is_most_outer_loop: bool, *args, **kwargs): - super().__init__(*args, **kwargs) - - self._is_most_outer_loop = is_most_outer_loop + def __init__(self, sweep_values, *actions, then_actions=(), + station=None, progress_interval=None, bg_task=None, + bg_final_task=None, bg_min_delay=None): + super().__init__(sweep_values, 0, *actions, then_actions=then_actions, + station=station, progress_interval=progress_interval, bg_task=bg_task, + bg_final_task=bg_final_task, bg_min_delay=bg_min_delay) def _set_buffered_sweep(self): self.sweep_values.parameter.set_buffered(list(self.sweep_values)) @@ -1026,7 +1030,35 @@ def _set_buffered_sweep(self): self.actions[0]._set_buffered_sweep() def _send_buffer(self): - self.sweep_values.parameter.send_buffer() + mw = self.sweep_values.parameter.send_buffer() + if mw is None: mw = {} + + if len(self.actions) == 1 and isinstance(self.actions[0], BufferedActiveLoop): + mw_tmp = self.actions[0]._send_buffer() + + if mw_tmp is not None: + mw = {**mw, **mw_tmp} + + return mw + + def _configure_measurement(self, measurement_windows): + for action in self.actions: + if isinstance(action, BufferedReadableParameter): # Measurement + measurement_windows = action.configure_measurement(measurement_windows) + + if len(self.actions) == 1 and isinstance(self.actions[0], BufferedActiveLoop): + measurement_windows = self.actions[0]._configure_measurement(measurement_windows) + + return measurement_windows + + def _arm_measurement(self): + for action in self.actions: + if isinstance(action, BufferedReadableParameter): # Measurement + action.arm_measurement() + + if len(self.actions) == 1 and isinstance(self.actions[0], BufferedActiveLoop): + self.actions[0]._arm_measurement() + def _run_loop(self, first_delay=0, action_indices=(), loop_indices=(), current_values=(), @@ -1042,9 +1074,16 @@ def _run_loop(self, first_delay=0, action_indices=(), signal_queue: queue to communicate with main process directly ignore_kwargs: for compatibility with other loop tasks """ - if self._is_most_outer_loop: + + if self._nest_first: self._set_buffered_sweep() - self._send_buffer() # TODO Measurement windows + measurement_windows = self._send_buffer() + remaining_measurement_windows = self._configure_measurement(measurement_windows) + + if remaining_measurement_windows: + raise AttributeError("The following measurement windows have no corresponding channel on the instrument: {}".format(measurement_windows.keys())) + + self._arm_measurement() # TODO send meas.win. to measurement-instrument # TODO arm measurement-inst diff --git a/qcodes/utils/validators.py b/qcodes/utils/validators.py index 4c157ed60b3f..d456ec926026 100644 --- a/qcodes/utils/validators.py +++ b/qcodes/utils/validators.py @@ -625,19 +625,24 @@ def __repr__(self): class ObjectTypeValidator(Validator): - def __init__(self, allowed_type=object): + def __init__(self, *allowed_types: type): """ Validator for objects """ self._valid_values = [0] - self._allowed_type = allowed_type + self._allowed_types = allowed_types def validate(self, value, context=''): - if not isinstance(value, self._allowed_type): + valid = False + for allowed_type in self._allowed_types: + if isinstance(value, allowed_type): + valid = True + break + if not valid: raise TypeError('{} is a forbidden type; {}'.format(type(value), context)) def __repr__(self): - if self._allowed_type is None: + if self._allowed_types is None: return '' else: - return ''.format(self._allowed_type) \ No newline at end of file + return ''.format(self._allowed_types) \ No newline at end of file From 92330bd4fe09d74e58ad288b6c772c4e6af4d55f Mon Sep 17 00:00:00 2001 From: "Lukas.Lankes" Date: Thu, 8 Nov 2018 12:23:18 +0100 Subject: [PATCH 12/35] Integrated qupulse into QCoDeS. PulseTemplates can now be looped on the hardware with a BufferedLoop. --- qcodes/instrument/parameter.py | 64 ++- .../QuTech/qupulse_instrument.py | 532 ++++++++++++------ qcodes/loops.py | 37 +- 3 files changed, 427 insertions(+), 206 deletions(-) diff --git a/qcodes/instrument/parameter.py b/qcodes/instrument/parameter.py index 259d1cb19139..856b191c8e30 100644 --- a/qcodes/instrument/parameter.py +++ b/qcodes/instrument/parameter.py @@ -1741,23 +1741,45 @@ def set_raw(self, value: Union[int, float]) -> None: class BufferedSweepableParameter(Parameter): + """ + A parameter that creates buffered sweeps in the instrument. + """ - def __init__(self, name: str, instrument: 'Instrument', + def __init__(self, name: str, + instrument: 'Instrument', sweep_parameter_cmd: Optional[Callable]=None, send_buffer_cmd: Optional[Callable]=None, *args, **kwargs): + """ + Creates a BufferedSweepableParameter that creates buffered sweeps in + the instrument. + + Args: + name: Name of the parameter + instrument: Parent instrument + sweep_parameter_cmd: Function that is called when the parameter is + swept in a buffered loop. + send_buffer_cmd: Function that is called when the buffer should be + sent to the device. + """ super().__init__(name, instrument=instrument, *args, **kwargs) self._sweep_parameter_cmd = sweep_parameter_cmd self._send_buffer_cmd = send_buffer_cmd def set_buffered(self, sweep_values): + """ + Define a buffered sweep for the parameter. + """ if self._sweep_parameter_cmd is not None: return self._sweep_parameter_cmd(self, sweep_values) else: return None def send_buffer(self): + """ + Send the buffer to the device. + """ if self._send_buffer_cmd is not None: return self._send_buffer_cmd(self) else: @@ -1765,36 +1787,56 @@ def send_buffer(self): class BufferedReadableParameter(ArrayParameter): + """ + An ArrayParameter for buffered measurements. + """ def __init__(self, name: str, get_buffered_cmd: Callable, config_meas_cmd: Optional[Callable]=None, arm_meas_cmd: Optional[Callable]=None, - **kwargs): + shape: Sequence[int]=(1,), + **kwargs) -> None: """ + Creates a BufferdReadableParameter for buffered measurements. + + Args: + name: Name of the parameter + get_buffered_cmd: Function which is called when this parameter is + measured. + config_meas_cmd: Function which is called when a measurement is + configured for this parameter. + arm_meas_cmd: Function which is called when the parameter should be + armed. """ - super().__init__(name, (1,), **kwargs) + super().__init__(name, shape, **kwargs) self._get_buffered_cmd = get_buffered_cmd self._config_meas_cmd = config_meas_cmd + self._arm_meas_cmd = arm_meas_cmd def get_raw(self): """ + Overridden function for the measurements. + + Returns: + Measurement values. """ return self._get_buffered_cmd(self) - def configure_measurement(self, measurement_windows): - """ + def configure_measurement(self, measurement_windows) -> None: """ - if self._config_meas_cmd is not None and self.name in measurement_windows: - self._config_meas_cmd(self, measurement_windows.pop(self.name)) + Configure the measurement for this parameter in the instrument. - return measurement_windows + Args: + measurement_windows: Time windows where the parameter is measured. + """ + if self._config_meas_cmd is not None: + self._config_meas_cmd(self, measurement_windows) - def arm_measurement(self, measurement_windows): + def arm_measurement(self) -> None: """ + Arm the measurement for this parameter in the instrument. """ if self._arm_meas_cmd is not None: self._arm_meas_cmd(self) - - return measurement_windows \ No newline at end of file diff --git a/qcodes/instrument_drivers/QuTech/qupulse_instrument.py b/qcodes/instrument_drivers/QuTech/qupulse_instrument.py index d31d1e54701b..ceab0f434095 100644 --- a/qcodes/instrument_drivers/QuTech/qupulse_instrument.py +++ b/qcodes/instrument_drivers/QuTech/qupulse_instrument.py @@ -6,34 +6,59 @@ @author: l.lankes """ -from typing import Callable, Union, Iterable +from typing import Callable, Union, Iterable, Optional, Dict from collections import defaultdict import numpy as np +import warnings from qcodes.instrument.base import Instrument -from qcodes.instrument.parameter import ArrayParameter, BufferedSweepableParameter, BufferedReadableParameter +from qcodes.instrument.parameter import BufferedSweepableParameter, BufferedReadableParameter import qcodes.utils.validators as vals + from qupulse.pulses.pulse_template import PulseTemplate -from qupulse.pulses import ForLoopPT, MappingPT, plotting -from qupulse.hardware.awgs.base import AWG from qupulse.hardware.dacs import DAC -from qupulse.hardware.setup import HardwareSetup, ChannelID, _SingleChannel, PlaybackChannel, MarkerChannel, MeasurementMask -from qupulse._program._loop import MultiChannelProgram, Loop -import warnings +from qupulse.hardware.setup import ChannelID, _SingleChannel, PlaybackChannel, MarkerChannel, MeasurementMask +from qupulse._program._loop import MultiChannelProgram + +from atsaverage.operations import OperationDefinition, Downsample class QuPulseTemplateParameters(Instrument): + """ + Submodule for the QuPulseAWGInstrument. It automatically generates + QCoDeS-parameters from qupulse-template-parameters. + + Parameters: + All parameters of the parent-instruments PulseTemplate-parameters. + """ - def __init__(self, name: str, **kwargs): + def __init__(self, name: str, **kwargs) -> None: """ + Creates the a QuPulseTemplateParameters object. + + QuPulseTemplateParameters is a submodule for the QuPulseAWGInstrument. + It automatically generates QCoDeS-parameters from qupulse-template- + parameters. + + Args: + name: The name of this instrument """ super().__init__(name, **kwargs) - def set_parameters(self, pulse_parameters, - sweep_parameter_cmd=None, - send_buffer_cmd=None): + def set_parameters(self, pulse_parameters: Iterable[str], + sweep_parameter_cmd: Optional[Callable]=None, + send_buffer_cmd: Optional[Callable]=None) -> None: """ + Adds sweepable QCoDeS-parameters from qupulse-template-parameters. + + Args: + pulse_parameters: A set of all parameters of the PulseTemplate. + sweep_parameter_cmd: Function that should be called when a + parameter should be sweept in a buffered loop. + send_buffer_cmd: Function that should be called when all buffered + sweeps are set. This function sends the complete buffer to the + AWG. """ self.parameters.clear() @@ -45,7 +70,14 @@ def set_parameters(self, pulse_parameters, send_buffer_cmd=send_buffer_cmd) - def to_dict(self): + def to_dict(self) -> Dict: + """ + Translates the parameters to a parameter-dictionary which can be used + to create the waveform program. + + Returns: + Dictionary of parameter names and values. + """ result = {} for p in self.parameters: @@ -54,112 +86,32 @@ def to_dict(self): return result -class QuPulseDACChannel(BufferedReadableParameter): - - def __init__(self, name: str, get_buffered_cmd: Callable, config_meas_cmd: Callable, - measurement_mask: Union[MeasurementMask, Iterable[MeasurementMask]], - **kwargs): - super().__init__(name, get_buffered_cmd=get_buffered_cmd, config_meas_cmd=config_meas_cmd, **kwargs) - - self.measurement_mask = measurement_mask - self.ready = False - - -class QuPulseDACInstrument(Instrument): - - def __init__(self, name: str, - **kwargs): - """ - """ - super().__init__(name, **kwargs) - - self.add_parameter("program_name", - set_cmd=None, - vals=vals.Strings(), - label="Program name") - - self.program_name.set(self.name + "_program") - - self._measurements = defaultdict(dict) - self._data = None - - - def add_measurement(self, name: str, - measurement_mask: Union[MeasurementMask, Iterable[MeasurementMask]]): - - if isinstance(measurement_mask, MeasurementMask): - if name in self.parameters: - warnings.warn( - "You add a measurement mask to an already registered measurement name. This is deprecated and will be removed in a future version. Please add all measurement masks at once.", - DeprecationWarning) - measurement_mask = self.parameters[name].measurement_mask | {measurement_mask} - else: - measurement_mask = {measurement_mask} - else: - try: - measurement_mask = set(measurement_mask) - except TypeError: - raise TypeError('Mask must be (a list) of type MeasurementMask') - - for old_name, parameter in self.parameters.items(): - if isinstance(parameter, QuPulseDACChannel) and (measurement_mask & parameter.measurement_mask): - raise ValueError('Measurement mask already registered for measurement "{}"'.format(old_name)) - - if name in self.parameters: - self.parameters[name].measurement_mask = measurement_mask - else: - self.add_parameter(name, parameter_class=QuPulseDACChannel, get_buffered_cmd=self._get_buffered, config_meas_cmd=self._configure_measurement, arm_meas_cmd=self._arm_measurement, measurement_mask=measurement_mask) - - - def _configure_measurement(self, parameter, measurement_window): - print("QuPulseDACInstrument._configure_measurement({}, {})".format(parameter, measurement_window)) - - if not parameter.ready: - self._measurements[parameter.measurement_mask.dac][parameter.name] = measurement_window - else: - raise AttributeError("It is not possible to define multiple measurement windows on one measurement channel ({}).".format(parameter.name)) - - - def _arm_measurement(self, parameter): - parameter.ready = True - all_ready = True - - for p in self.parameters: - if not p.ready: - all_ready = False - break - - if all_ready: - for dac, dac_windows in self._measurements.items(): - dac.register_window_measurement(self.program_name, dac_windows) - - for dac in self._measurements.keys(): - dac.arm_program(self.program_name) - - - def _get_buffered(self, parameter): - if not self._data: - self._data = {} - for dac, dac_windows in self._measurements.items(): - data = dac.measure_program() - if data: - self._data = {**self._data, **data} - - # TODO - n = 1 # Count of measurement values, depends on Operation (e.g. Downsampling only produces one measurement per measurement-window) - - values = self._data[parameter.measurement_mask.mask_name][:n] - del self._data[parameter.measurement_mask.mask_name][:n] - - return values - - class QuPulseAWGInstrument(Instrument): + """ + An instrument that wraps an qupulse-PulseTemplate and uses qupulse to send + it to an AWG. + + Parameters: + template: PulseTemplate taht is handled by this instrument. + program_name: Name of the program which will be send to the AWG. + channel_mapping: Channel-mapping for the PulseTemplate. + measurement_mapping: Channel-mapping for the measurement channels of + the PulseTemplate. + registered_channels: A dictionary of all channels on the AWG hardware. + + Submodules: + template_parameters: Automatically generated parameters from the + PulseTemplate + """ def __init__(self, name: str, - pulse_template: PulseTemplate=None, **kwargs) -> None: """ + Creates an QuPulseAWGInstrument that wraps an qupulse-PulseTemplate and + uses qupulse to send it to an AWG. + + Args: + name: Name of the instrument """ super().__init__(name, **kwargs); @@ -186,31 +138,25 @@ def __init__(self, name: str, self.add_submodule("template_parameters", QuPulseTemplateParameters(self.name + "_template_parameters")) - if pulse_template: - self.template.set(pulse_template) - self.program_name.set(self.name + "_program") self._channel_map = {} - self._output_counter = 0 # TODO Test - self._send_counter = 0 # TODO Test - self._build_counter = 0 # TODO Test - self._reset_counter = 0 # TODO Test - - def _get_template(self): + def _get_template(self) -> PulseTemplate: """ - Gets the PulseTemplate + Returns: + PulseTemplate """ return self._template - def _set_template(self, value): + def _set_template(self, value: PulseTemplate) -> None: """ Sets the PulseTemplate and extracts its parameters - template: PulseTemplate + Args: + value: PulseTemplate """ self._template = value self._loops = [] @@ -221,9 +167,13 @@ def _set_template(self, value): def set_channel(self, identifier: ChannelID, - single_channel: Union[_SingleChannel, Iterable[_SingleChannel]], - allow_multiple_registration: bool=False) -> None: + single_channel: Union[_SingleChannel, Iterable[_SingleChannel]]) -> None: """ + Adds a hardware channel to the instrument. + + Args: + identifier: Channel name + single_channel: Object (or set of objects) that represents the hardware channel """ if isinstance(single_channel, (PlaybackChannel, MarkerChannel)): if identifier in self._channel_map: @@ -239,11 +189,10 @@ def set_channel(self, identifier: ChannelID, except TypeError: raise TypeError('Channel must be (a list of) either a playback or a marker channel') - if not allow_multiple_registration: - for ch_id, channel_set in self._channel_map.items(): - if single_channel & channel_set: - raise ValueError('Channel already registered as {} for channel {}'.format( - type(self._channel_map[ch_id]).__name__, ch_id)) + for ch_id, channel_set in self._channel_map.items(): + if single_channel & channel_set: + raise ValueError('Channel already registered as {} for channel {}'.format( + type(self._channel_map[ch_id]).__name__, ch_id)) for s_channel in single_channel: if not isinstance(s_channel, (PlaybackChannel, MarkerChannel)): @@ -252,25 +201,32 @@ def set_channel(self, identifier: ChannelID, self._channel_map[identifier] = single_channel - def remove_channel(self, identifier: ChannelID): + def remove_channel(self, identifier: ChannelID) -> None: """ + Removes an existing hardware channel from the instrument object. + + Args: + identifier: Channel name """ self._channel_map.pop(identifier) - def _get_registered_channels(self): + def _get_registered_channels(self) -> Dict: """ + Returns: + A dictionary of all hardware channels. """ return self._channel_map - def _sweep_parameter(self, parameter, sweep_values): + def _sweep_parameter(self, parameter, sweep_values) -> None: """ + Adds the sweep information to a list, to build up a single buffer later + Args: + parameter: Parameter to sweep + sweep_values: Values the parameter should be swept over. """ - self._build_counter += 1 # TODO Test - print("QuPulseTemplate._build_buffer({}, {}) called ({})".format(parameter, sweep_values, self._build_counter)) - for l in self._loops: if l['parameter'] == parameter.name: raise AttributeError('It is not supported to sweep the same parameter ({}) more than once.'.format(parameter)) @@ -284,13 +240,18 @@ def _sweep_parameter(self, parameter, sweep_values): self._loops.append(loop) - def _send_buffer(self, parameter): - """ - Sends the buffer to the device and the device waits for the trigger + def _send_buffer(self, parameter) -> Dict: """ - self._send_counter += 1 # TODO Test - print("QuPulseTemplate._send_buffer({}) called ({})".format(parameter, self._send_counter)) # TODO Test + Waits until this function is called for all swept parameters. Then the + buffer will be built and sent to the device. + Args: + parameter: The parameter that calls the function + + Returns: + Dictionary of the measurement windows if the function was called + the last parameter. If not it returns None. + """ send = True for loop in self._loops: @@ -304,11 +265,14 @@ def _send_buffer(self, parameter): else: return None - def _really_send_buffer(self): + def _really_send_buffer(self) -> Dict: """ - """ - print(" --> Send") # TODO Test + Builts up and sends the buffer to the device and makes the device wait + for the trigger + Returns: + Dictionary of the measurement windows. + """ parameter_mapping = {} params = self.template_parameters.to_dict() @@ -324,38 +288,24 @@ def _really_send_buffer(self): for loop in reversed(self._loops): pt = ForLoopPT(pt, loop['iterator'], range(len(loop['sweep_values']))) - program = pt.create_program(parameters=params, channel_mapping=self.channel_mapping.get()) + program = pt.create_program(parameters=params, + channel_mapping=self.channel_mapping.get(), + measurement_mapping=self.measurement_mapping.get()) measurement_windows = self._run_program(program) -# self._hardware_setup.register_program(self.program_name.get(), program, update=True) -# -# awg = next(iter(self._hardware_setup.known_awgs)) # TODO -# dac = next(iter(self._hardware_setup.known_dacs)) # TODO -# -# awg._device.set_chan_state([False, False, True, True]) -# -# self._hardware_setup.arm_program(self.program_name.get()) -# -# awg.run_current_program() -# -# self.scanline_data = dac.card.extractNextScanline() -# -# for measurement in self._hardware_setup._measurement_map: -# -# self.DS_C = self.scanline_data.operationResults['DS_C'].getAsVoltage(dac.config.inputConfiguration[2].inputRange) -# self.DS_D = self.scanline_data.operationResults['DS_D'].getAsVoltage(dac.config.inputConfiguration[2].inputRange) -# -# self.DS_C = self.DS_C.reshape((40, 40)) -# self.DS_D = self.DS_D.reshape((40, 40)) -# -# # resetting loops because the buffer is already at the hardware -# self._loops.clear() -# print(" --> Reset") # TODO Test + return measurement_windows - def _run_program(self, program, update=False): + def _run_program(self, program, update=True) -> Dict: """ + Sends the buffer to the device and arms it. + + Args: + program: Program object + + Returns: + Dictionary of the measurement windows. """ mcp = MultiChannelProgram(program) if mcp.channels - set(self._channel_map.keys()): @@ -421,16 +371,238 @@ def get_default_info(awg): awg.arm(None) return measurement_windows + + +class QuPulseDACChannel(BufferedReadableParameter): + """ + A parameter for an operation on a measurement mask. These parameters can be + measured in a QCoDeS-loop. + + Parameters: + measurement_window: Measurement windows for this parameter. + operation: Operation that was executed on the measurement. + configured: True, if a measurement was configured for this parameter. + ready: True, if the parameter is ready to be armed and measured. + """ + + def __init__(self, name: str, + get_buffered_cmd: Callable, + config_meas_cmd: Callable, + arm_meas_cmd: Callable, + operation: OperationDefinition, + **kwargs) -> None: + """ + Creates a QuPulseDACChannel parameter for an operation on a measurement + mask. These parameters can be measured in a QCoDeS-loop. + + Args: + name: Name of the parameter + get_buffered_cmd: Function which is called when this parameter is + measured. + config_meas_cmd: Function which is called when a measurement is + configured for this parameter. + arm_meas_cmd: Function which is called when the parameter should be + armed. + operation: The operation which is executed on the measurement. + (Only Downsample operations are supported yet) + """ + if isinstance(operation, Downsample): + shape = (1,) + else: + raise NotImplementedError('Operations of type {} are not supported yet.'.format(type(operation))) + + super().__init__(name, + get_buffered_cmd=get_buffered_cmd, + config_meas_cmd=config_meas_cmd, + arm_meas_cmd=arm_meas_cmd, + shape=shape, + **kwargs) + + self.measurement_window = None, None + self.operation = operation + self.configured = False + self.ready = False + + +class QuPulseDACInstrument(Instrument): + """ + An instrument that represents a DAC which supports buffered measurements. + + Parameters: + program_name: Name of the measurement program which will be sent to the + device. + """ + + def __init__(self, name: str, + **kwargs) -> None: + """ + Creates a QuPulseDACInstrument which represents a DAC which supports# + buffered measurements. + + Args: + name: Name of the instrument + """ + super().__init__(name, **kwargs) + + self.add_parameter("program_name", + set_cmd=None, + vals=vals.Strings(), + label="Program name") + + self.program_name.set(self.name + "_program") + + self._measurement_masks = dict() + self._affected_dacs = None + self._data = None + + + def set_measurement(self, name: str, + measurement_masks: Union[MeasurementMask, Iterable[MeasurementMask]]) -> None: + """ + Adds a measurement mask (or a list of masks) to the instrument. + + Args: + name: Name of the mask + measurement_masks: MeasurementMask object (or a list of them) + """ + if isinstance(measurement_masks, MeasurementMask): + if name in self._measurement_masks: + warnings.warn( + "You add a measurement mask to an already registered measurement name. This is deprecated and will be removed in a future version. Please add all measurement masks at once.", + DeprecationWarning) + measurement_masks = self._measurement_masks[name] | {measurement_masks} + else: + measurement_masks = {measurement_masks} + else: + try: + measurement_masks = set(measurement_masks) + except TypeError: + raise TypeError('Mask must be (a list) of type MeasurementMask') + + for old_name, mask_set in self._measurement_masks.items(): + if measurement_masks & mask_set: + raise ValueError('Measurement mask already registered for measurement "{}"'.format(old_name)) + + self._measurement_masks[name] = measurement_masks + + + def register_operations(self, dac: DAC, operations: Union[OperationDefinition, Iterable[OperationDefinition]]) -> None: + """ + Register operations to the device. Each operation creates a parameter + which can be measured in a loop. + + Args: + dac: DAC hardware object + operations: Operations to add to the device. + """ + if isinstance(operations, OperationDefinition): + operations = [operations] + + for op in operations: + if not op.identifier in self.parameters: + self.add_parameter(op.identifier, + parameter_class=QuPulseDACChannel, + get_buffered_cmd=self._get_buffered, + config_meas_cmd=self._configure_measurement, + arm_meas_cmd=self._arm_measurement, + operation=op) + + dac.register_operations(self.program_name, operations) + + + def _configure_measurement(self, parameter: QuPulseDACChannel, + measurement_windows) -> None: + """ + Configures a measurement to a parameter - def _get_output(self): + Args: + parameter: Parameter to measure + measurement_windows: Measurement windows """ + if parameter.configured: + raise Exception('It is not allowed to measure the same parameter ({}) twice.'.format(parameter)) + + mask_name = parameter.operation.maskID + + for window_name, masks in self._measurement_masks.items(): + for mask in masks: + if mask.mask_name == mask_name: + if window_name in measurement_windows: + parameter.measurement_window = window_name, measurement_windows[window_name] + break + else: + raise Exception('Measurement window "{}" is not given.'.format(window_name)) + else: + continue + + break + + parameter.configured = True + + + def _arm_measurement(self, parameter: QuPulseDACChannel) -> None: + """ + Arms the measurement of a parameter + + Args: + parameter: Parameter whose measurements should be armed. """ - self._output_counter += 1 # TODO Test - #print("QuPulseTemplate._get_output() called ({}); parameters: {}.".format(self._output_counter, self.template_parameters.to_dict())) # TODO Test + all_ready = True + parameter.ready = True + + for name, p in self.parameters.items(): + if isinstance(p, QuPulseDACChannel) and p.configured: + if not p.ready: + all_ready = False + break + + if all_ready: + self._affected_dacs = defaultdict(dict) + + for p in self.parameters.values(): + if isinstance(p, QuPulseDACChannel) and p.ready: + window_name, window = p.measurement_window + mask_name = p.operation.maskID + masks = self._measurement_masks[window_name] + for mask in masks: + if mask.mask_name == mask_name: + self._affected_dacs[mask.dac][mask.mask_name] = window + break + + for dac, dac_windows in self._affected_dacs.items(): + dac.register_measurement_windows(self.program_name, dac_windows) + + for dac in self._affected_dacs.keys(): + dac.arm_program(self.program_name) + + + def _get_buffered(self, parameter: QuPulseDACChannel): + """ + Measures all parameters on the first call and returns the measurement + value(s) of the current measurement step. + + Args: + parameter: Parameter to measure. + + Returns: + The measurement value(s) of the current measurement step. The shape + of the value(s) depends on the operation. + """ + if not self._data: + parameters = [] + for p in self.parameters.values(): + if isinstance(p, QuPulseDACChannel) and p.ready: + parameters.append(p.name) + + self._data = {} + for dac in self._affected_dacs.keys(): + data = dac.measure_program(parameters) + if data: + self._data = {**self._data, **data} + + n = parameter.shape[0] - # TODO - # first call: measure all values and resturn the first - # next calls: return next values until the list of return values is empty - # empty list: -> first call because it has to be a new measure + values = self._data[parameter.name][:n] + del self._data[parameter.name][:n] - return 0 #np.array([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]]) + return values \ No newline at end of file diff --git a/qcodes/loops.py b/qcodes/loops.py index 64f863b7b830..879527b31b98 100644 --- a/qcodes/loops.py +++ b/qcodes/loops.py @@ -333,8 +333,18 @@ def snapshot_base(self, update=False): class BufferedLoop(Loop): + """ + A loop that sweeps the parameters in a buffer that is sent once to the + device. So the loop will be run on the hardware. + """ def __init__(self, sweep_values, station=None): + """ + Creates a BufferedLoop + + Args: + sweep_values: Sweep values of a BufferedSweepableParameter + """ super().__init__(sweep_values, station=station) if not isinstance(sweep_values.parameter, BufferedSweepableParameter): @@ -370,7 +380,6 @@ def each(self, *actions): # check for nested Loops, and activate them with default measurement for i, action in enumerate(actions): - print("XXX: {}".format(type(action))) if isinstance(action, BufferedLoop): default = Station.default.default_measurement actions[i] = action.each(*default) @@ -1015,10 +1024,17 @@ def _wait(self, delay): class BufferedActiveLoop(ActiveLoop): - + """ + An ActiveLoop that sweeps the parameters in a buffer that is sent once to + the device. So the loop will be run on the hardware. + """ + def __init__(self, sweep_values, *actions, then_actions=(), station=None, progress_interval=None, bg_task=None, bg_final_task=None, bg_min_delay=None): + """ + Creates a BufferedActiveLoop + """ super().__init__(sweep_values, 0, *actions, then_actions=then_actions, station=station, progress_interval=progress_interval, bg_task=bg_task, bg_final_task=bg_final_task, bg_min_delay=bg_min_delay) @@ -1044,13 +1060,11 @@ def _send_buffer(self): def _configure_measurement(self, measurement_windows): for action in self.actions: if isinstance(action, BufferedReadableParameter): # Measurement - measurement_windows = action.configure_measurement(measurement_windows) + action.configure_measurement(measurement_windows) if len(self.actions) == 1 and isinstance(self.actions[0], BufferedActiveLoop): - measurement_windows = self.actions[0]._configure_measurement(measurement_windows) - - return measurement_windows - + self.actions[0]._configure_measurement(measurement_windows) + def _arm_measurement(self): for action in self.actions: if isinstance(action, BufferedReadableParameter): # Measurement @@ -1058,7 +1072,6 @@ def _arm_measurement(self): if len(self.actions) == 1 and isinstance(self.actions[0], BufferedActiveLoop): self.actions[0]._arm_measurement() - def _run_loop(self, first_delay=0, action_indices=(), loop_indices=(), current_values=(), @@ -1078,16 +1091,10 @@ def _run_loop(self, first_delay=0, action_indices=(), if self._nest_first: self._set_buffered_sweep() measurement_windows = self._send_buffer() - remaining_measurement_windows = self._configure_measurement(measurement_windows) - - if remaining_measurement_windows: - raise AttributeError("The following measurement windows have no corresponding channel on the instrument: {}".format(measurement_windows.keys())) + self._configure_measurement(measurement_windows) self._arm_measurement() - # TODO send meas.win. to measurement-instrument - # TODO arm measurement-inst - # at the beginning of the loop, the time to wait after setting # the loop parameter may be increased if an outer loop requested longer delay = max(self.delay, first_delay) From 37393628d5f4e555014655499ea4a8f67facdd58 Mon Sep 17 00:00:00 2001 From: "Lukas.Lankes" Date: Tue, 13 Nov 2018 08:09:13 +0100 Subject: [PATCH 13/35] Bug fixes, fine tuning and comments --- .../QuTech/qupulse_instrument.py | 23 +++++++++++-- qcodes/loops.py | 33 +++++++++++++++---- qcodes/utils/validators.py | 5 ++- 3 files changed, 52 insertions(+), 9 deletions(-) diff --git a/qcodes/instrument_drivers/QuTech/qupulse_instrument.py b/qcodes/instrument_drivers/QuTech/qupulse_instrument.py index ceab0f434095..e34456757b5a 100644 --- a/qcodes/instrument_drivers/QuTech/qupulse_instrument.py +++ b/qcodes/instrument_drivers/QuTech/qupulse_instrument.py @@ -15,6 +15,7 @@ from qcodes.instrument.parameter import BufferedSweepableParameter, BufferedReadableParameter import qcodes.utils.validators as vals +from qupulse.pulses import MappingPT, ForLoopPT from qupulse.pulses.pulse_template import PulseTemplate from qupulse.hardware.dacs import DAC from qupulse.hardware.setup import ChannelID, _SingleChannel, PlaybackChannel, MarkerChannel, MeasurementMask @@ -141,6 +142,7 @@ def __init__(self, name: str, self.program_name.set(self.name + "_program") self._channel_map = {} + self._loops = [] def _get_template(self) -> PulseTemplate: @@ -159,7 +161,7 @@ def _set_template(self, value: PulseTemplate) -> None: value: PulseTemplate """ self._template = value - self._loops = [] + self._loops.clear() self.template_parameters.set_parameters(self._template.parameter_names, self._sweep_parameter, @@ -294,6 +296,8 @@ def _really_send_buffer(self) -> Dict: measurement_windows = self._run_program(program) + self._loops.clear() + return measurement_windows @@ -418,8 +422,16 @@ def __init__(self, name: str, shape=shape, **kwargs) - self.measurement_window = None, None self.operation = operation + + self.reset() + + def reset(self): + """ + Reset the parameter to its default state, so it can be used for another + measurement + """ + self.measurement_window = None, None self.configured = False self.ready = False @@ -593,6 +605,7 @@ def _get_buffered(self, parameter: QuPulseDACChannel): for p in self.parameters.values(): if isinstance(p, QuPulseDACChannel) and p.ready: parameters.append(p.name) + p.reset() self._data = {} for dac in self._affected_dacs.keys(): @@ -605,4 +618,10 @@ def _get_buffered(self, parameter: QuPulseDACChannel): values = self._data[parameter.name][:n] del self._data[parameter.name][:n] + if self._data[parameter.name] != None and not self._data[parameter.name]: + del self._data[parameter.name] + + if self._data != None and not self._data: + self._data = None + return values \ No newline at end of file diff --git a/qcodes/loops.py b/qcodes/loops.py index 879527b31b98..94f6f8b65bae 100644 --- a/qcodes/loops.py +++ b/qcodes/loops.py @@ -164,7 +164,6 @@ def buffered_loop(self, sweep_values): Args: sweep_values (): - delay (int): Examples: >>> Loop(sv1, d1).buffered_loop(sv2, d2).each(*a) @@ -345,7 +344,7 @@ def __init__(self, sweep_values, station=None): Args: sweep_values: Sweep values of a BufferedSweepableParameter """ - super().__init__(sweep_values, station=station) + super().__init__(sweep_values, delay=0, station=station) if not isinstance(sweep_values.parameter, BufferedSweepableParameter): raise TypeError("The sweep values of a buffered loop must sweep a buffered parameter.") @@ -1040,12 +1039,20 @@ def __init__(self, sweep_values, *actions, then_actions=(), bg_final_task=bg_final_task, bg_min_delay=bg_min_delay) def _set_buffered_sweep(self): + """ + Builds up the buffers on the instrument by setting the loop information + (sweep_values). + """ self.sweep_values.parameter.set_buffered(list(self.sweep_values)) if len(self.actions) == 1 and isinstance(self.actions[0], BufferedActiveLoop): self.actions[0]._set_buffered_sweep() def _send_buffer(self): + """ + Lets the instrument(s) send the buffer(s) to the hardware (and arms the + hardware). + """ mw = self.sweep_values.parameter.send_buffer() if mw is None: mw = {} @@ -1058,6 +1065,10 @@ def _send_buffer(self): return mw def _configure_measurement(self, measurement_windows): + """ + Configures the measurement on the instrument by setting the measurement + windows (measurement times). + """ for action in self.actions: if isinstance(action, BufferedReadableParameter): # Measurement action.configure_measurement(measurement_windows) @@ -1066,6 +1077,9 @@ def _configure_measurement(self, measurement_windows): self.actions[0]._configure_measurement(measurement_windows) def _arm_measurement(self): + """ + Arms the instruments hardware for the measurement + """ for action in self.actions: if isinstance(action, BufferedReadableParameter): # Measurement action.arm_measurement() @@ -1088,12 +1102,19 @@ def _run_loop(self, first_delay=0, action_indices=(), ignore_kwargs: for compatibility with other loop tasks """ + # The most outer loop controls the buffered loop: + # - It lets the instrument(s) build up the buffer, + # - it registers the instruments program/buffer to the hardware, + # - it lets the instrument(s) send their buffers to the hardware, + # - (it arms the program on the instruments hardware), + # - it configures the measurement on the instrument and + # - it arms the measruement on the instruments hardware. if self._nest_first: - self._set_buffered_sweep() - measurement_windows = self._send_buffer() - self._configure_measurement(measurement_windows) + self._set_buffered_sweep() # builds up the buffer + measurement_windows = self._send_buffer() # sends the buffer to the hardware and arms the hardware - self._arm_measurement() + self._configure_measurement(measurement_windows) # configures the measurement windows + self._arm_measurement() # arms the hardware for the measurement # at the beginning of the loop, the time to wait after setting # the loop parameter may be increased if an outer loop requested longer diff --git a/qcodes/utils/validators.py b/qcodes/utils/validators.py index d456ec926026..8db1bafd8b34 100644 --- a/qcodes/utils/validators.py +++ b/qcodes/utils/validators.py @@ -624,10 +624,13 @@ def __repr__(self): class ObjectTypeValidator(Validator): + """ + This validator checks, if an object is an instance of a specific type. + """ def __init__(self, *allowed_types: type): """ - Validator for objects + Create an ObjectTypeValidator for validating objects of specific types """ self._valid_values = [0] self._allowed_types = allowed_types From 045d51b6cf7fa4e79568ba5766343d619b69b171 Mon Sep 17 00:00:00 2001 From: "Lukas.Lankes" Date: Thu, 15 Nov 2018 13:06:54 +0100 Subject: [PATCH 14/35] Metadata support for QuPulse-Instruments --- qcodes/instrument/base.py | 2 +- .../QuTech/qupulse_instrument.py | 113 ++++++++++++++++-- qcodes/utils/validators.py | 39 ++++-- 3 files changed, 128 insertions(+), 26 deletions(-) diff --git a/qcodes/instrument/base.py b/qcodes/instrument/base.py index 313f9276edd9..e6eb866469ee 100644 --- a/qcodes/instrument/base.py +++ b/qcodes/instrument/base.py @@ -180,7 +180,7 @@ def snapshot_base(self, update: bool=False, update = False try: snap['parameters'][name] = param.snapshot(update=update) - except: + except Exception as e: # really log this twice. Once verbose for the UI and once # at lower level with more info for file based loggers log.warning(f"Snapshot: Could not update parameter: " diff --git a/qcodes/instrument_drivers/QuTech/qupulse_instrument.py b/qcodes/instrument_drivers/QuTech/qupulse_instrument.py index e34456757b5a..4b7fb5bd1428 100644 --- a/qcodes/instrument_drivers/QuTech/qupulse_instrument.py +++ b/qcodes/instrument_drivers/QuTech/qupulse_instrument.py @@ -6,7 +6,7 @@ @author: l.lankes """ -from typing import Callable, Union, Iterable, Optional, Dict +from typing import Callable, Union, Iterable, Optional, Dict, Sequence from collections import defaultdict import numpy as np import warnings @@ -14,6 +14,7 @@ from qcodes.instrument.base import Instrument from qcodes.instrument.parameter import BufferedSweepableParameter, BufferedReadableParameter import qcodes.utils.validators as vals +from qcodes.utils.helpers import full_class from qupulse.pulses import MappingPT, ForLoopPT from qupulse.pulses.pulse_template import PulseTemplate @@ -24,6 +25,19 @@ from atsaverage.operations import OperationDefinition, Downsample +class QuPulseTemplateParameter(BufferedSweepableParameter): + """ + A parameter that creates buffered sweeps in the instrument. + """ + + def __init__(self, *args, **kwargs): + """ + Creates a QuPulseTemplateParameter that creates buffered sweeps in + the instrument. + """ + super().__init__(*args, **kwargs) + + class QuPulseTemplateParameters(Instrument): """ Submodule for the QuPulseAWGInstrument. It automatically generates @@ -65,7 +79,7 @@ def set_parameters(self, pulse_parameters: Iterable[str], if pulse_parameters is not None: for p in pulse_parameters: - self.add_parameter(p, parameter_class=BufferedSweepableParameter, + self.add_parameter(p, parameter_class=QuPulseTemplateParameter, vals=vals.Numbers(), set_cmd=None, sweep_parameter_cmd=sweep_parameter_cmd, send_buffer_cmd=send_buffer_cmd) @@ -119,7 +133,7 @@ def __init__(self, name: str, self.add_parameter("template", get_cmd=self._get_template, set_cmd=self._set_template, - vals=vals.ObjectTypeValidator(PulseTemplate, type(None)), + vals=vals.MultiType(vals.ObjectType(PulseTemplate), vals.NoneType()), label="Pulse template") self.add_parameter("program_name", set_cmd=None, @@ -127,11 +141,11 @@ def __init__(self, name: str, label="Program name") self.add_parameter("channel_mapping", set_cmd=None, - vals=vals.ObjectTypeValidator(dict, type(None)), + vals=vals.MultiType(vals.ObjectType(Dict), vals.NoneType()), label="Channel mapping") self.add_parameter("measurement_mapping", set_cmd=None, - vals=vals.ObjectTypeValidator(dict, type(None)), + vals=vals.MultiType(vals.ObjectType(Dict), vals.NoneType()), label="Measurement mapping") self.add_parameter("registered_channels", get_cmd=self._get_registered_channels, @@ -143,6 +157,7 @@ def __init__(self, name: str, self._channel_map = {} self._loops = [] + self.template.set(None) def _get_template(self) -> PulseTemplate: @@ -163,11 +178,16 @@ def _set_template(self, value: PulseTemplate) -> None: self._template = value self._loops.clear() - self.template_parameters.set_parameters(self._template.parameter_names, - self._sweep_parameter, - self._send_buffer) + if self._template: + self.template_parameters.set_parameters(self._template.parameter_names, + self._sweep_parameter, + self._send_buffer) + else: + self.template_parameters.set_parameters(None, None, None) + + def set_channel(self, identifier: ChannelID, single_channel: Union[_SingleChannel, Iterable[_SingleChannel]]) -> None: """ @@ -375,6 +395,24 @@ def get_default_info(awg): awg.arm(None) return measurement_windows + + + def snapshot_base(self, update: bool=False, + params_to_skip_update: Sequence[str]=None): + if params_to_skip_update is None: + params_to_skip_update = [] + + params_to_skip_update = list(params_to_skip_update) + params_to_skip_update.append(self.template.name) + + metadata = super().snapshot_base(update, params_to_skip_update) + + template_dict = self.template.get().get_serialization_data() + + metadata['parameters'][self.template.name]['value'] = template_dict + metadata['parameters'][self.template.name]['raw_value'] = template_dict + + return metadata class QuPulseDACChannel(BufferedReadableParameter): @@ -426,6 +464,7 @@ def __init__(self, name: str, self.reset() + def reset(self): """ Reset the parameter to its default state, so it can be used for another @@ -434,6 +473,17 @@ def reset(self): self.measurement_window = None, None self.configured = False self.ready = False + + + def snapshot_base(self, update: bool=False, + params_to_skip_update: Sequence[str]=None): + metadata = super().snapshot_base(update, params_to_skip_update) + + metadata['operation'] = {'__class__': full_class(self.operation), + 'identifier': self.operation.identifier, + 'maskID': self.operation.maskID} + + return metadata class QuPulseDACInstrument(Instrument): @@ -460,6 +510,16 @@ def __init__(self, name: str, set_cmd=None, vals=vals.Strings(), label="Program name") + self.add_parameter("measurement_masks", + get_cmd=self._get_measurement_masks, + label="Measurement masks") + + self.add_function("set_measurement", + call_cmd=self._set_measurement, + args=[vals.Strings(), vals.MultiType(vals.ObjectType(MeasurementMask), vals.ObjectType(Iterable))]) + self.add_function("register_operations", + call_cmd=self._register_operations, + args=[vals.ObjectType(DAC), vals.MultiType(vals.ObjectType(MeasurementMask), vals.ObjectType(Iterable))]) self.program_name.set(self.name + "_program") @@ -467,9 +527,17 @@ def __init__(self, name: str, self._affected_dacs = None self._data = None + + def _get_measurement_masks(self): + """ + Returns: + A dictionary with all measurement masks for each measurement window. + """ + return self._measurement_masks + - def set_measurement(self, name: str, - measurement_masks: Union[MeasurementMask, Iterable[MeasurementMask]]) -> None: + def _set_measurement(self, name: str, + measurement_masks: Union[MeasurementMask, Iterable[MeasurementMask]]) -> None: """ Adds a measurement mask (or a list of masks) to the instrument. @@ -498,7 +566,8 @@ def set_measurement(self, name: str, self._measurement_masks[name] = measurement_masks - def register_operations(self, dac: DAC, operations: Union[OperationDefinition, Iterable[OperationDefinition]]) -> None: + def _register_operations(self, dac: DAC, + operations: Union[OperationDefinition, Iterable[OperationDefinition]]) -> None: """ Register operations to the device. Each operation creates a parameter which can be measured in a loop. @@ -624,4 +693,24 @@ def _get_buffered(self, parameter: QuPulseDACChannel): if self._data != None and not self._data: self._data = None - return values \ No newline at end of file + return values + + + def snapshot_base(self, update: bool=False, + params_to_skip_update: Sequence[str]=None): + if params_to_skip_update is None: + params_to_skip_update = [] + + # Skip measurement_masks and add them manually + params_to_skip_update = list(params_to_skip_update) + params_to_skip_update.append(self.measurement_masks.name) + + metadata = super().snapshot_base(update, params_to_skip_update) + + # Add all measurement masks into a dictionary for the metadata + masks_metadata = {name: [{'__class__': full_class(m), 'mask_name': m.mask_name, 'dac': repr(m.dac)} for m in masks] for name, masks in self._measurement_masks.items()} + + metadata['parameters'][self.measurement_masks.name]['value'] = masks_metadata + metadata['parameters'][self.measurement_masks.name]['raw_value'] = masks_metadata + + return metadata diff --git a/qcodes/utils/validators.py b/qcodes/utils/validators.py index 8db1bafd8b34..a9ab542b8a1b 100644 --- a/qcodes/utils/validators.py +++ b/qcodes/utils/validators.py @@ -623,29 +623,42 @@ def __repr__(self): return ''.format(self.allowed_keys) -class ObjectTypeValidator(Validator): +class NoneType(Validator): + """ + This validator checks, if an object is None. + """ + + def __init__(self): + """ + Create an NoneType for validating objects that are None + """ + self._valid_values = [None] + + def validate(self, value, context=''): + if value is not None: + raise TypeError('{} is not None; {}'.format(repr(value), context)) + + def __repr__(self): + return '' + +class ObjectType(Validator): """ This validator checks, if an object is an instance of a specific type. """ - def __init__(self, *allowed_types: type): + def __init__(self, allowed_type: type): """ - Create an ObjectTypeValidator for validating objects of specific types + Create an ObjectType validator for validating objects of specific types """ self._valid_values = [0] - self._allowed_types = allowed_types + self._allowed_type = allowed_type def validate(self, value, context=''): - valid = False - for allowed_type in self._allowed_types: - if isinstance(value, allowed_type): - valid = True - break - if not valid: + if not isinstance(value, self._allowed_type): raise TypeError('{} is a forbidden type; {}'.format(type(value), context)) def __repr__(self): - if self._allowed_types is None: - return '' + if self._allowed_type is None: + return '' else: - return ''.format(self._allowed_types) \ No newline at end of file + return ''.format(self._allowed_type) \ No newline at end of file From c6e1bb0bde2ed9b5cd79b3f456f83638b82dbf6d Mon Sep 17 00:00:00 2001 From: 4K User Date: Thu, 22 Nov 2018 11:41:03 +0100 Subject: [PATCH 15/35] Fix Harvard DecaDAC terminator --- qcodes/instrument_drivers/Harvard/Decadac.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qcodes/instrument_drivers/Harvard/Decadac.py b/qcodes/instrument_drivers/Harvard/Decadac.py index 275d751413fc..6d38fd7fa754 100644 --- a/qcodes/instrument_drivers/Harvard/Decadac.py +++ b/qcodes/instrument_drivers/Harvard/Decadac.py @@ -399,7 +399,7 @@ def __init__(self, name, address, min_val=-5, max_val=5, **kwargs): """ - super().__init__(name, address, **kwargs) + super().__init__(name, address, terminator='\n', **kwargs) # Do feature detection self._feature_detect() @@ -407,7 +407,7 @@ def __init__(self, name, address, min_val=-5, max_val=5, **kwargs): # Create channels channels = ChannelList(self, "Channels", self.DAC_CHANNEL_CLASS, snapshotable=False) slots = ChannelList(self, "Slots", self.DAC_SLOT_CLASS) - for i in range(5): # Create the 6 DAC slots + for i in range(4): # Create the 4 DAC slots slots.append(self.DAC_SLOT_CLASS(self, "Slot{}".format(i), i, min_val, max_val)) channels.extend(slots[i].channels) slots.lock() From 72410d44232703520b571eb7f52a08cb2fdcc598 Mon Sep 17 00:00:00 2001 From: "Lukas.Lankes" Date: Mon, 26 Nov 2018 09:43:58 +0100 Subject: [PATCH 16/35] Fixed the broken read method. This should fix issue #3. --- .../instrument_drivers/Harvard/FZJ_Decadac.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py b/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py index af90b2cb5f1a..4ba163857069 100644 --- a/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py +++ b/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py @@ -231,8 +231,7 @@ def _get_volt(self): """ Reads out the voltage of the channel as dac-code """ - self._parent._write(self, DacBase._COMMAND_GET_VOLT) - buf = self._parent._read(self, self._BUF_SIZE) + buf = self._parent._read(self, DacBase._COMMAND_GET_VOLT) self._volt = int(buf[-self._BUF_SIZE+1:-1]) return self._volt @@ -665,16 +664,14 @@ def _write(self, obj, cmd): if Decadac.enable_output: print("Decadac._write(\"{}\")".format(cmd)) - return self._read(self, len(cmd)) - - def _read(self, obj, buf_size): + def _read(self, obj, cmd): """ Read the buffer of the device Arguments: - obj (object): object that wants to read something (needed to set the current slot and channel) - buf_size (int): size of the buffer to read + obj (object): object that wants to read something (needed to set the current slot and channel) + cmd (str): command """ if obj != None: if isinstance(obj, DacSlot): @@ -682,9 +679,9 @@ def _read(self, obj, buf_size): elif isinstance(obj, DacChannel): self._set_channel(obj._parent, obj) - result = super().ask_raw(buf_size) + result = super().write_raw(cmd) if Decadac.enable_output: - print("Decadac._read({}) = \"{}\"".format(buf_size, result)) - + print("Decadac._read(\"{}\") = {}".format(cmd, result)) + return result \ No newline at end of file From f509aa8943f33ce12b9557cc22ca8acd0e31dc55 Mon Sep 17 00:00:00 2001 From: "Lukas.Lankes" Date: Mon, 26 Nov 2018 10:00:22 +0100 Subject: [PATCH 17/35] This should fix issue #4. It's necessary to set the switch pos before setting or measuring voltages with the Decadac. When this isn't done the instrument can't convert between real voltages and those values that are used by the hardware. Because of that I added a error message for this special case. Additionaly I added the parameter "default_switch_pos" to the constructor of Decadac. This value is used by the reset function to reset the switch position. --- .../instrument_drivers/Harvard/FZJ_Decadac.py | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py b/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py index 4ba163857069..73f143be943c 100644 --- a/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py +++ b/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py @@ -155,7 +155,7 @@ def _evaluate_switchpos(pos): class DacChannel(InstrumentChannel, DacBase): - def __init__(self, parent, name, channel): + def __init__(self, parent, name, channel, default_switch_pos=DacBase._DEFAULT_SWITCH_POS): """ Initialize the channel @@ -184,6 +184,9 @@ def __init__(self, parent, name, channel): self._volt_val = DacVoltValidator(self) self._volt_raw_val = vals.Ints(0, 65535) self._ramp_val = vals.Numbers(0, 10) + self._min_volt = None + self._max_volt = None + self._default_switch_pos = default_switch_pos # Channel parameters # Voltage @@ -213,13 +216,14 @@ def reset(self): Resets all parameters to default """ self._volt = DacBase._DEFAULT_VOLT - self._switch_pos = DacBase._DEFAULT_SWITCH_POS self._lower_limit = DacBase._DEFAULT_LOWER_LIMIT self._upper_limit = DacBase._DEFAULT_UPPER_LIMIT self._update_period = DacBase._DEFAULT_UPDATE_PERIOD self._slope = DacBase._DEFAULT_SLOPE self._trig_mode = DacBase._DEFAULT_TRIG_MODE + self.switch_pos.set(self._default_switch_pos) + return (DacBase._COMMAND_SET_UPPER_LIMIT.format(self._upper_limit) + DacBase._COMMAND_SET_LOWER_LIMIT.format(self._lower_limit) + DacBase._COMMAND_SET_VOLT.format(self._volt) @@ -452,14 +456,17 @@ def validate(self, value, context=""): """ if not isinstance(value, vals.Numbers.validtypes): raise TypeError("{} is not an int or float.\n{}".format(repr(value), context)) - + + if self._parent._min_volt is None or self._parent._max_volt is None: + raise ValueError("No voltage interval is given for the Decadac instrument. Please set the switch_pos parameter.\n{}".format(context)) + if not (self._parent._min_volt <= value <= self._parent._max_volt): raise ValueError("DacVoltValidator is invalid: must be between {} and {} inclusive.\n{}".format(self._parent._min_volt, self._parent._max_volt, context)) class DacSlot(InstrumentChannel, DacBase): - def __init__(self, parent, name, slot): + def __init__(self, parent, name, slot, default_switch_pos=DacBase._DEFAULT_SWITCH_POS): """ Initialize the slot @@ -483,7 +490,7 @@ def __init__(self, parent, name, slot): channels = ChannelList(self, "Slot_Chans", DacChannel) for channel in range(4): - channels.append(DacChannel(self, "Chan{}".format(slot, channel), channel)) + channels.append(DacChannel(self, "Chan{}".format(slot, channel), channel, default_switch_pos=default_switch_pos)) self.add_submodule("channels", channels) @@ -538,21 +545,27 @@ class Decadac(VisaInstrument): _device_connected = False enable_output = False - def __init__(self, name, address, reset=_DEFAULT_RESET, baudrate=_DEFAULT_BAUDRATE, timeout=_DEFAULT_TIMEOUT, **kwargs): + def __init__(self, name, address, + reset=_DEFAULT_RESET, + baudrate=_DEFAULT_BAUDRATE, + timeout=_DEFAULT_TIMEOUT, + default_switch_pos=DacBase._DEFAULT_SWITCH_POS, + **kwargs): """ Initialize the device Args: - name (str): name of the device - address (str): address of the device (e.g. /dev/ttyUSB0) - reset (bool): if "True", set all voltages to zero, set trigger mode to "always update" and stop ramps. If "False" only the upper and lower limit is reset. - baudrate (int): baud rate of ASCII protocol - timeout (int): seconds to allow for responses. Default 5 + name (str): name of the device + address (str): address of the device (e.g. /dev/ttyUSB0) + reset (bool): if "True", set all voltages to zero, set trigger mode to "always update" and stop ramps. If "False" only the upper and lower limit is reset. + baudrate (int): baud rate of ASCII protocol + timeout (int): seconds to allow for responses. Default 5 + default_switch_pos (int): default switch position (-1, 0 or 1) that is set when reset is called (default: 0) Attributes: - name (str): name - slots (ChannelList): list of all slots - channels (ChannelList): list of all channels + name (str): name + slots (ChannelList): list of all slots + channels (ChannelList): list of all channels """ super().__init__(name, address, timeout=timeout, **kwargs) @@ -567,7 +580,7 @@ def __init__(self, name, address, reset=_DEFAULT_RESET, baudrate=_DEFAULT_BAUDRA slots = ChannelList(self, "Slots", DacSlot) for slot in range(5): - slots.append(DacSlot(self, "Slot{}".format(slot), slot)) + slots.append(DacSlot(self, "Slot{}".format(slot), slot, default_switch_pos=default_switch_pos)) channels.extend(slots[slot].channels) slots.lock() From 6fcf662eb9cedb3222c94a292806e11eac60c7e3 Mon Sep 17 00:00:00 2001 From: "Lukas.Lankes" Date: Mon, 3 Dec 2018 12:21:49 +0100 Subject: [PATCH 18/35] Addition to issue #4. Parameter switch_pos is initially set to None, so an error raises if the parameter is read before it was set. Also added a warning to the reset function, that all parameters are reset to their default values. --- qcodes/instrument_drivers/Harvard/FZJ_Decadac.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py b/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py index 73f143be943c..ed3dddf39ed8 100644 --- a/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py +++ b/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py @@ -11,6 +11,7 @@ from qcodes.utils import validators as vals from qcodes.instrument.visa import VisaInstrument +import warnings class DACException(Exception): pass @@ -186,6 +187,7 @@ def __init__(self, parent, name, channel, default_switch_pos=DacBase._DEFAULT_SW self._ramp_val = vals.Numbers(0, 10) self._min_volt = None self._max_volt = None + self._switch_pos = None self._default_switch_pos = default_switch_pos # Channel parameters @@ -224,6 +226,8 @@ def reset(self): self.switch_pos.set(self._default_switch_pos) + warnings.warn('All parameters of the Dacadac "{}" have been reset to their defaults.'.format(self.name)) + return (DacBase._COMMAND_SET_UPPER_LIMIT.format(self._upper_limit) + DacBase._COMMAND_SET_LOWER_LIMIT.format(self._lower_limit) + DacBase._COMMAND_SET_VOLT.format(self._volt) @@ -253,6 +257,9 @@ def _get_switch_pos(self): """ Gets the switch_pos """ + if self._switch_pos is None: + raise ValueError('The switch position has not been set.') + return self._switch_pos From aa44bcae5b39a56ece2648d32514e5d549c75a6a Mon Sep 17 00:00:00 2001 From: "Lukas.Lankes" Date: Mon, 3 Dec 2018 12:30:41 +0100 Subject: [PATCH 19/35] Calling ask_raw in Decadac._read instead of write_raw. This should fix issue #3 --- qcodes/instrument_drivers/Harvard/Decadac.py | 7 ++----- qcodes/instrument_drivers/Harvard/FZJ_Decadac.py | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/qcodes/instrument_drivers/Harvard/Decadac.py b/qcodes/instrument_drivers/Harvard/Decadac.py index 4dbf5d2aa75d..4e412e71befe 100644 --- a/qcodes/instrument_drivers/Harvard/Decadac.py +++ b/qcodes/instrument_drivers/Harvard/Decadac.py @@ -454,15 +454,12 @@ def __init__(self, name: str, address: str, channels = ChannelList(self, "Channels", self.DAC_CHANNEL_CLASS, snapshotable=False) slots = ChannelList(self, "Slots", self.DAC_SLOT_CLASS) -<<<<<<< HEAD - for i in range(4): # Create the 4 DAC slots - slots.append(self.DAC_SLOT_CLASS(self, "Slot{}".format(i), i, min_val, max_val)) -======= + for i in range(5): # Create the 6 DAC slots slots.append(self.DAC_SLOT_CLASS(self, "Slot{}".format(i), i, min_val, max_val)) ->>>>>>> 1cfae35f96c8a471213e9af62ed9ebe9353640a1 channels.extend(slots[i].channels) + slots.lock() channels.lock() self.add_submodule("slots", slots) diff --git a/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py b/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py index ed3dddf39ed8..651186d890ff 100644 --- a/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py +++ b/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py @@ -699,7 +699,7 @@ def _read(self, obj, cmd): elif isinstance(obj, DacChannel): self._set_channel(obj._parent, obj) - result = super().write_raw(cmd) + result = super().ask_raw(cmd) if Decadac.enable_output: print("Decadac._read(\"{}\") = {}".format(cmd, result)) From eb8a9f1a47083e3d78aa8501142b8dd7f86c408a Mon Sep 17 00:00:00 2001 From: "Lukas.Lankes" Date: Mon, 3 Dec 2018 13:27:01 +0100 Subject: [PATCH 20/35] Changed the default terminator for the FZJ_Decadac driver to "\n" instead of an empty string. --- qcodes/instrument_drivers/Harvard/FZJ_Decadac.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py b/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py index 651186d890ff..2bfa16ea0a3b 100644 --- a/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py +++ b/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py @@ -557,6 +557,7 @@ def __init__(self, name, address, baudrate=_DEFAULT_BAUDRATE, timeout=_DEFAULT_TIMEOUT, default_switch_pos=DacBase._DEFAULT_SWITCH_POS, + terminator='\n', **kwargs): """ Initialize the device @@ -575,7 +576,8 @@ def __init__(self, name, address, channels (ChannelList): list of all channels """ - super().__init__(name, address, timeout=timeout, **kwargs) + super().__init__(name, address, timeout=timeout, terminator=terminator, + **kwargs) self.current_slot = None self.current_channel = None From 008c90217d7adc52f304ee77c7ab4a7b2de52c11 Mon Sep 17 00:00:00 2001 From: "Lukas.Lankes" Date: Tue, 4 Dec 2018 09:23:28 +0100 Subject: [PATCH 21/35] More comments --- .../QuTech/qupulse_instrument.py | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/qcodes/instrument_drivers/QuTech/qupulse_instrument.py b/qcodes/instrument_drivers/QuTech/qupulse_instrument.py index 4b7fb5bd1428..48ef098ff801 100644 --- a/qcodes/instrument_drivers/QuTech/qupulse_instrument.py +++ b/qcodes/instrument_drivers/QuTech/qupulse_instrument.py @@ -178,8 +178,6 @@ def _set_template(self, value: PulseTemplate) -> None: self._template = value self._loops.clear() - - if self._template: self.template_parameters.set_parameters(self._template.parameter_names, self._sweep_parameter, @@ -399,6 +397,20 @@ def get_default_info(awg): def snapshot_base(self, update: bool=False, params_to_skip_update: Sequence[str]=None): + """ + State of the instrument as a JSON-compatible dict. + + Args: + update: If True, update the state by querying the + instrument. If False, just use the latest values in memory. + params_to_skip_update: List of parameter names that will be skipped + in update even if update is True. This is useful if you have + parameters that are slow to update but can be updated in a + different way (as in the qdac) + + Returns: + dict: base snapshot + """ if params_to_skip_update is None: params_to_skip_update = [] @@ -477,6 +489,20 @@ def reset(self): def snapshot_base(self, update: bool=False, params_to_skip_update: Sequence[str]=None): + """ + State of the instrument as a JSON-compatible dict. + + Args: + update: If True, update the state by querying the + instrument. If False, just use the latest values in memory. + params_to_skip_update: List of parameter names that will be skipped + in update even if update is True. This is useful if you have + parameters that are slow to update but can be updated in a + different way (as in the qdac) + + Returns: + dict: base snapshot + """ metadata = super().snapshot_base(update, params_to_skip_update) metadata['operation'] = {'__class__': full_class(self.operation), @@ -698,6 +724,20 @@ def _get_buffered(self, parameter: QuPulseDACChannel): def snapshot_base(self, update: bool=False, params_to_skip_update: Sequence[str]=None): + """ + State of the instrument as a JSON-compatible dict. + + Args: + update: If True, update the state by querying the + instrument. If False, just use the latest values in memory. + params_to_skip_update: List of parameter names that will be skipped + in update even if update is True. This is useful if you have + parameters that are slow to update but can be updated in a + different way (as in the qdac) + + Returns: + dict: base snapshot + """ if params_to_skip_update is None: params_to_skip_update = [] From 95ab62fcfeb1bdc4198c2c2c4744b3cf089b188b Mon Sep 17 00:00:00 2001 From: Lukas Lankes Date: Thu, 13 Dec 2018 18:02:00 +0100 Subject: [PATCH 22/35] Added "run_program"-callback to "BufferedLoop", so the user can run the measurements as soon as everything is armed. --- .../QuTech/qupulse_instrument.py | 1 + qcodes/loops.py | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/qcodes/instrument_drivers/QuTech/qupulse_instrument.py b/qcodes/instrument_drivers/QuTech/qupulse_instrument.py index 48ef098ff801..79b502d9dca1 100644 --- a/qcodes/instrument_drivers/QuTech/qupulse_instrument.py +++ b/qcodes/instrument_drivers/QuTech/qupulse_instrument.py @@ -695,6 +695,7 @@ def _get_buffered(self, parameter: QuPulseDACChannel): The measurement value(s) of the current measurement step. The shape of the value(s) depends on the operation. """ + if not self._data: parameters = [] for p in self.parameters.values(): diff --git a/qcodes/loops.py b/qcodes/loops.py index 94f6f8b65bae..c383ad08da36 100644 --- a/qcodes/loops.py +++ b/qcodes/loops.py @@ -337,7 +337,7 @@ class BufferedLoop(Loop): device. So the loop will be run on the hardware. """ - def __init__(self, sweep_values, station=None): + def __init__(self, sweep_values, station=None, run_program_cmd=None): """ Creates a BufferedLoop @@ -349,6 +349,8 @@ def __init__(self, sweep_values, station=None): if not isinstance(sweep_values.parameter, BufferedSweepableParameter): raise TypeError("The sweep values of a buffered loop must sweep a buffered parameter.") + self.run_program_cmd = run_program_cmd + def _copy(self): out = BufferedLoop(self.sweep_values) out.nested_loop = self.nested_loop @@ -394,6 +396,7 @@ def each(self, *actions): actions = [self.nested_loop.each(*actions)] return BufferedActiveLoop(self.sweep_values, *actions, + run_program_cmd=self.run_program_cmd, then_actions=self.then_actions, station=self.station, progress_interval=self.progress_interval, bg_task=self.bg_task, bg_final_task=self.bg_final_task, @@ -1028,7 +1031,7 @@ class BufferedActiveLoop(ActiveLoop): the device. So the loop will be run on the hardware. """ - def __init__(self, sweep_values, *actions, then_actions=(), + def __init__(self, sweep_values, *actions, run_program_cmd=None, then_actions=(), station=None, progress_interval=None, bg_task=None, bg_final_task=None, bg_min_delay=None): """ @@ -1037,6 +1040,8 @@ def __init__(self, sweep_values, *actions, then_actions=(), super().__init__(sweep_values, 0, *actions, then_actions=then_actions, station=station, progress_interval=progress_interval, bg_task=bg_task, bg_final_task=bg_final_task, bg_min_delay=bg_min_delay) + + self.run_program_cmd = run_program_cmd def _set_buffered_sweep(self): """ @@ -1087,6 +1092,10 @@ def _arm_measurement(self): if len(self.actions) == 1 and isinstance(self.actions[0], BufferedActiveLoop): self.actions[0]._arm_measurement() + def _run_program(self): + if self.run_program_cmd is not None: + self.run_program_cmd() + def _run_loop(self, first_delay=0, action_indices=(), loop_indices=(), current_values=(), **ignore_kwargs): @@ -1116,6 +1125,8 @@ def _run_loop(self, first_delay=0, action_indices=(), self._configure_measurement(measurement_windows) # configures the measurement windows self._arm_measurement() # arms the hardware for the measurement + self._run_program() # call user defined run_program-function + # at the beginning of the loop, the time to wait after setting # the loop parameter may be increased if an outer loop requested longer delay = max(self.delay, first_delay) From d483ad63097de8b5799bf4ae3fe19fe6ffeb5eeb Mon Sep 17 00:00:00 2001 From: lankes-fzj Date: Tue, 22 Jan 2019 16:42:27 +0100 Subject: [PATCH 23/35] First try to work with RepetitionPTs in qcodes --- .../QuTech/qupulse_instrument.py | 59 ++++++++++++++----- 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/qcodes/instrument_drivers/QuTech/qupulse_instrument.py b/qcodes/instrument_drivers/QuTech/qupulse_instrument.py index 79b502d9dca1..d05ba4a375bf 100644 --- a/qcodes/instrument_drivers/QuTech/qupulse_instrument.py +++ b/qcodes/instrument_drivers/QuTech/qupulse_instrument.py @@ -16,7 +16,7 @@ import qcodes.utils.validators as vals from qcodes.utils.helpers import full_class -from qupulse.pulses import MappingPT, ForLoopPT +from qupulse.pulses import MappingPT, ForLoopPT, RepetitionPT from qupulse.pulses.pulse_template import PulseTemplate from qupulse.hardware.dacs import DAC from qupulse.hardware.setup import ChannelID, _SingleChannel, PlaybackChannel, MarkerChannel, MeasurementMask @@ -83,6 +83,12 @@ def set_parameters(self, pulse_parameters: Iterable[str], vals=vals.Numbers(), set_cmd=None, sweep_parameter_cmd=sweep_parameter_cmd, send_buffer_cmd=send_buffer_cmd) + + self.add_parameter('_repetitions', parameter_class=QuPulseTemplateParameter, + vals=vals.Numbers(), set_cmd=None, + sweep_parameter_cmd=sweep_parameter_cmd, + send_buffer_cmd=send_buffer_cmd) + self._repetitions.set(0) def to_dict(self) -> Dict: @@ -160,6 +166,9 @@ def __init__(self, name: str, self.template.set(None) + def reset_programs(self): + self._loops.clear() + def _get_template(self) -> PulseTemplate: """ Returns: @@ -247,16 +256,27 @@ def _sweep_parameter(self, parameter, sweep_values) -> None: parameter: Parameter to sweep sweep_values: Values the parameter should be swept over. """ - for l in self._loops: - if l['parameter'] == parameter.name: - raise AttributeError('It is not supported to sweep the same parameter ({}) more than once.'.format(parameter)) - - loop = { - 'parameter' : parameter.name, - 'iterator' : '__' + parameter.name + '_it', - 'sweep_values' : sweep_values, - 'is_sent' : False - } + + if parameter.name == '_repetitions': + loop = { + 'parameter' : parameter.name, + 'is_rep' : True, + 'num' : len(sweep_values), + 'is_sent' : False + } + else: + for l in self._loops: + if l['parameter'] == parameter.name: + raise AttributeError('It is not supported to sweep the same parameter ({}) more than once.'.format(parameter)) + + loop = { + 'parameter' : parameter.name, + 'is_rep' : False, + 'iterator' : '__' + parameter.name + '_it', + 'sweep_values' : sweep_values, + 'is_sent' : False + } + self._loops.append(loop) @@ -300,13 +320,17 @@ def _really_send_buffer(self) -> Dict: parameter_mapping[p] = p for loop in self._loops: - parameter_mapping[loop['parameter']] = "{}[{}]".format(loop['parameter'], loop['iterator']) - params[loop['parameter']] = loop['sweep_values'] + if not loop['is_rep']: + parameter_mapping[loop['parameter']] = "{}[{}]".format(loop['parameter'], loop['iterator']) + params[loop['parameter']] = loop['sweep_values'] pt = MappingPT(self.template.get(), parameter_mapping=parameter_mapping) for loop in reversed(self._loops): - pt = ForLoopPT(pt, loop['iterator'], range(len(loop['sweep_values']))) + if loop['is_rep']: + pt = RepetitionPT(pt, loop['num']) + else: + pt = ForLoopPT(pt, loop['iterator'], range(len(loop['sweep_values']))) program = pt.create_program(parameters=params, channel_mapping=self.channel_mapping.get(), @@ -553,6 +577,12 @@ def __init__(self, name: str, self._affected_dacs = None self._data = None + + def reset_measurements(self): + for p in self.parameters.values(): + if isinstance(p, QuPulseDACChannel): + p.reset() + def _get_measurement_masks(self): """ @@ -695,7 +725,6 @@ def _get_buffered(self, parameter: QuPulseDACChannel): The measurement value(s) of the current measurement step. The shape of the value(s) depends on the operation. """ - if not self._data: parameters = [] for p in self.parameters.values(): From b2ce30e3b9e21fa6faf1335f93d667f32bc33248 Mon Sep 17 00:00:00 2001 From: "Lukas.Lankes" Date: Mon, 28 Jan 2019 11:37:28 +0100 Subject: [PATCH 24/35] FZJ_Decadac: Removed the read-function and merged it into the write-function because every write must be followed by a read. Otherwise the device buffer becomes inconsitent. --- .../instrument_drivers/Harvard/FZJ_Decadac.py | 57 ++++++------------- 1 file changed, 17 insertions(+), 40 deletions(-) diff --git a/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py b/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py index 2bfa16ea0a3b..2a440495af34 100644 --- a/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py +++ b/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py @@ -13,8 +13,10 @@ import warnings -class DACException(Exception): - pass +class DACException(BaseException): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) class DacBase(object): @@ -52,9 +54,6 @@ class DacBase(object): _SLOT_VAL = vals.Ints(0, 4) _TRIG_MODE_VAL = vals.Enum(0, 2,3,4,5,6,7,8, 10,11,12,13,14,15) # The trigger modes 1 and 9 are undefined - # Default buffer size for reading the device parameters - _BUF_SIZE=10 - # Default values of the parameters _DEFAULT_VOLT = 0 _DEFAULT_SWITCH_POS = SWITCH_MID @@ -239,8 +238,8 @@ def _get_volt(self): """ Reads out the voltage of the channel as dac-code """ - buf = self._parent._read(self, DacBase._COMMAND_GET_VOLT) - self._volt = int(buf[-self._BUF_SIZE+1:-1]) + buf = self._parent._write(self, DacBase._COMMAND_GET_VOLT) + self._volt = int(buf[1:-1]) return self._volt @@ -535,13 +534,6 @@ def _write(self, obj, cmd): return self._parent._write(obj, cmd) - def _read(self, obj, buf_size): - """ - Forward the read method of the Decadac class - """ - return self._parent._read(obj, buf_size) - - class Decadac(VisaInstrument): _DEFAULT_RESET = False @@ -582,9 +574,6 @@ def __init__(self, name, address, self.current_slot = None self.current_channel = None - self.current_slot = None - self.current_channel = None - channels = ChannelList(self, "Channels", DacChannel) slots = ChannelList(self, "Slots", DacSlot) @@ -681,29 +670,17 @@ def _write(self, obj, cmd): elif isinstance(obj, DacChannel): self._set_channel(obj._parent, obj) - super().write_raw(cmd) - - if Decadac.enable_output: - print("Decadac._write(\"{}\")".format(cmd)) - - - def _read(self, obj, cmd): - """ - Read the buffer of the device - - Arguments: - obj (object): object that wants to read something (needed to set the current slot and channel) - cmd (str): command - """ - if obj != None: - if isinstance(obj, DacSlot): - self._set_slot(obj) - elif isinstance(obj, DacChannel): - self._set_channel(obj._parent, obj) + buf = super().ask_raw(cmd) - result = super().ask_raw(cmd) + # Check if write and read run successfully + # The first letter of query and answer has to be equal otherwise the answer does not belong to the write-query. Maybe the device buffer is damaged. + # The last letter of the answer has to be an exclamation mark ("!"). This means the write-query was handled successfully. + if buf[0] != cmd[0]: + raise DACException("Could not write \"{}\" to the device. Please check the device buffer.".format(cmd)) + elif buf[-1] != '!': + raise DACException("Could not write \"{}\" to the device.".format(cmd)) if Decadac.enable_output: - print("Decadac._read(\"{}\") = {}".format(cmd, result)) - - return result \ No newline at end of file + print("Decadac._write(\"{}\") = {}".format(cmd, buf)) + + return buf \ No newline at end of file From de5e52384c46e51bc38feedac0daa68c89682e86 Mon Sep 17 00:00:00 2001 From: "Lukas.Lankes" Date: Tue, 12 Feb 2019 08:23:45 +0100 Subject: [PATCH 25/35] qupulse_instrument: run_program_cmd is an attribute of the instrument now and not part of the loop anymore. Added reset methods to the qupulse-instruments, that can ba called after a measurement with errors --- qcodes/instrument/parameter.py | 11 ++ .../QuTech/qupulse_instrument.py | 114 ++++++++++++++---- qcodes/loops.py | 17 ++- 3 files changed, 110 insertions(+), 32 deletions(-) diff --git a/qcodes/instrument/parameter.py b/qcodes/instrument/parameter.py index 856b191c8e30..2c1f2099f3d4 100644 --- a/qcodes/instrument/parameter.py +++ b/qcodes/instrument/parameter.py @@ -1749,6 +1749,7 @@ def __init__(self, name: str, instrument: 'Instrument', sweep_parameter_cmd: Optional[Callable]=None, send_buffer_cmd: Optional[Callable]=None, + run_program_cmd: Optional[Callable]=None, *args, **kwargs): """ Creates a BufferedSweepableParameter that creates buffered sweeps in @@ -1766,6 +1767,7 @@ def __init__(self, name: str, self._sweep_parameter_cmd = sweep_parameter_cmd self._send_buffer_cmd = send_buffer_cmd + self._run_program_cmd = run_program_cmd def set_buffered(self, sweep_values): """ @@ -1784,6 +1786,15 @@ def send_buffer(self): return self._send_buffer_cmd(self) else: return None + + def run_program(self): + """ + Send the buffer to the device. + """ + if self._run_program_cmd is not None: + return self._run_program_cmd(self) + else: + return None class BufferedReadableParameter(ArrayParameter): diff --git a/qcodes/instrument_drivers/QuTech/qupulse_instrument.py b/qcodes/instrument_drivers/QuTech/qupulse_instrument.py index 79b502d9dca1..810a903356e0 100644 --- a/qcodes/instrument_drivers/QuTech/qupulse_instrument.py +++ b/qcodes/instrument_drivers/QuTech/qupulse_instrument.py @@ -63,7 +63,8 @@ def __init__(self, name: str, **kwargs) -> None: def set_parameters(self, pulse_parameters: Iterable[str], sweep_parameter_cmd: Optional[Callable]=None, - send_buffer_cmd: Optional[Callable]=None) -> None: + send_buffer_cmd: Optional[Callable]=None, + run_program_cmd: Optional[Callable]=None) -> None: """ Adds sweepable QCoDeS-parameters from qupulse-template-parameters. @@ -82,7 +83,8 @@ def set_parameters(self, pulse_parameters: Iterable[str], self.add_parameter(p, parameter_class=QuPulseTemplateParameter, vals=vals.Numbers(), set_cmd=None, sweep_parameter_cmd=sweep_parameter_cmd, - send_buffer_cmd=send_buffer_cmd) + send_buffer_cmd=send_buffer_cmd, + run_program_cmd=run_program_cmd) def to_dict(self) -> Dict: @@ -120,6 +122,7 @@ class QuPulseAWGInstrument(Instrument): """ def __init__(self, name: str, + run_program_cmd: Optional[Callable]=None, **kwargs) -> None: """ Creates an QuPulseAWGInstrument that wraps an qupulse-PulseTemplate and @@ -155,6 +158,7 @@ def __init__(self, name: str, self.program_name.set(self.name + "_program") + self.run_program_cmd = run_program_cmd self._channel_map = {} self._loops = [] self.template.set(None) @@ -181,7 +185,8 @@ def _set_template(self, value: PulseTemplate) -> None: if self._template: self.template_parameters.set_parameters(self._template.parameter_names, self._sweep_parameter, - self._send_buffer) + self._send_buffer, + self._run_program) else: self.template_parameters.set_parameters(None, None, None) @@ -255,7 +260,8 @@ def _sweep_parameter(self, parameter, sweep_values) -> None: 'parameter' : parameter.name, 'iterator' : '__' + parameter.name + '_it', 'sweep_values' : sweep_values, - 'is_sent' : False + 'is_sent' : False, + 'is_run' : False } self._loops.append(loop) @@ -272,19 +278,21 @@ def _send_buffer(self, parameter) -> Dict: Dictionary of the measurement windows if the function was called the last parameter. If not it returns None. """ - send = True - - for loop in self._loops: - if loop['parameter'] == parameter.name: - loop['is_sent'] = True - elif not loop['is_sent']: - send = False - - if send: - return self._really_send_buffer() - else: - return None - + if self._loops: + send = True + + for loop in self._loops: + if loop['parameter'] == parameter.name: + loop['is_sent'] = True + elif not loop['is_sent']: + send = False + + if send: + return self._really_send_buffer() + else: + return None + + def _really_send_buffer(self) -> Dict: """ Builts up and sends the buffer to the device and makes the device wait @@ -312,14 +320,12 @@ def _really_send_buffer(self) -> Dict: channel_mapping=self.channel_mapping.get(), measurement_mapping=self.measurement_mapping.get()) - measurement_windows = self._run_program(program) - - self._loops.clear() + measurement_windows = self._upload_program(program) return measurement_windows - def _run_program(self, program, update=True) -> Dict: + def _upload_program(self, program, update=True) -> Dict: """ Sends the buffer to the device and arms it. @@ -393,7 +399,50 @@ def get_default_info(awg): awg.arm(None) return measurement_windows + + + def _run_program(self, parameter): + """ + """ + if self._loops: + run = True + + for loop in self._loops: + if loop['parameter'] == parameter.name: + loop['is_run'] = True + elif not loop['is_run']: + run = False + + if run: + return self._really_run_program() + else: + return None + + + def _really_run_program(self): + """ + """ + if self.run_program_cmd is not None: + self.run_program_cmd() + + self.reset_programs() + + def reset_programs(self): + """ + Resets all previously defined parameter-sweeps. In case of an error + while running a program this can be used before running a new program. + """ + self._loops.clear() + + + def remove_programs(self): + """ + Removes all defined parameter-sweeps, channels and programs + """ + self.reset_programs() + self._channel_map.clear() + def snapshot_base(self, update: bool=False, params_to_skip_update: Sequence[str]=None): @@ -695,7 +744,6 @@ def _get_buffered(self, parameter: QuPulseDACChannel): The measurement value(s) of the current measurement step. The shape of the value(s) depends on the operation. """ - if not self._data: parameters = [] for p in self.parameters.values(): @@ -718,9 +766,31 @@ def _get_buffered(self, parameter: QuPulseDACChannel): del self._data[parameter.name] if self._data != None and not self._data: + del self._data self._data = None return values + + + def reset_measurements(self): + """ + Resets all previously defined measurements. In case of an error while + measuring this can be used before starting a new measurement. + """ + for p in self.parameters.values(): + if isinstance(p, QuPulseDACChannel): + p.reset() + + + def remove_measurements(self): + """ + Removes all defined measurements, measurement-masks and operations + """ + for p in self.parameters.values(): + if isinstance(p, QuPulseDACChannel): + self.parameters.pop(p.name) + + self._measurement_masks.clear() def snapshot_base(self, update: bool=False, diff --git a/qcodes/loops.py b/qcodes/loops.py index c383ad08da36..95fb38bf21de 100644 --- a/qcodes/loops.py +++ b/qcodes/loops.py @@ -337,7 +337,7 @@ class BufferedLoop(Loop): device. So the loop will be run on the hardware. """ - def __init__(self, sweep_values, station=None, run_program_cmd=None): + def __init__(self, sweep_values, station=None): """ Creates a BufferedLoop @@ -348,9 +348,7 @@ def __init__(self, sweep_values, station=None, run_program_cmd=None): if not isinstance(sweep_values.parameter, BufferedSweepableParameter): raise TypeError("The sweep values of a buffered loop must sweep a buffered parameter.") - - self.run_program_cmd = run_program_cmd - + def _copy(self): out = BufferedLoop(self.sweep_values) out.nested_loop = self.nested_loop @@ -396,7 +394,6 @@ def each(self, *actions): actions = [self.nested_loop.each(*actions)] return BufferedActiveLoop(self.sweep_values, *actions, - run_program_cmd=self.run_program_cmd, then_actions=self.then_actions, station=self.station, progress_interval=self.progress_interval, bg_task=self.bg_task, bg_final_task=self.bg_final_task, @@ -1031,7 +1028,7 @@ class BufferedActiveLoop(ActiveLoop): the device. So the loop will be run on the hardware. """ - def __init__(self, sweep_values, *actions, run_program_cmd=None, then_actions=(), + def __init__(self, sweep_values, *actions, then_actions=(), station=None, progress_interval=None, bg_task=None, bg_final_task=None, bg_min_delay=None): """ @@ -1040,8 +1037,6 @@ def __init__(self, sweep_values, *actions, run_program_cmd=None, then_actions=() super().__init__(sweep_values, 0, *actions, then_actions=then_actions, station=station, progress_interval=progress_interval, bg_task=bg_task, bg_final_task=bg_final_task, bg_min_delay=bg_min_delay) - - self.run_program_cmd = run_program_cmd def _set_buffered_sweep(self): """ @@ -1093,8 +1088,10 @@ def _arm_measurement(self): self.actions[0]._arm_measurement() def _run_program(self): - if self.run_program_cmd is not None: - self.run_program_cmd() + self.sweep_values.parameter.run_program() + + if len(self.actions) == 1 and isinstance(self.actions[0], BufferedActiveLoop): + self.actions[0]._run_program() def _run_loop(self, first_delay=0, action_indices=(), loop_indices=(), current_values=(), From 18286b3b6b9b891fa243fcbe0f049ee6382b6ebb Mon Sep 17 00:00:00 2001 From: "Lukas.Lankes" Date: Tue, 12 Feb 2019 10:37:15 +0100 Subject: [PATCH 26/35] - Added repeat function to SweepFixedValues - The functions append, extend, reverse and repeat of SweepFixedValues now return the self-object for a simpler usage --- qcodes/instrument/sweep_values.py | 9 +++++++++ qcodes/loops.py | 4 +++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/qcodes/instrument/sweep_values.py b/qcodes/instrument/sweep_values.py index 07f0df5c3530..0ed3a099b7f9 100644 --- a/qcodes/instrument/sweep_values.py +++ b/qcodes/instrument/sweep_values.py @@ -200,6 +200,7 @@ def append(self, value): self.validate((value,)) self._values.append(value) self._value_snapshot.append({'item': value}) + return self def extend(self, new_values): """ @@ -225,6 +226,7 @@ def extend(self, new_values): else: raise TypeError( 'cannot extend SweepFixedValues with {}'.format(new_values)) + return self def copy(self): """ @@ -247,6 +249,13 @@ def reverse(self): for snap in self._value_snapshot: if 'first' in snap and 'last' in snap: snap['last'], snap['first'] = snap['first'], snap['last'] + return self + + def repeat(self, n): + """ Repeat SweepFixValues n times. """ + self._values = list(self._values) * n + self._value_snapshot = list(self._value_snapshot) * n + return self def snapshot_base(self, update=False): """ diff --git a/qcodes/loops.py b/qcodes/loops.py index 95fb38bf21de..9060b854deed 100644 --- a/qcodes/loops.py +++ b/qcodes/loops.py @@ -1115,7 +1115,9 @@ def _run_loop(self, first_delay=0, action_indices=(), # - (it arms the program on the instruments hardware), # - it configures the measurement on the instrument and # - it arms the measruement on the instruments hardware. - if self._nest_first: + # - it runs the program and starts the measurement + if len(action_indices) == 0 and len(loop_indices) == 0 and len(current_values) == 0: + print("outer loop") self._set_buffered_sweep() # builds up the buffer measurement_windows = self._send_buffer() # sends the buffer to the hardware and arms the hardware From 68ca253717182f4f6404fbe25de7c05b6fdec513 Mon Sep 17 00:00:00 2001 From: "Lukas.Lankes" Date: Wed, 13 Feb 2019 08:34:21 +0100 Subject: [PATCH 27/35] Removed debugging output in loops.py --- qcodes/loops.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qcodes/loops.py b/qcodes/loops.py index 9060b854deed..935b1a9ab22e 100644 --- a/qcodes/loops.py +++ b/qcodes/loops.py @@ -1117,7 +1117,6 @@ def _run_loop(self, first_delay=0, action_indices=(), # - it arms the measruement on the instruments hardware. # - it runs the program and starts the measurement if len(action_indices) == 0 and len(loop_indices) == 0 and len(current_values) == 0: - print("outer loop") self._set_buffered_sweep() # builds up the buffer measurement_windows = self._send_buffer() # sends the buffer to the hardware and arms the hardware From a55584ecd0c415c9160830fdb875b10c6e9edb99 Mon Sep 17 00:00:00 2001 From: "Lukas.Lankes" Date: Thu, 14 Feb 2019 12:43:18 +0100 Subject: [PATCH 28/35] Added classes 'Repetition' and 'BufferedRepetition' and functions 'repeat' and 'buffered_repeat' to loops.py. Repetitions can be used to repeat inner loops without specifiing a parameter. --- qcodes/__init__.py | 2 +- qcodes/instrument/parameter.py | 23 +- .../QuTech/qupulse_instrument.py | 58 +- qcodes/loops.py | 1203 +++++++++++++---- 4 files changed, 1026 insertions(+), 260 deletions(-) diff --git a/qcodes/__init__.py b/qcodes/__init__.py index 0564bde4b536..6c0547944340 100644 --- a/qcodes/__init__.py +++ b/qcodes/__init__.py @@ -33,7 +33,7 @@ from qcodes.station import Station -from qcodes.loops import Loop, BufferedLoop, active_loop, active_data_set +from qcodes.loops import Loop, BufferedLoop, Repetition, BufferedRepetition, active_loop, active_data_set from qcodes.measure import Measure from qcodes.actions import Task, Wait, BreakIf haswebsockets = True diff --git a/qcodes/instrument/parameter.py b/qcodes/instrument/parameter.py index 2c1f2099f3d4..8f4a92121c92 100644 --- a/qcodes/instrument/parameter.py +++ b/qcodes/instrument/parameter.py @@ -1748,6 +1748,7 @@ class BufferedSweepableParameter(Parameter): def __init__(self, name: str, instrument: 'Instrument', sweep_parameter_cmd: Optional[Callable]=None, + repeat_parameter_cmd: Optional[Callable]=None, send_buffer_cmd: Optional[Callable]=None, run_program_cmd: Optional[Callable]=None, *args, **kwargs): @@ -1766,33 +1767,43 @@ def __init__(self, name: str, super().__init__(name, instrument=instrument, *args, **kwargs) self._sweep_parameter_cmd = sweep_parameter_cmd + self._repeat_parameter_cmd = repeat_parameter_cmd self._send_buffer_cmd = send_buffer_cmd self._run_program_cmd = run_program_cmd - def set_buffered(self, sweep_values): + def set_buffered(self, sweep_values, layer): """ Define a buffered sweep for the parameter. """ if self._sweep_parameter_cmd is not None: - return self._sweep_parameter_cmd(self, sweep_values) + return self._sweep_parameter_cmd(self, sweep_values, layer) + else: + return None + + def repeat_buffered(self, repetition_count, layer): + """ + Define a buffered sweep for the parameter. + """ + if self._repeat_parameter_cmd is not None: + return self._repeat_parameter_cmd(repetition_count, layer) else: return None - def send_buffer(self): + def send_buffer(self, layer): """ Send the buffer to the device. """ if self._send_buffer_cmd is not None: - return self._send_buffer_cmd(self) + return self._send_buffer_cmd(layer) else: return None - def run_program(self): + def run_program(self, layer): """ Send the buffer to the device. """ if self._run_program_cmd is not None: - return self._run_program_cmd(self) + return self._run_program_cmd(layer) else: return None diff --git a/qcodes/instrument_drivers/QuTech/qupulse_instrument.py b/qcodes/instrument_drivers/QuTech/qupulse_instrument.py index 810a903356e0..d4773b9a7e68 100644 --- a/qcodes/instrument_drivers/QuTech/qupulse_instrument.py +++ b/qcodes/instrument_drivers/QuTech/qupulse_instrument.py @@ -16,7 +16,7 @@ import qcodes.utils.validators as vals from qcodes.utils.helpers import full_class -from qupulse.pulses import MappingPT, ForLoopPT +from qupulse.pulses import MappingPT, ForLoopPT, RepetitionPT from qupulse.pulses.pulse_template import PulseTemplate from qupulse.hardware.dacs import DAC from qupulse.hardware.setup import ChannelID, _SingleChannel, PlaybackChannel, MarkerChannel, MeasurementMask @@ -63,6 +63,7 @@ def __init__(self, name: str, **kwargs) -> None: def set_parameters(self, pulse_parameters: Iterable[str], sweep_parameter_cmd: Optional[Callable]=None, + repeat_parameter_cmd: Optional[Callable]=None, send_buffer_cmd: Optional[Callable]=None, run_program_cmd: Optional[Callable]=None) -> None: """ @@ -83,6 +84,7 @@ def set_parameters(self, pulse_parameters: Iterable[str], self.add_parameter(p, parameter_class=QuPulseTemplateParameter, vals=vals.Numbers(), set_cmd=None, sweep_parameter_cmd=sweep_parameter_cmd, + repeat_parameter_cmd=repeat_parameter_cmd, send_buffer_cmd=send_buffer_cmd, run_program_cmd=run_program_cmd) @@ -185,6 +187,7 @@ def _set_template(self, value: PulseTemplate) -> None: if self._template: self.template_parameters.set_parameters(self._template.parameter_names, self._sweep_parameter, + self._repeat_parameter, self._send_buffer, self._run_program) else: @@ -244,19 +247,23 @@ def _get_registered_channels(self) -> Dict: return self._channel_map - def _sweep_parameter(self, parameter, sweep_values) -> None: + def _sweep_parameter(self, parameter, sweep_values, layer) -> None: """ Adds the sweep information to a list, to build up a single buffer later Args: parameter: Parameter to sweep sweep_values: Values the parameter should be swept over. + layer: nnumber of nested loop (most outer loop: 0) """ for l in self._loops: - if l['parameter'] == parameter.name: + if l['layer'] == layer: + raise AttributeError('It is not possible to define multiple loops per layer (layer={}).'.format(layer)) + if 'parameter' in l and l['parameter'] == parameter.name: raise AttributeError('It is not supported to sweep the same parameter ({}) more than once.'.format(parameter)) loop = { + 'layer' : layer, 'parameter' : parameter.name, 'iterator' : '__' + parameter.name + '_it', 'sweep_values' : sweep_values, @@ -264,15 +271,36 @@ def _sweep_parameter(self, parameter, sweep_values) -> None: 'is_run' : False } self._loops.append(loop) + + + def _repeat_parameter(self, repetition_count, layer) -> None: + """ + Adds the repetition information to a list to build up a single buffer later + + Args: + repetition_count: number of repetitions + layer: nnumber of nested loop (most outer loop: 0) + """ + for l in self._loops: + if l['layer'] == layer: + raise AttributeError('It is not possible to define multiple loops per layer (layer={}).'.format(layer)) + + loop = { + 'layer' : layer, + 'repetitions' : repetition_count, + 'is_sent' : False, + 'is_run' : False + } + self._loops.append(loop) - def _send_buffer(self, parameter) -> Dict: + def _send_buffer(self, layer) -> Dict: """ Waits until this function is called for all swept parameters. Then the buffer will be built and sent to the device. Args: - parameter: The parameter that calls the function + layer: nnumber of nested loop (most outer loop: 0) Returns: Dictionary of the measurement windows if the function was called @@ -282,7 +310,7 @@ def _send_buffer(self, parameter) -> Dict: send = True for loop in self._loops: - if loop['parameter'] == parameter.name: + if loop['layer'] == layer: loop['is_sent'] = True elif not loop['is_sent']: send = False @@ -308,13 +336,17 @@ def _really_send_buffer(self) -> Dict: parameter_mapping[p] = p for loop in self._loops: - parameter_mapping[loop['parameter']] = "{}[{}]".format(loop['parameter'], loop['iterator']) - params[loop['parameter']] = loop['sweep_values'] + if 'sweep_values' in loop: + parameter_mapping[loop['parameter']] = "{}[{}]".format(loop['parameter'], loop['iterator']) + params[loop['parameter']] = loop['sweep_values'] pt = MappingPT(self.template.get(), parameter_mapping=parameter_mapping) for loop in reversed(self._loops): - pt = ForLoopPT(pt, loop['iterator'], range(len(loop['sweep_values']))) + if 'sweep_values' in loop: + pt = ForLoopPT(pt, loop['iterator'], range(len(loop['sweep_values']))) + elif 'repetitions' in loop: + pt = RepetitionPT(pt, loop['repetitions']) program = pt.create_program(parameters=params, channel_mapping=self.channel_mapping.get(), @@ -401,14 +433,16 @@ def get_default_info(awg): return measurement_windows - def _run_program(self, parameter): + def _run_program(self, layer): """ + Runs the waveform program on the awg as soon as this function was + called for the last (buffered) loop """ if self._loops: run = True for loop in self._loops: - if loop['parameter'] == parameter.name: + if loop['layer'] == layer: loop['is_run'] = True elif not loop['is_run']: run = False @@ -421,6 +455,8 @@ def _run_program(self, parameter): def _really_run_program(self): """ + Runs the program on the awg by calling the configured + callback. """ if self.run_program_cmd is not None: self.run_program_cmd() diff --git a/qcodes/loops.py b/qcodes/loops.py index 935b1a9ab22e..8fecf09dfae9 100644 --- a/qcodes/loops.py +++ b/qcodes/loops.py @@ -66,7 +66,7 @@ def active_loop(): - return ActiveLoop.active_loop + return _BaseActiveLoop.active_loop def active_data_set(): @@ -77,39 +77,16 @@ def active_data_set(): return None -class Loop(Metadatable): +class _BaseLoop(Metadatable): """ - The entry point for creating measurement loops - - Args: - sweep_values: a SweepValues or compatible object describing what - parameter to set in the loop and over what values - delay: a number of seconds to wait after setting a value before - continuing. 0 (default) means no waiting and no warnings. > 0 - means to wait, potentially filling the delay time with monitoring, - and give an error if you wait longer than expected. - progress_interval: should progress of the loop every x seconds. Default - is None (no output) - - After creating a Loop, you attach ``action``\s to it, making an ``ActiveLoop`` - - TODO: - how? Maybe obvious but not specified! that you can ``.run()``, - or you can ``.run()`` a ``Loop`` directly, in which - case it takes the default ``action``\s from the default ``Station`` - - ``actions`` are a sequence of things to do at each ``Loop`` step: they can be - ``Parameter``\s to measure, ``Task``\s to do (any callable that does not yield - data), ``Wait`` times, or other ``ActiveLoop``\s or ``Loop``\s to nest inside - this one. + Base class for Loops, Repetitions, etc. """ - def __init__(self, sweep_values, delay=0, station=None, - progress_interval=None): + + def __init__(self, delay, station, progress_interval): super().__init__() if delay < 0: raise ValueError('delay must be > 0, not {}'.format(repr(delay))) - self.sweep_values = sweep_values self.delay = delay self.station = station self.nested_loop = None @@ -119,7 +96,7 @@ def __init__(self, sweep_values, delay=0, station=None, self.bg_final_task = None self.bg_min_delay = None self.progress_interval = progress_interval - + def __getitem__(self, item): """ Retrieves action with index `item` @@ -183,49 +160,60 @@ def buffered_loop(self, sweep_values): out.nested_loop = BufferedLoop(sweep_values) return out - - def _copy(self): - out = Loop(self.sweep_values, self.delay, - progress_interval=self.progress_interval) - out.nested_loop = self.nested_loop - out.then_actions = self.then_actions - out.station = self.station - return out - def each(self, *actions): + def repeat(self, repetition_count, delay=0): """ - Perform a set of actions at each setting of this loop. - TODO(setting vs setpoints) ? better be verbose. + Nest another loop inside this one. Args: - *actions (Any): actions to perform at each setting of the loop + sweep_values (): + delay (int): - Each action can be: + Examples: + >>> Loop(sv1, d1).loop(sv2, d2).each(*a) - - a Parameter to measure - - a Task to execute - - a Wait - - another Loop or ActiveLoop + is equivalent to: + >>> Loop(sv1, d1).each(Loop(sv2, d2).each(*a)) + + Returns: a new Loop object - the original is untouched """ - actions = list(actions) + out = self._copy() - # check for nested Loops, and activate them with default measurement - for i, action in enumerate(actions): - if isinstance(action, Loop): - default = Station.default.default_measurement - actions[i] = action.each(*default) + if out.nested_loop: + # nest this new loop inside the deepest level + out.nested_loop = out.nested_loop.repeat(repetition_count, delay) + else: + out.nested_loop = Repetition(repetition_count, delay) - self.validate_actions(*actions) + return out + + def buffered_repeat(self, repetition_count): + """ + Nest another loop inside this one. - if self.nested_loop: - # recurse into the innermost loop and apply these actions there - actions = [self.nested_loop.each(*actions)] + Args: + sweep_values (): + + Examples: + >>> Loop(sv1, d1).buffered_loop(sv2, d2).each(*a) + + is equivalent to: + + >>> Loop(sv1, d1).each(BufferedLoop(sv2, d2).each(*a)) + + Returns: a new Loop object - the original is untouched + """ + out = self._copy() + + if out.nested_loop: + # nest this new loop inside the deepest level + out.nested_loop = out.nested_loop.buffered_repeat(repetition_count) + else: + out.nested_loop = BufferedRepetition(repetition_count) + + return out - return ActiveLoop(self.sweep_values, self.delay, *actions, - then_actions=self.then_actions, station=self.station, - progress_interval=self.progress_interval, - bg_task=self.bg_task, bg_final_task=self.bg_final_task, bg_min_delay=self.bg_min_delay) def with_bg_task(self, task, bg_final_task=None, min_delay=0.01): """ @@ -256,7 +244,7 @@ def validate_actions(*actions): if an action is not recognized """ for action in actions: - if isinstance(action, (Task, Wait, BreakIf, ActiveLoop)): + if isinstance(action, (Task, Wait, BreakIf, _BaseActiveLoop)): continue if hasattr(action, 'get') and (hasattr(action, 'name') or hasattr(action, 'names')): @@ -311,6 +299,99 @@ def then(self, *actions, overwrite=False): """ return _attach_then_actions(self._copy(), actions, overwrite) + def snapshot_base(self, update=False): + """ + State of the loop as a JSON-compatible dict. + + Args: + update (bool): If True, update the state by querying the underlying + sweep_values and actions. If False, just use the latest values in + memory. + + Returns: + dict: base snapshot + """ + return { + '__class__': full_class(self), + 'delay': self.delay, + 'then_actions': _actions_snapshot(self.then_actions, update) + } + +class Loop(_BaseLoop): + """ + The entry point for creating measurement loops + + Args: + sweep_values: a SweepValues or compatible object describing what + parameter to set in the loop and over what values + delay: a number of seconds to wait after setting a value before + continuing. 0 (default) means no waiting and no warnings. > 0 + means to wait, potentially filling the delay time with monitoring, + and give an error if you wait longer than expected. + progress_interval: should progress of the loop every x seconds. Default + is None (no output) + + After creating a Loop, you attach ``action``\s to it, making an ``ActiveLoop`` + + TODO: + how? Maybe obvious but not specified! that you can ``.run()``, + or you can ``.run()`` a ``Loop`` directly, in which + case it takes the default ``action``\s from the default ``Station`` + + ``actions`` are a sequence of things to do at each ``Loop`` step: they can be + ``Parameter``\s to measure, ``Task``\s to do (any callable that does not yield + data), ``Wait`` times, or other ``ActiveLoop``\s or ``Loop``\s to nest inside + this one. + """ + def __init__(self, sweep_values, delay=0, station=None, + progress_interval=None): + super().__init__(delay, station, progress_interval) + + self.sweep_values = sweep_values + + def _copy(self): + out = Loop(self.sweep_values, self.delay, + progress_interval=self.progress_interval) + out.nested_loop = self.nested_loop + out.then_actions = self.then_actions + out.station = self.station + return out + + def each(self, *actions): + """ + Perform a set of actions at each setting of this loop. + TODO(setting vs setpoints) ? better be verbose. + + Args: + *actions (Any): actions to perform at each setting of the loop + + Each action can be: + + - a Parameter to measure + - a Task to execute + - a Wait + - another Loop or ActiveLoop + + """ + actions = list(actions) + + # check for nested Loops, and activate them with default measurement + for i, action in enumerate(actions): + if isinstance(action, _BaseLoop): + default = Station.default.default_measurement + actions[i] = action.each(*default) + + self.validate_actions(*actions) + + if self.nested_loop: + # recurse into the innermost loop and apply these actions there + actions = [self.nested_loop.each(*actions)] + + return ActiveLoop(self.sweep_values, self.delay, *actions, + then_actions=self.then_actions, station=self.station, + progress_interval=self.progress_interval, + bg_task=self.bg_task, bg_final_task=self.bg_final_task, bg_min_delay=self.bg_min_delay) + def snapshot_base(self, update=False): """ State of the loop as a JSON-compatible dict. @@ -344,7 +425,7 @@ def __init__(self, sweep_values, station=None): Args: sweep_values: Sweep values of a BufferedSweepableParameter """ - super().__init__(sweep_values, delay=0, station=station) + super().__init__(sweep_values, station=station) if not isinstance(sweep_values.parameter, BufferedSweepableParameter): raise TypeError("The sweep values of a buffered loop must sweep a buffered parameter.") @@ -358,6 +439,9 @@ def _copy(self): def loop(self, *args, **kwargs): raise AttributeError('It is not supported to nest a \'normal\' Loop in a BufferedLoop.') + + def repeat(self, *args, **kwargs): + raise AttributeError('It is not supported to nest a \'normal\' Repetition in a BufferedLoop.') def each(self, *actions): """ @@ -379,10 +463,10 @@ def each(self, *actions): # check for nested Loops, and activate them with default measurement for i, action in enumerate(actions): - if isinstance(action, BufferedLoop): + if isinstance(action, (BufferedLoop, BufferedRepetition)): default = Station.default.default_measurement actions[i] = action.each(*default) - elif isinstance(action, Loop): + elif isinstance(action, _BaseLoop): raise AttributeError('It is not supported to nest a \'normal\' Loop in a BufferedLoop.') elif isinstance(action, _BaseParameter) and not isinstance(action, BufferedReadableParameter): raise AttributeError("The measuring parameters in a buffered loop must be BufferedReadableParameters.") @@ -400,55 +484,199 @@ def each(self, *actions): bg_min_delay=self.bg_min_delay) -def _attach_then_actions(loop, actions, overwrite): - """Inner code for both Loop.then and ActiveLoop.then.""" - for action in actions: - if not isinstance(action, (Task, Wait)): - raise TypeError('Unrecognized action:', action, - '.then() allows only `Task` and `Wait` ' - 'actions.') +class Repetition(_BaseLoop): + """ + The entry point for creating repetitions of nested loops - if overwrite: - loop.then_actions = actions - else: - loop.then_actions = loop.then_actions + actions + Args: + repetition_count: number of repetitions + delay: a number of seconds to wait after setting a value before + continuing. 0 (default) means no waiting and no warnings. > 0 + means to wait, potentially filling the delay time with monitoring, + and give an error if you wait longer than expected. + progress_interval: should progress of the loop every x seconds. Default + is None (no output) - return loop + After creating a Repetition, you attach ``action``\s to it, making an + ``ActiveRepetition`` + """ + def __init__(self, repetition_count, delay=0, station=None, + progress_interval=None): + super().__init__(delay, station, progress_interval) + self.repetition_count = repetition_count -def _attach_bg_task(loop, task, bg_final_task, min_delay): - """Inner code for both Loop and ActiveLoop.bg_task""" - if loop.bg_task is None: - loop.bg_task = task - loop.bg_min_delay = min_delay - else: - raise RuntimeError('Only one background task is allowed per loop') + def _copy(self): + out = Repetition(self.repetition_count, self.delay, + progress_interval=self.progress_interval) + out.nested_loop = self.nested_loop + out.then_actions = self.then_actions + out.station = self.station + return out - if bg_final_task: - loop.bg_final_task = bg_final_task + def each(self, *actions): + """ + Perform a set of actions at each setting of this loop. + TODO(setting vs setpoints) ? better be verbose. - return loop + Args: + *actions (Any): actions to perform at each setting of the loop + Each action can be: -class ActiveLoop(Metadatable): - """ - Created by attaching actions to a *Loop*, this is the object that actually - runs a measurement loop. An *ActiveLoop* can no longer be nested, only run, - or used as an action inside another `Loop` which will run the whole thing. + - a Parameter to measure + - a Task to execute + - a Wait + - another Loop or ActiveLoop - The *ActiveLoop* determines what *DataArray*\s it will need to hold the data - it collects, and it creates a *DataSet* holding these *DataArray*\s - """ + """ + actions = list(actions) - # Currently active loop, is set when calling loop.run(set_active=True) - # is reset to None when active measurement is finished - active_loop = None + # check for nested Loops, and activate them with default measurement + for i, action in enumerate(actions): + if isinstance(action, _BaseLoop): + default = Station.default.default_measurement + actions[i] = action.each(*default) - def __init__(self, sweep_values, delay, *actions, then_actions=(), - station=None, progress_interval=None, bg_task=None, - bg_final_task=None, bg_min_delay=None): - super().__init__() - self.sweep_values = sweep_values + self.validate_actions(*actions) + + if self.nested_loop: + # recurse into the innermost loop and apply these actions there + actions = [self.nested_loop.each(*actions)] + + return ActiveRepetition(self.repetition_count, self.delay, *actions, + then_actions=self.then_actions, station=self.station, + progress_interval=self.progress_interval, + bg_task=self.bg_task, bg_final_task=self.bg_final_task, bg_min_delay=self.bg_min_delay) + + def snapshot_base(self, update=False): + """ + State of the loop as a JSON-compatible dict. + + Args: + update (bool): If True, update the state by querying the underlying + sweep_values and actions. If False, just use the latest values in + memory. + + Returns: + dict: base snapshot + """ + return { + '__class__': full_class(self), + 'repetition_count': self.repetition_count, + 'delay': self.delay, + 'then_actions': _actions_snapshot(self.then_actions, update) + } + + +class BufferedRepetition(Repetition): + """ + A loop that sweeps the parameters in a buffer that is sent once to the + device. So the loop will be run on the hardware. + """ + + def __init__(self, repetition_count, station=None): + """ + Creates a BufferedLoop + + Args: + sweep_values: Sweep values of a BufferedSweepableParameter + """ + super().__init__(repetition_count, station=station) + + def _copy(self): + out = BufferedRepetition(self.repetition_count) + out.nested_loop = self.nested_loop + out.then_actions = self.then_actions + out.station = self.station + return out + + def loop(self, *args, **kwargs): + raise AttributeError('It is not supported to nest a \'normal\' Loop in a BufferedLoop or -Repetition.') + + def repeat(self, *args, **kwargs): + raise AttributeError('It is not supported to nest a \'normal\' Repetition in a BufferedLoop or -Repetition.') + + def each(self, *actions): + """ + Perform a set of actions at each setting of this loop. + TODO(setting vs setpoints) ? better be verbose. + + Args: + *actions (Any): actions to perform at each setting of the loop + + Each action can be: + + - a Parameter to measure + - a Task to execute + - a Wait + - another Loop or ActiveLoop + + """ + actions = list(actions) + + # check for nested Loops, and activate them with default measurement + for i, action in enumerate(actions): + if isinstance(action, (BufferedLoop, BufferedRepetition)): + default = Station.default.default_measurement + actions[i] = action.each(*default) + elif isinstance(action, _BaseLoop): + raise AttributeError('It is not supported to nest a \'normal\' Loop in a BufferedLoop.') + elif isinstance(action, _BaseParameter) and not isinstance(action, BufferedReadableParameter): + raise AttributeError("The measuring parameters in a buffered loop must be BufferedReadableParameters.") + + self.validate_actions(*actions) + + if self.nested_loop: + # recurse into the innermost loop and apply these actions there + actions = [self.nested_loop.each(*actions)] + + return BufferedActiveRepetition(self.repetition_count, *actions, + then_actions=self.then_actions, station=self.station, + progress_interval=self.progress_interval, + bg_task=self.bg_task, bg_final_task=self.bg_final_task, + bg_min_delay=self.bg_min_delay) + + +def _attach_then_actions(loop, actions, overwrite): + """Inner code for both Loop.then and ActiveLoop.then.""" + for action in actions: + if not isinstance(action, (Task, Wait)): + raise TypeError('Unrecognized action:', action, + '.then() allows only `Task` and `Wait` ' + 'actions.') + + if overwrite: + loop.then_actions = actions + else: + loop.then_actions = loop.then_actions + actions + + return loop + + +def _attach_bg_task(loop, task, bg_final_task, min_delay): + """Inner code for both Loop and ActiveLoop.bg_task""" + if loop.bg_task is None: + loop.bg_task = task + loop.bg_min_delay = min_delay + else: + raise RuntimeError('Only one background task is allowed per loop') + + if bg_final_task: + loop.bg_final_task = bg_final_task + + return loop + + +class _BaseActiveLoop(Metadatable): + # Currently active loop, is set when calling loop.run(set_active=True) + # is reset to None when active measurement is finished + active_loop = None + + def __init__(self, delay, *actions, then_actions=(), station=None, + progress_interval=None, bg_task=None, bg_final_task=None, + bg_min_delay=None): + super().__init__() self.delay = delay self.actions = list(actions) self.progress_interval = progress_interval @@ -475,26 +703,6 @@ def __getitem__(self, item): """ return self.actions[item] - def then(self, *actions, overwrite=False): - """ - Attach actions to be performed after the loop completes. - - These can only be `Task` and `Wait` actions, as they may not generate - any data. - - returns a new ActiveLoop object - the original is untouched - - \*actions: `Task` and `Wait` objects to execute in order - - Args: - overwrite: (default False) whether subsequent .then() calls (including - calls in an ActiveLoop after .then() has already been called on - the Loop) will add to each other or overwrite the earlier ones. - """ - loop = ActiveLoop(self.sweep_values, self.delay, *self.actions, - then_actions=self.then_actions, station=self.station) - return _attach_then_actions(loop, actions, overwrite) - def with_bg_task(self, task, bg_final_task=None, min_delay=0.01): """ Attaches a background task to this loop. @@ -518,55 +726,11 @@ def snapshot_base(self, update=False): """Snapshot of this ActiveLoop's definition.""" return { '__class__': full_class(self), - 'sweep_values': self.sweep_values.snapshot(update=update), 'delay': self.delay, 'actions': _actions_snapshot(self.actions, update), 'then_actions': _actions_snapshot(self.then_actions, update) } - def containers(self): - """ - Finds the data arrays that will be created by the actions in this - loop, and nests them inside this level of the loop. - - Recursively calls `.containers` on any enclosed actions. - """ - loop_size = len(self.sweep_values) - data_arrays = [] - loop_array = DataArray(parameter=self.sweep_values.parameter, - is_setpoint=True) - loop_array.nest(size=loop_size) - - data_arrays = [loop_array] - # hack set_data into actions - new_actions = self.actions[:] - if hasattr(self.sweep_values, "parameters"): # combined parameter - for parameter in self.sweep_values.parameters: - new_actions.append(parameter) - - for i, action in enumerate(new_actions): - if hasattr(action, 'containers'): - action_arrays = action.containers() - - elif hasattr(action, 'get'): - # this action is a parameter to measure - # note that this supports lists (separate output arrays) - # and arrays (nested in one/each output array) of return values - action_arrays = self._parameter_arrays(action) - - else: - # this *is* covered but the report misses it because Python - # optimizes it away. See: - # https://bitbucket.org/ned/coveragepy/issues/198 - continue # pragma: no cover - - for array in action_arrays: - array.nest(size=loop_size, action_index=i, - set_array=loop_array) - data_arrays.extend(action_arrays) - - return data_arrays - def _parameter_arrays(self, action): out = [] @@ -826,7 +990,7 @@ def run(self, use_threads=False, quiet=False, station=None, data_set.save_metadata() if set_active: - ActiveLoop.active_loop = self + _BaseActiveLoop.active_loop = self try: if not quiet: @@ -844,7 +1008,7 @@ def run(self, use_threads=False, quiet=False, station=None, # this one later. self.data_set = None if set_active: - ActiveLoop.active_loop = None + _BaseActiveLoop.active_loop = None return ds @@ -873,7 +1037,7 @@ def _compile_actions(self, actions, action_indices=()): def _compile_one(self, action, new_action_indices): if isinstance(action, Wait): return Task(self._wait, action.delay) - elif isinstance(action, ActiveLoop): + elif isinstance(action, _BaseActiveLoop): return _Nest(action, new_action_indices) else: return action @@ -890,66 +1054,165 @@ def _run_wrapper(self, *args, **kwargs): self.data_set.add_metadata({'loop': {'ts_end': ts}}) self.data_set.finalize() - def _run_loop(self, first_delay=0, action_indices=(), - loop_indices=(), current_values=(), - **ignore_kwargs): - """ - the routine that actually executes the loop, and can be called - from one loop to execute a nested loop + def _wait(self, delay): + if delay: + finish_clock = time.perf_counter() + delay + t = wait_secs(finish_clock) + time.sleep(t) - first_delay: any delay carried over from an outer loop - action_indices: where we are in any outer loop action arrays - loop_indices: setpoint indices in any outer loops - current_values: setpoint values in any outer loops - signal_queue: queue to communicate with main process directly - ignore_kwargs: for compatibility with other loop tasks - """ +class ActiveLoop(_BaseActiveLoop): + """ + Created by attaching actions to a *Loop*, this is the object that actually + runs a measurement loop. An *ActiveLoop* can no longer be nested, only run, + or used as an action inside another `Loop` which will run the whole thing. - # at the beginning of the loop, the time to wait after setting - # the loop parameter may be increased if an outer loop requested longer - delay = max(self.delay, first_delay) + The *ActiveLoop* determines what *DataArray*\s it will need to hold the data + it collects, and it creates a *DataSet* holding these *DataArray*\s + """ - callables = self._compile_actions(self.actions, action_indices) - n_callables = 0 - for item in callables: - if hasattr(item, 'param_ids'): - n_callables += len(item.param_ids) - else: - n_callables += 1 - t0 = time.time() - last_task = t0 - imax = len(self.sweep_values) + def __init__(self, sweep_values, delay, *actions, then_actions=(), + station=None, progress_interval=None, bg_task=None, + bg_final_task=None, bg_min_delay=None): + super().__init__(delay, *actions, then_actions=then_actions, + station=station, progress_interval=progress_interval, + bg_task=bg_task, bg_final_task=bg_final_task, + bg_min_delay=bg_min_delay) + + self.sweep_values = sweep_values - self.last_task_failed = False + def then(self, *actions, overwrite=False): + """ + Attach actions to be performed after the loop completes. - for i, value in enumerate(self.sweep_values): - if self.progress_interval is not None: - tprint('loop %s: %d/%d (%.1f [s])' % ( - self.sweep_values.name, i, imax, time.time() - t0), - dt=self.progress_interval, tag='outerloop') - if i: - tprint("Estimated finish time: %s" % ( - time.asctime(time.localtime(t0 + ((time.time() - t0) * imax / i)))), - dt=self.progress_interval, tag="finish") + These can only be `Task` and `Wait` actions, as they may not generate + any data. - set_val = self.sweep_values.set(value) + returns a new ActiveLoop object - the original is untouched - new_indices = loop_indices + (i,) - new_values = current_values + (value,) - data_to_store = {} + \*actions: `Task` and `Wait` objects to execute in order - if hasattr(self.sweep_values, "parameters"): # combined parameter - set_name = self.data_set.action_id_map[action_indices] - if hasattr(self.sweep_values, 'aggregate'): - value = self.sweep_values.aggregate(*set_val) - # below is useful but too verbose even at debug - # log.debug('Calling .store method of DataSet because ' - # 'sweep_values.parameters exist') - self.data_set.store(new_indices, {set_name: value}) - # set_val list of values to set [param1_setpoint, param2_setpoint ..] - for j, val in enumerate(set_val): - set_index = action_indices + (j+n_callables, ) - set_name = (self.data_set.action_id_map[set_index]) + Args: + overwrite: (default False) whether subsequent .then() calls (including + calls in an ActiveLoop after .then() has already been called on + the Loop) will add to each other or overwrite the earlier ones. + """ + loop = ActiveLoop(self.sweep_values, self.delay, *self.actions, + then_actions=self.then_actions, station=self.station) + return _attach_then_actions(loop, actions, overwrite) + + def snapshot_base(self, update=False): + """Snapshot of this ActiveLoop's definition.""" + return { + '__class__': full_class(self), + 'sweep_values': self.sweep_values.snapshot(update=update), + 'delay': self.delay, + 'actions': _actions_snapshot(self.actions, update), + 'then_actions': _actions_snapshot(self.then_actions, update) + } + + def containers(self): + """ + Finds the data arrays that will be created by the actions in this + loop, and nests them inside this level of the loop. + + Recursively calls `.containers` on any enclosed actions. + """ + loop_size = len(self.sweep_values) + data_arrays = [] + loop_array = DataArray(parameter=self.sweep_values.parameter, + is_setpoint=True) + loop_array.nest(size=loop_size) + + data_arrays = [loop_array] + # hack set_data into actions + new_actions = self.actions[:] + if hasattr(self.sweep_values, "parameters"): # combined parameter + for parameter in self.sweep_values.parameters: + new_actions.append(parameter) + + for i, action in enumerate(new_actions): + if hasattr(action, 'containers'): + action_arrays = action.containers() + + elif hasattr(action, 'get'): + # this action is a parameter to measure + # note that this supports lists (separate output arrays) + # and arrays (nested in one/each output array) of return values + action_arrays = self._parameter_arrays(action) + + else: + # this *is* covered but the report misses it because Python + # optimizes it away. See: + # https://bitbucket.org/ned/coveragepy/issues/198 + continue # pragma: no cover + + for array in action_arrays: + array.nest(size=loop_size, action_index=i, + set_array=loop_array) + data_arrays.extend(action_arrays) + + return data_arrays + + def _run_loop(self, first_delay=0, action_indices=(), + loop_indices=(), current_values=(), + **ignore_kwargs): + """ + the routine that actually executes the loop, and can be called + from one loop to execute a nested loop + + first_delay: any delay carried over from an outer loop + action_indices: where we are in any outer loop action arrays + loop_indices: setpoint indices in any outer loops + current_values: setpoint values in any outer loops + signal_queue: queue to communicate with main process directly + ignore_kwargs: for compatibility with other loop tasks + """ + + # at the beginning of the loop, the time to wait after setting + # the loop parameter may be increased if an outer loop requested longer + delay = max(self.delay, first_delay) + + callables = self._compile_actions(self.actions, action_indices) + n_callables = 0 + for item in callables: + if hasattr(item, 'param_ids'): + n_callables += len(item.param_ids) + else: + n_callables += 1 + t0 = time.time() + last_task = t0 + imax = len(self.sweep_values) + + self.last_task_failed = False + + for i, value in enumerate(self.sweep_values): + if self.progress_interval is not None: + tprint('loop %s: %d/%d (%.1f [s])' % ( + self.sweep_values.name, i, imax, time.time() - t0), + dt=self.progress_interval, tag='outerloop') + if i: + tprint("Estimated finish time: %s" % ( + time.asctime(time.localtime(t0 + ((time.time() - t0) * imax / i)))), + dt=self.progress_interval, tag="finish") + + set_val = self.sweep_values.set(value) + + new_indices = loop_indices + (i,) + new_values = current_values + (value,) + data_to_store = {} + + if hasattr(self.sweep_values, "parameters"): # combined parameter + set_name = self.data_set.action_id_map[action_indices] + if hasattr(self.sweep_values, 'aggregate'): + value = self.sweep_values.aggregate(*set_val) + # below is useful but too verbose even at debug + # log.debug('Calling .store method of DataSet because ' + # 'sweep_values.parameters exist') + self.data_set.store(new_indices, {set_name: value}) + # set_val list of values to set [param1_setpoint, param2_setpoint ..] + for j, val in enumerate(set_val): + set_index = action_indices + (j+n_callables, ) + set_name = (self.data_set.action_id_map[set_index]) data_to_store[set_name] = val else: set_name = self.data_set.action_id_map[action_indices] @@ -1015,12 +1278,6 @@ def _run_loop(self, first_delay=0, action_indices=(), log.debug('Running the bg_final_task') self.bg_final_task() - def _wait(self, delay): - if delay: - finish_clock = time.perf_counter() + delay - t = wait_secs(finish_clock) - time.sleep(t) - class BufferedActiveLoop(ActiveLoop): """ @@ -1037,27 +1294,52 @@ def __init__(self, sweep_values, *actions, then_actions=(), super().__init__(sweep_values, 0, *actions, then_actions=then_actions, station=station, progress_interval=progress_interval, bg_task=bg_task, bg_final_task=bg_final_task, bg_min_delay=bg_min_delay) + + self._most_outer_buffered_loop = None + + def _is_most_outer_buffered_loop(self, set_value=True): + """ + The first call of this functin will set the _most_outer_loop attribute + to True for the self object and to False for all nested loops + """ + if self._most_outer_buffered_loop == None: + self._most_outer_buffered_loop = set_value + + if len(self.actions) == 1 and isinstance(self.actions[0], (BufferedActiveLoop, BufferedActiveRepetition)): + self.actions[0]._is_most_outer_buffered_loop(False) + + return self._most_outer_buffered_loop - def _set_buffered_sweep(self): + def _get_loop_parameter(self): + """ + Returns parameter of the loop. + """ + return self.sweep_values.parameter + + def _set_buffered_sweep(self, layer=0): """ Builds up the buffers on the instrument by setting the loop information (sweep_values). """ - self.sweep_values.parameter.set_buffered(list(self.sweep_values)) + parameter = self._get_loop_parameter() + + parameter.set_buffered(list(self.sweep_values), layer) - if len(self.actions) == 1 and isinstance(self.actions[0], BufferedActiveLoop): - self.actions[0]._set_buffered_sweep() + if len(self.actions) == 1 and isinstance(self.actions[0], (BufferedActiveLoop, BufferedActiveRepetition)): + self.actions[0]._set_buffered_sweep(layer + 1) - def _send_buffer(self): + def _send_buffer(self, layer=0): """ Lets the instrument(s) send the buffer(s) to the hardware (and arms the hardware). """ - mw = self.sweep_values.parameter.send_buffer() + parameter = self._get_loop_parameter() + + mw = parameter.send_buffer(layer) if mw is None: mw = {} - if len(self.actions) == 1 and isinstance(self.actions[0], BufferedActiveLoop): - mw_tmp = self.actions[0]._send_buffer() + if len(self.actions) == 1 and isinstance(self.actions[0], (BufferedActiveLoop, BufferedActiveRepetition)): + mw_tmp = self.actions[0]._send_buffer(layer + 1) if mw_tmp is not None: mw = {**mw, **mw_tmp} @@ -1073,7 +1355,7 @@ def _configure_measurement(self, measurement_windows): if isinstance(action, BufferedReadableParameter): # Measurement action.configure_measurement(measurement_windows) - if len(self.actions) == 1 and isinstance(self.actions[0], BufferedActiveLoop): + if len(self.actions) == 1 and isinstance(self.actions[0], (BufferedActiveLoop, BufferedActiveRepetition)): self.actions[0]._configure_measurement(measurement_windows) def _arm_measurement(self): @@ -1084,14 +1366,16 @@ def _arm_measurement(self): if isinstance(action, BufferedReadableParameter): # Measurement action.arm_measurement() - if len(self.actions) == 1 and isinstance(self.actions[0], BufferedActiveLoop): + if len(self.actions) == 1 and isinstance(self.actions[0], (BufferedActiveLoop, BufferedActiveRepetition)): self.actions[0]._arm_measurement() - def _run_program(self): - self.sweep_values.parameter.run_program() + def _run_program(self, layer=0): + parameter = self._get_loop_parameter() + + parameter.run_program(layer) - if len(self.actions) == 1 and isinstance(self.actions[0], BufferedActiveLoop): - self.actions[0]._run_program() + if len(self.actions) == 1 and isinstance(self.actions[0], (BufferedActiveLoop, BufferedActiveRepetition)): + self.actions[0]._run_program(layer + 1) def _run_loop(self, first_delay=0, action_indices=(), loop_indices=(), current_values=(), @@ -1116,7 +1400,7 @@ def _run_loop(self, first_delay=0, action_indices=(), # - it configures the measurement on the instrument and # - it arms the measruement on the instruments hardware. # - it runs the program and starts the measurement - if len(action_indices) == 0 and len(loop_indices) == 0 and len(current_values) == 0: + if self._is_most_outer_buffered_loop(): self._set_buffered_sweep() # builds up the buffer measurement_windows = self._send_buffer() # sends the buffer to the hardware and arms the hardware @@ -1235,6 +1519,441 @@ def _run_loop(self, first_delay=0, action_indices=(), log.debug('Running the bg_final_task') self.bg_final_task() + def _wait(self, delay): + if delay: + finish_clock = time.perf_counter() + delay + t = wait_secs(finish_clock) + time.sleep(t) + + +class ActiveRepetition(_BaseActiveLoop): + """ + Created by attaching actions to a *Repetition*, this is the object that actually + repeats a nested measurement loop. An *ActiveRepetition* can no longer be nested, only run, + or used as an action inside another `Loop` or `Repetition` which will run the whole thing. + + The *ActiveRepetition* determines what *DataArray*\s it will need to hold the data + it collects, and it creates a *DataSet* holding these *DataArray*\s + """ + + def __init__(self, repetition_count, delay, *actions, then_actions=(), + station=None, progress_interval=None, bg_task=None, + bg_final_task=None, bg_min_delay=None): + super().__init__(delay, *actions, then_actions=then_actions, + station=station, progress_interval=progress_interval, + bg_task=bg_task, bg_final_task=bg_final_task, + bg_min_delay=bg_min_delay) + + self.repetition_count = repetition_count + + def then(self, *actions, overwrite=False): + """ + Attach actions to be performed after the loop completes. + + These can only be `Task` and `Wait` actions, as they may not generate + any data. + + returns a new ActiveLoop object - the original is untouched + + \*actions: `Task` and `Wait` objects to execute in order + + Args: + overwrite: (default False) whether subsequent .then() calls (including + calls in an ActiveLoop after .then() has already been called on + the Loop) will add to each other or overwrite the earlier ones. + """ + loop = ActiveRepetition(self.repetition_count, self.delay, *self.actions, + then_actions=self.then_actions, station=self.station) + return _attach_then_actions(loop, actions, overwrite) + + def snapshot_base(self, update=False): + """Snapshot of this ActiveLoop's definition.""" + return { + '__class__': full_class(self), + 'repetition_count': self.repetition_count, + 'delay': self.delay, + 'actions': _actions_snapshot(self.actions, update), + 'then_actions': _actions_snapshot(self.then_actions, update) + } + + def containers(self): + """ + Finds the data arrays that will be created by the actions in this + loop, and nests them inside this level of the loop. + + Recursively calls `.containers` on any enclosed actions. + """ + loop_size = self.repetition_count + data_arrays = [] + loop_array = DataArray(name='__repetition', is_setpoint=True) + loop_array.nest(size=loop_size) + + data_arrays = [loop_array] + # hack set_data into actions + new_actions = self.actions[:] + + for i, action in enumerate(new_actions): + if hasattr(action, 'containers'): + action_arrays = action.containers() + + elif hasattr(action, 'get'): + # this action is a parameter to measure + # note that this supports lists (separate output arrays) + # and arrays (nested in one/each output array) of return values + action_arrays = self._parameter_arrays(action) + + else: + # this *is* covered but the report misses it because Python + # optimizes it away. See: + # https://bitbucket.org/ned/coveragepy/issues/198 + continue # pragma: no cover + + for array in action_arrays: + array.nest(size=loop_size, action_index=i, + set_array=loop_array) + data_arrays.extend(action_arrays) + + return data_arrays + + def _run_loop(self, first_delay=0, action_indices=(), + loop_indices=(), current_values=(), + **ignore_kwargs): + """ + the routine that actually executes the loop, and can be called + from one loop to execute a nested loop + + first_delay: any delay carried over from an outer loop + action_indices: where we are in any outer loop action arrays + loop_indices: setpoint indices in any outer loops + current_values: setpoint values in any outer loops + signal_queue: queue to communicate with main process directly + ignore_kwargs: for compatibility with other loop tasks + """ + + # at the beginning of the loop, the time to wait after setting + # the loop parameter may be increased if an outer loop requested longer + delay = max(self.delay, first_delay) + + callables = self._compile_actions(self.actions, action_indices) + n_callables = 0 + for item in callables: + if hasattr(item, 'param_ids'): + n_callables += len(item.param_ids) + else: + n_callables += 1 + t0 = time.time() + last_task = t0 + imax = self.repetition_count + + self.last_task_failed = False + + for i in range(self.repetition_count): + if self.progress_interval is not None: + tprint('repetition: %d/%d (%.1f [s])' % ( + i, imax, time.time() - t0), + dt=self.progress_interval, tag='outerloop') + if i: + tprint("Estimated finish time: %s" % ( + time.asctime(time.localtime(t0 + ((time.time() - t0) * imax / i)))), + dt=self.progress_interval, tag="finish") + + new_indices = loop_indices + (i,) + new_values = current_values + (i,) + data_to_store = {} + + set_name = self.data_set.action_id_map[action_indices] + data_to_store[set_name] = i + # below is useful but too verbose even at debug + # log.debug('Calling .store method of DataSet because a sweep step' + # ' was taken') + self.data_set.store(new_indices, data_to_store) + + if not self._nest_first: + # only wait the delay time if an inner loop will not inherit it + self._wait(delay) + + try: + for f in callables: + # below is useful but too verbose even at debug + # log.debug('Going through callables at this sweep step.' + # ' Calling {}'.format(f)) + f(first_delay=delay, + loop_indices=new_indices, + current_values=new_values) + + # after the first action, no delay is inherited + delay = 0 + except _QcodesBreak: + break + + # after the first setpoint, delay reverts to the loop delay + delay = self.delay + + # now check for a background task and execute it if it's + # been long enough since the last time + # don't let exceptions in the background task interrupt + # the loop + # if the background task fails twice consecutively, stop + # executing it + if self.bg_task is not None: + t = time.time() + if t - last_task >= self.bg_min_delay: + try: + self.bg_task() + except Exception: + if self.last_task_failed: + self.bg_task = None + self.last_task_failed = True + log.exception("Failed to execute bg task") + + last_task = t + + # run the background task one last time to catch the last setpoint(s) + if self.bg_task is not None: + log.debug('Running the background task one last time.') + self.bg_task() + + # the loop is finished - run the .then actions + #log.debug('Finishing loop, running the .then actions...') + for f in self._compile_actions(self.then_actions, ()): + #log.debug('...running .then action {}'.format(f)) + f() + + # run the bg_final_task from the bg_task: + if self.bg_final_task is not None: + log.debug('Running the bg_final_task') + self.bg_final_task() + + +class BufferedActiveRepetition(ActiveRepetition): + """ + An ActiveLoop that sweeps the parameters in a buffer that is sent once to + the device. So the loop will be run on the hardware. + """ + + def __init__(self, repetition_count, *actions, then_actions=(), + station=None, progress_interval=None, bg_task=None, + bg_final_task=None, bg_min_delay=None): + """ + Creates a BufferedActiveLoop + """ + super().__init__(repetition_count, 0, *actions, then_actions=then_actions, + station=station, progress_interval=progress_interval, bg_task=bg_task, + bg_final_task=bg_final_task, bg_min_delay=bg_min_delay) + + self._most_outer_buffered_loop = None + + def _is_most_outer_buffered_loop(self, set_value=True): + """ + The first call of this functin will set the _most_outer_loop attribute + to True for the self object and to False for all nested loops + """ + if self._most_outer_buffered_loop == None: + self._most_outer_buffered_loop = set_value + + if len(self.actions) == 1 and isinstance(self.actions[0], (BufferedActiveLoop, BufferedActiveRepetition)): + self.actions[0]._is_most_outer_buffered_loop(False) + + return self._most_outer_buffered_loop + + + def _get_loop_parameter(self): + """ + Returns the parameter of the inner loop. If the inner loop is also a + repetition, the next inner loop will be checked, because repetitions + are not mapped to a parameter or instrument. + """ + if len(self.actions) == 1 and isinstance(self.actions[0], (BufferedActiveLoop, BufferedActiveRepetition)): + return self.actions[0]._get_loop_parameter() + else: + return None + + def _set_buffered_sweep(self, layer=0): + """ + Builds up the buffers on the instrument by setting the loop information + (sweep_values). + """ + parameter = self._get_loop_parameter() + + parameter.repeat_buffered(self.repetition_count, layer) + + if len(self.actions) == 1 and isinstance(self.actions[0], (BufferedActiveLoop, BufferedActiveRepetition)): + self.actions[0]._set_buffered_sweep(layer + 1) + + def _send_buffer(self, layer=0): + """ + Lets the instrument(s) send the buffer(s) to the hardware (and arms the + hardware). + """ + parameter = self._get_loop_parameter() + + mw = parameter.send_buffer(layer) + if mw is None: mw = {} + + if len(self.actions) == 1 and isinstance(self.actions[0], (BufferedActiveLoop, BufferedActiveRepetition)): + mw_tmp = self.actions[0]._send_buffer(layer + 1) + + if mw_tmp is not None: + mw = {**mw, **mw_tmp} + + return mw + + def _configure_measurement(self, measurement_windows): + """ + Configures the measurement on the instrument by setting the measurement + windows (measurement times). + """ + for action in self.actions: + if isinstance(action, BufferedReadableParameter): # Measurement + action.configure_measurement(measurement_windows) + + if len(self.actions) == 1 and isinstance(self.actions[0], (BufferedActiveLoop, BufferedActiveRepetition)): + self.actions[0]._configure_measurement(measurement_windows) + + def _arm_measurement(self): + """ + Arms the instruments hardware for the measurement + """ + for action in self.actions: + if isinstance(action, BufferedReadableParameter): # Measurement + action.arm_measurement() + + if len(self.actions) == 1 and isinstance(self.actions[0], (BufferedActiveLoop, BufferedActiveRepetition)): + self.actions[0]._arm_measurement() + + def _run_program(self, layer=0): + parameter = self._get_loop_parameter() + + parameter.run_program(layer) + + if len(self.actions) == 1 and isinstance(self.actions[0], (BufferedActiveLoop, BufferedActiveRepetition)): + self.actions[0]._run_program(layer + 1) + + def _run_loop(self, first_delay=0, action_indices=(), + loop_indices=(), current_values=(), + **ignore_kwargs): + """ + the routine that actually executes the loop, and can be called + from one loop to execute a nested loop + + first_delay: any delay carried over from an outer loop + action_indices: where we are in any outer loop action arrays + loop_indices: setpoint indices in any outer loops + current_values: setpoint values in any outer loops + signal_queue: queue to communicate with main process directly + ignore_kwargs: for compatibility with other loop tasks + """ + # The most outer loop controls the buffered loop: + # - It lets the instrument(s) build up the buffer, + # - it registers the instruments program/buffer to the hardware, + # - it lets the instrument(s) send their buffers to the hardware, + # - (it arms the program on the instruments hardware), + # - it configures the measurement on the instrument and + # - it arms the measruement on the instruments hardware. + # - it runs the program and starts the measurement + if self._is_most_outer_buffered_loop(): + self._set_buffered_sweep() # builds up the buffer + measurement_windows = self._send_buffer() # sends the buffer to the hardware and arms the hardware + + self._configure_measurement(measurement_windows) # configures the measurement windows + self._arm_measurement() # arms the hardware for the measurement + + self._run_program() # call user defined run_program-function + + # at the beginning of the loop, the time to wait after setting + # the loop parameter may be increased if an outer loop requested longer + delay = max(self.delay, first_delay) + + callables = self._compile_actions(self.actions, action_indices) + n_callables = 0 + for item in callables: + if hasattr(item, 'param_ids'): + n_callables += len(item.param_ids) + else: + n_callables += 1 + t0 = time.time() + last_task = t0 + imax = self.repetition_count + + self.last_task_failed = False + + for i in range(self.repetition_count): + if self.progress_interval is not None: + tprint('repetition: %d/%d (%.1f [s])' % ( + i, imax, time.time() - t0), + dt=self.progress_interval, tag='outerloop') + if i: + tprint("Estimated finish time: %s" % ( + time.asctime(time.localtime(t0 + ((time.time() - t0) * imax / i)))), + dt=self.progress_interval, tag="finish") + + new_indices = loop_indices + (i,) + new_values = current_values + (i,) + data_to_store = {} + + set_name = self.data_set.action_id_map[action_indices] + data_to_store[set_name] = i + # below is useful but too verbose even at debug + # log.debug('Calling .store method of DataSet because a sweep step' + # ' was taken') + self.data_set.store(new_indices, data_to_store) + + if not self._nest_first: + # only wait the delay time if an inner loop will not inherit it + self._wait(delay) + + try: + for f in callables: + # below is useful but too verbose even at debug + # log.debug('Going through callables at this sweep step.' + # ' Calling {}'.format(f)) + f(first_delay=delay, + loop_indices=new_indices, + current_values=new_values) + + # after the first action, no delay is inherited + delay = 0 + except _QcodesBreak: + break + + # after the first setpoint, delay reverts to the loop delay + delay = self.delay + + # now check for a background task and execute it if it's + # been long enough since the last time + # don't let exceptions in the background task interrupt + # the loop + # if the background task fails twice consecutively, stop + # executing it + if self.bg_task is not None: + t = time.time() + if t - last_task >= self.bg_min_delay: + try: + self.bg_task() + except Exception: + if self.last_task_failed: + self.bg_task = None + self.last_task_failed = True + log.exception("Failed to execute bg task") + + last_task = t + + # run the background task one last time to catch the last setpoint(s) + if self.bg_task is not None: + log.debug('Running the background task one last time.') + self.bg_task() + + # the loop is finished - run the .then actions + #log.debug('Finishing loop, running the .then actions...') + for f in self._compile_actions(self.then_actions, ()): + #log.debug('...running .then action {}'.format(f)) + f() + + # run the bg_final_task from the bg_task: + if self.bg_final_task is not None: + log.debug('Running the bg_final_task') + self.bg_final_task() + def _wait(self, delay): if delay: finish_clock = time.perf_counter() + delay From 62f5d051194afbc2facd402531c25720678324dc Mon Sep 17 00:00:00 2001 From: "Lukas.Lankes" Date: Mon, 11 Mar 2019 10:34:25 +0100 Subject: [PATCH 29/35] Renamed BufferedReadableParameter to BufferedReadableArrayParameter --- qcodes/__init__.py | 2 +- qcodes/instrument/parameter.py | 2 +- .../QuTech/qupulse_instrument.py | 4 ++-- qcodes/loops.py | 18 +++++++++--------- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/qcodes/__init__.py b/qcodes/__init__.py index 51d1622aa42b..faeae92d4b74 100644 --- a/qcodes/__init__.py +++ b/qcodes/__init__.py @@ -61,7 +61,7 @@ from qcodes.instrument.parameter import ( Parameter, BufferedSweepableParameter, - BufferedReadableParameter, + BufferedReadableArrayParameter, ArrayParameter, MultiParameter, ParameterWithSetpoints, diff --git a/qcodes/instrument/parameter.py b/qcodes/instrument/parameter.py index a2f406662286..1f4e3989d043 100644 --- a/qcodes/instrument/parameter.py +++ b/qcodes/instrument/parameter.py @@ -1989,7 +1989,7 @@ def run_program(self, layer): return None -class BufferedReadableParameter(ArrayParameter): +class BufferedReadableArrayParameter(ArrayParameter): """ An ArrayParameter for buffered measurements. """ diff --git a/qcodes/instrument_drivers/QuTech/qupulse_instrument.py b/qcodes/instrument_drivers/QuTech/qupulse_instrument.py index d4773b9a7e68..aaa8a13a5281 100644 --- a/qcodes/instrument_drivers/QuTech/qupulse_instrument.py +++ b/qcodes/instrument_drivers/QuTech/qupulse_instrument.py @@ -12,7 +12,7 @@ import warnings from qcodes.instrument.base import Instrument -from qcodes.instrument.parameter import BufferedSweepableParameter, BufferedReadableParameter +from qcodes.instrument.parameter import BufferedSweepableParameter, BufferedReadableArrayParameter import qcodes.utils.validators as vals from qcodes.utils.helpers import full_class @@ -512,7 +512,7 @@ def snapshot_base(self, update: bool=False, return metadata -class QuPulseDACChannel(BufferedReadableParameter): +class QuPulseDACChannel(BufferedReadableArrayParameter): """ A parameter for an operation on a measurement mask. These parameters can be measured in a QCoDeS-loop. diff --git a/qcodes/loops.py b/qcodes/loops.py index ab0d4ae96c5b..e3dbb9497512 100644 --- a/qcodes/loops.py +++ b/qcodes/loops.py @@ -56,7 +56,7 @@ from qcodes.data.data_array import DataArray from qcodes.utils.helpers import wait_secs, full_class, tprint from qcodes.utils.metadata import Metadatable -from qcodes.instrument.parameter import _BaseParameter, BufferedSweepableParameter, BufferedReadableParameter +from qcodes.instrument.parameter import _BaseParameter, BufferedSweepableParameter, BufferedReadableArrayParameter from .actions import (_actions_snapshot, Task, Wait, _Measure, _Nest, BreakIf, _QcodesBreak) @@ -469,8 +469,8 @@ def each(self, *actions): actions[i] = action.each(*default) elif isinstance(action, _BaseLoop): raise AttributeError('It is not supported to nest a \'normal\' Loop in a BufferedLoop.') - elif isinstance(action, _BaseParameter) and not isinstance(action, BufferedReadableParameter): - raise AttributeError("The measuring parameters in a buffered loop must be BufferedReadableParameters.") + elif isinstance(action, _BaseParameter) and not isinstance(action, BufferedReadableArrayParameter): + raise AttributeError("The measuring parameters in a buffered loop must be BufferedReadableArrayParameters.") self.validate_actions(*actions) @@ -623,8 +623,8 @@ def each(self, *actions): actions[i] = action.each(*default) elif isinstance(action, _BaseLoop): raise AttributeError('It is not supported to nest a \'normal\' Loop in a BufferedLoop.') - elif isinstance(action, _BaseParameter) and not isinstance(action, BufferedReadableParameter): - raise AttributeError("The measuring parameters in a buffered loop must be BufferedReadableParameters.") + elif isinstance(action, _BaseParameter) and not isinstance(action, BufferedReadableArrayParameter): + raise AttributeError("The measuring parameters in a buffered loop must be BufferedReadableArrayParameters.") self.validate_actions(*actions) @@ -1355,7 +1355,7 @@ def _configure_measurement(self, measurement_windows): windows (measurement times). """ for action in self.actions: - if isinstance(action, BufferedReadableParameter): # Measurement + if isinstance(action, BufferedReadableArrayParameter): # Measurement action.configure_measurement(measurement_windows) if len(self.actions) == 1 and isinstance(self.actions[0], (BufferedActiveLoop, BufferedActiveRepetition)): @@ -1366,7 +1366,7 @@ def _arm_measurement(self): Arms the instruments hardware for the measurement """ for action in self.actions: - if isinstance(action, BufferedReadableParameter): # Measurement + if isinstance(action, BufferedReadableArrayParameter): # Measurement action.arm_measurement() if len(self.actions) == 1 and isinstance(self.actions[0], (BufferedActiveLoop, BufferedActiveRepetition)): @@ -1809,7 +1809,7 @@ def _configure_measurement(self, measurement_windows): windows (measurement times). """ for action in self.actions: - if isinstance(action, BufferedReadableParameter): # Measurement + if isinstance(action, BufferedReadableArrayParameter): # Measurement action.configure_measurement(measurement_windows) if len(self.actions) == 1 and isinstance(self.actions[0], (BufferedActiveLoop, BufferedActiveRepetition)): @@ -1820,7 +1820,7 @@ def _arm_measurement(self): Arms the instruments hardware for the measurement """ for action in self.actions: - if isinstance(action, BufferedReadableParameter): # Measurement + if isinstance(action, BufferedReadableArrayParameter): # Measurement action.arm_measurement() if len(self.actions) == 1 and isinstance(self.actions[0], (BufferedActiveLoop, BufferedActiveRepetition)): From 5707a58948042e012bc2e8de99a8a5dc6df568c7 Mon Sep 17 00:00:00 2001 From: "Lukas.Lankes" Date: Wed, 13 Mar 2019 10:35:33 +0100 Subject: [PATCH 30/35] Implementation of buffered ramping for the Decadac, so it can be used in a process of buffered acquisition. --- qcodes/instrument/parameter.py | 14 +- .../instrument_drivers/Harvard/FZJ_Decadac.py | 166 +++++++++++++++++- 2 files changed, 173 insertions(+), 7 deletions(-) diff --git a/qcodes/instrument/parameter.py b/qcodes/instrument/parameter.py index 1f4e3989d043..04cc3f017527 100644 --- a/qcodes/instrument/parameter.py +++ b/qcodes/instrument/parameter.py @@ -1959,7 +1959,7 @@ def set_buffered(self, sweep_values, layer): if self._sweep_parameter_cmd is not None: return self._sweep_parameter_cmd(self, sweep_values, layer) else: - return None + raise NotImplementedError('The callable "sweep_parameter_cmd" was not set for the parameter "{}".'.format(self.name)) def repeat_buffered(self, repetition_count, layer): """ @@ -1968,7 +1968,7 @@ def repeat_buffered(self, repetition_count, layer): if self._repeat_parameter_cmd is not None: return self._repeat_parameter_cmd(repetition_count, layer) else: - return None + raise NotImplementedError('The callable "repeat_parameter_cmd" was not set for the parameter "{}".'.format(self.name)) def send_buffer(self, layer): """ @@ -1977,7 +1977,7 @@ def send_buffer(self, layer): if self._send_buffer_cmd is not None: return self._send_buffer_cmd(layer) else: - return None + raise NotImplementedError('The callable "send_buffer_cmd" was not set for the parameter "{}".'.format(self.name)) def run_program(self, layer): """ @@ -1986,8 +1986,8 @@ def run_program(self, layer): if self._run_program_cmd is not None: return self._run_program_cmd(layer) else: - return None - + raise NotImplementedError('The callable "run_program_cmd" was not set for the parameter "{}".'.format(self.name)) + class BufferedReadableArrayParameter(ArrayParameter): """ @@ -2036,6 +2036,8 @@ def configure_measurement(self, measurement_windows) -> None: """ if self._config_meas_cmd is not None: self._config_meas_cmd(self, measurement_windows) + else: + raise NotImplementedError('The callable "config_meas_cmd" was not set for the parameter "{}".'.format(self.name)) def arm_measurement(self) -> None: """ @@ -2043,6 +2045,8 @@ def arm_measurement(self) -> None: """ if self._arm_meas_cmd is not None: self._arm_meas_cmd(self) + else: + raise NotImplementedError('The callable "arm_meas_cmd" was not set for the parameter "{}".'.format(self.name)) def expand_setpoints_helper(parameter: ParameterWithSetpoints) -> List[ diff --git a/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py b/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py index 2a440495af34..d6b729c9f326 100644 --- a/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py +++ b/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py @@ -7,10 +7,13 @@ # Date: July 2018 -from qcodes import InstrumentChannel, ChannelList +from typing import Dict +from qcodes import InstrumentChannel, ChannelList, BufferedSweepableParameter, SweepFixedValues from qcodes.utils import validators as vals from qcodes.instrument.visa import VisaInstrument +import numpy as np + import warnings class DACException(BaseException): @@ -191,7 +194,9 @@ def __init__(self, parent, name, channel, default_switch_pos=DacBase._DEFAULT_SW # Channel parameters # Voltage - self.add_parameter("volt", get_cmd=self._get_volt, get_parser=self._dac_code_to_v, set_cmd=self._set_volt, set_parser=self._dac_v_to_code, vals=self._volt_val, label="Voltage", unit="V") + self.add_parameter("volt", parameter_class=BufferedSweepableParameter, + sweep_parameter_cmd=self._sweep_parameter, send_buffer_cmd=self._send_buffer, run_program_cmd=self._run_program, + get_cmd=self._get_volt, get_parser=self._dac_code_to_v, set_cmd=self._set_volt, set_parser=self._dac_v_to_code, vals=self._volt_val, label="Voltage", unit="V") self.add_parameter("volt_raw", get_cmd=self._get_volt, set_cmd=self._set_volt, vals=self._volt_raw_val, label="Voltage (raw data)") # The limit commands are used to sweep dac voltages. They are not safety features. @@ -211,6 +216,37 @@ def __init__(self, parent, name, channel, default_switch_pos=DacBase._DEFAULT_SW self.add_function("ramp", call_cmd=self._ramp, args=(self._volt_val, self._ramp_val)) self.add_function("ramp_wait", call_cmd=self._ramp_wait, args=(self._volt_val, self._ramp_val)) + def _sweep_parameter(self, parameter, sweep_values, layer) -> None: + """ + Adds the sweep information to a list, to build up a single buffer later + + Args: + parameter: Parameter to sweep + sweep_values: Values the parameter should be swept over. + layer: nnumber of nested loop (most outer loop: 0) + """ + self._parent._sweep_parameter(self, parameter, sweep_values, layer) + + def _send_buffer(self, layer) -> Dict: + """ + Waits until this function is called for all swept parameters. Then the + buffer will be built and sent to the device. + + Args: + layer: nnumber of nested loop (most outer loop: 0) + + Returns: + Dictionary of the measurement windows if the function was called + the last parameter. If not it returns None. + """ + return self._parent._send_buffer(self, layer) + + def _run_program(self, layer): + """ + Runs the waveform program on the awg as soon as this function was + called for the last (buffered) loop + """ + return self._parent._run_program(self, layer) def reset(self): """ @@ -501,7 +537,38 @@ def __init__(self, parent, name, slot, default_switch_pos=DacBase._DEFAULT_SWITC self.add_submodule("channels", channels) self.add_parameter("mode", get_cmd=self._get_mode, set_cmd=self._set_mode, vals=self._MODE_VAL, label="Slot Mode") + + def _sweep_parameter(self, obj, parameter, sweep_values, layer) -> None: + """ + Adds the sweep information to a list, to build up a single buffer later + + Args: + parameter: Parameter to sweep + sweep_values: Values the parameter should be swept over. + layer: nnumber of nested loop (most outer loop: 0) + """ + self._parent._sweep_parameter(obj, parameter, sweep_values, layer) + + def _send_buffer(self, obj, layer) -> Dict: + """ + Waits until this function is called for all swept parameters. Then the + buffer will be built and sent to the device. + + Args: + layer: nnumber of nested loop (most outer loop: 0) + + Returns: + Dictionary of the measurement windows if the function was called + the last parameter. If not it returns None. + """ + return self._parent._send_buffer(obj, layer) + def _run_program(self, obj, layer): + """ + Runs the waveform program on the awg as soon as this function was + called for the last (buffered) loop + """ + return self._parent._run_program(obj, layer) def reset(self): """ @@ -550,6 +617,7 @@ def __init__(self, name, address, timeout=_DEFAULT_TIMEOUT, default_switch_pos=DacBase._DEFAULT_SWITCH_POS, terminator='\n', + run_buffered_cmd=None, **kwargs): """ Initialize the device @@ -587,6 +655,9 @@ def __init__(self, name, address, self.add_submodule("slots", slots) self.add_submodule("channels", channels) + self.run_buffered_cmd = run_buffered_cmd + self._buffered_loop = None + if reset: self.reset() @@ -607,6 +678,8 @@ def reset(self): """ Reset all parameters to default """ + self.reset_programs() + cmd = "" for slot in self.slots: @@ -620,7 +693,96 @@ def reset(self): cmd += DacBase._COMMAND_SET_SLOT.format(0) + DacBase._COMMAND_SET_CHANNEL.format(0) # Select first slot and channel self._write(self, cmd) + + def reset_programs(self): + """ + Resets all buffered loop actions + """ + self._buffered_loop = None + + def _sweep_parameter(self, obj, parameter, sweep_values, layer) -> None: + """ + Adds the sweep information to a list, to build up a single buffer later + + Args: + parameter: Parameter to sweep + sweep_values: Values the parameter should be swept over. + layer: nnumber of nested loop (most outer loop: 0) + """ + if self._buffered_loop is not None: + raise NotImplementedError('It is not supported by the Decadac to nest multiple buffered loops.') + + ramp_start = sweep_values[0] + ramp_stop = sweep_values[-1] + ramp_num = len(sweep_values) + + if list(SweepFixedValues(obj, start=ramp_start, stop=ramp_stop, num=ramp_num)) != sweep_values or ramp_start == ramp_stop: + raise NotImplementedError('It is not supported by the Decadac to sweep parameters in a non-linear order.') + + self._buffered_loop = { + 'parameter' : parameter, + 'ramp_start' : ramp_start, + 'ramp_stop' : ramp_stop, + 'ramp_num' : ramp_num + } + + def _send_buffer(self, obj, layer) -> Dict: + """ + Waits until this function is called for all swept parameters. Then the + buffer will be built and sent to the device. + + Args: + obj: Channel -> Caller of the function + layer: Number of nested loop (most outer loop: 0) + + Returns: + Dictionary of the measurement windows if the function was called + the last parameter. If not it returns None. + """ + if self._buffered_loop is not None: + param = self._buffered_loop['parameter'] + + v_start = self._buffered_loop['ramp_start'] + v_stop = self._buffered_loop['ramp_stop'] + num = self._buffered_loop['ramp_num'] + + # Convert voltages to dac-codes + c_start = self._v_to_code(v_start) + c_stop = self._v_to_code(v_stop) + + # Calculate the slope + slope = int((c_stop - c_start) / (num - 1) * 65536) + + meas_length = param.update_period.get () * 1000. # Microseconds to nanoseconds + + window_begins = np.linspace(0, meas_length * (num-1), num=num) + window_lengths = np.array([meas_length] * num) + + measurement_windows = { 'M' : (window_begins, window_lengths) } + + param.volt.set(v_start) + + # Now let's set up our limits and ramp slope + if v_start < v_stop: + param.upper_ramp_limit.set(v_stop) + else: + param.lower_ramp_limit.set(v_stop) + + param.slope.set(slope) + + return measurement_windows + + return None + def _run_program(self, obj, layer): + """ + Runs the waveform program on the awg as soon as this function was + called for the last (buffered) loop + """ + if self.run_buffered_cmd is not None: + self.run_buffered_cmd() + + self.reset_programs() def _set_slot(self, slot: DacSlot): """ From ce641f6776912dbaede9410108b4a94044d9ecac Mon Sep 17 00:00:00 2001 From: "Lukas.Lankes" Date: Thu, 14 Mar 2019 08:34:28 +0100 Subject: [PATCH 31/35] Bug fix in BufferedActiveLoop (loops.py). Moved qupulse_instrument to the folder qupulse --- .../{QuTech => qupulse}/qupulse_instrument.py | 0 qcodes/loops.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename qcodes/instrument_drivers/{QuTech => qupulse}/qupulse_instrument.py (100%) diff --git a/qcodes/instrument_drivers/QuTech/qupulse_instrument.py b/qcodes/instrument_drivers/qupulse/qupulse_instrument.py similarity index 100% rename from qcodes/instrument_drivers/QuTech/qupulse_instrument.py rename to qcodes/instrument_drivers/qupulse/qupulse_instrument.py diff --git a/qcodes/loops.py b/qcodes/loops.py index e3dbb9497512..6f32122d4304 100644 --- a/qcodes/loops.py +++ b/qcodes/loops.py @@ -1439,7 +1439,7 @@ def _run_loop(self, first_delay=0, action_indices=(), time.asctime(time.localtime(t0 + ((time.time() - t0) * imax / i)))), dt=self.progress_interval, tag="finish") - set_val = self.sweep_values.set(value) + set_val = value # TODO self.sweep_values.set(value) new_indices = loop_indices + (i,) new_values = current_values + (value,) From 28232a2559ae346190d1e79af120dc83cd1fb951 Mon Sep 17 00:00:00 2001 From: "Lukas.Lankes" Date: Thu, 14 Mar 2019 09:40:53 +0100 Subject: [PATCH 32/35] Changes in the buffered acquisition. Bug fixes in the Decadac class. --- .../instrument_drivers/Harvard/FZJ_Decadac.py | 51 +++++++++++-------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py b/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py index d6b729c9f326..83874810f390 100644 --- a/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py +++ b/qcodes/instrument_drivers/Harvard/FZJ_Decadac.py @@ -95,11 +95,11 @@ def _dac_v_to_code(volt, min_volt, max_volt): Returns: (int): dac-code """ - if volt < min_volt or volt >= max_volt: + if volt < min_volt or volt > max_volt: raise ValueError("Cannot convert voltage {} V to a voltage code, value out of range ({} V - {} V).".format(volt, min_volt, max_volt)) frac = (volt - min_volt) / (max_volt - min_volt) - val = int(round(frac * 65536)) + val = int(round(frac * 65535)) # extra check to be absolutely sure that the instrument does nothing # receive an out-of-bounds value @@ -123,7 +123,7 @@ def _dac_code_to_v(code, min_volt, max_volt): Returns: (float): voltage in V """ - frac = code / 65536.0 + frac = code / 65535.0 return (frac * (max_volt - min_volt)) + min_volt @@ -532,7 +532,7 @@ def __init__(self, parent, name, slot, default_switch_pos=DacBase._DEFAULT_SWITC channels = ChannelList(self, "Slot_Chans", DacChannel) for channel in range(4): - channels.append(DacChannel(self, "Chan{}".format(slot, channel), channel, default_switch_pos=default_switch_pos)) + channels.append(DacChannel(self, "Chan{}".format(channel), channel, default_switch_pos=default_switch_pos)) self.add_submodule("channels", channels) @@ -716,11 +716,11 @@ def _sweep_parameter(self, obj, parameter, sweep_values, layer) -> None: ramp_stop = sweep_values[-1] ramp_num = len(sweep_values) - if list(SweepFixedValues(obj, start=ramp_start, stop=ramp_stop, num=ramp_num)) != sweep_values or ramp_start == ramp_stop: + if list(SweepFixedValues(obj, start=ramp_start, stop=ramp_stop, num=ramp_num)) != list(sweep_values) or ramp_start == ramp_stop: raise NotImplementedError('It is not supported by the Decadac to sweep parameters in a non-linear order.') self._buffered_loop = { - 'parameter' : parameter, + 'parameter' : parameter.name, 'ramp_start' : ramp_start, 'ramp_stop' : ramp_stop, 'ramp_num' : ramp_num @@ -740,35 +740,44 @@ def _send_buffer(self, obj, layer) -> Dict: the last parameter. If not it returns None. """ if self._buffered_loop is not None: - param = self._buffered_loop['parameter'] + param_name = self._buffered_loop['parameter'] - v_start = self._buffered_loop['ramp_start'] - v_stop = self._buffered_loop['ramp_stop'] - num = self._buffered_loop['ramp_num'] - - # Convert voltages to dac-codes - c_start = self._v_to_code(v_start) - c_stop = self._v_to_code(v_stop) + if param_name == obj.volt.name: + v_start = self._buffered_loop['ramp_start'] + v_stop = self._buffered_loop['ramp_stop'] + num = self._buffered_loop['ramp_num'] + + # Convert voltages to dac-codes + c_start = obj._dac_v_to_code(v_start) + c_stop = obj._dac_v_to_code(v_stop) + elif param_name == obj.volt_raw.name: + c_start = self._buffered_loop['ramp_start'] + c_stop = self._buffered_loop['ramp_stop'] + num = self._buffered_loop['ramp_num'] + else: + raise NotImplementedError('Only the volt-parameters can be used in a buffered loop with the Dacadac.') # Calculate the slope slope = int((c_stop - c_start) / (num - 1) * 65536) - meas_length = param.update_period.get () * 1000. # Microseconds to nanoseconds + meas_length = obj.update_period.get () * 1000. # Microseconds to nanoseconds window_begins = np.linspace(0, meas_length * (num-1), num=num) window_lengths = np.array([meas_length] * num) measurement_windows = { 'M' : (window_begins, window_lengths) } - param.volt.set(v_start) - # Now let's set up our limits and ramp slope - if v_start < v_stop: - param.upper_ramp_limit.set(v_stop) + if c_start < c_stop: + obj.lower_ramp_limit_raw.set(c_start) + obj.upper_ramp_limit_raw.set(c_stop) else: - param.lower_ramp_limit.set(v_stop) + obj.upper_ramp_limit_raw.set(c_start) + obj.lower_ramp_limit_raw.set(c_stop) + + obj.volt_raw.set(c_start) - param.slope.set(slope) + obj.slope.set(slope) return measurement_windows From 93e2b4c4de40bb91356c4b4785940ad08d5507c3 Mon Sep 17 00:00:00 2001 From: Rene Otten Date: Tue, 21 May 2019 12:44:22 +0200 Subject: [PATCH 33/35] Fix baud rate --- qcodes/instrument_drivers/QDev/QDac.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/instrument_drivers/QDev/QDac.py b/qcodes/instrument_drivers/QDev/QDac.py index 303fcfa52fa3..d2bc328e3882 100644 --- a/qcodes/instrument_drivers/QDev/QDac.py +++ b/qcodes/instrument_drivers/QDev/QDac.py @@ -50,7 +50,7 @@ def __init__(self, name, address, num_chans=48, update_currents=True): # This is the baud rate on power-up. It can be changed later but # you must start out with this value. - handle.baud_rate = 480600 + handle.baud_rate = 460800 handle.parity = visa.constants.Parity(0) handle.data_bits = 8 self.set_terminator('\n') From 66adae6714317d106ce3221d14fc43c5cb3b4119 Mon Sep 17 00:00:00 2001 From: Rene Otten Date: Tue, 21 May 2019 12:45:34 +0200 Subject: [PATCH 34/35] Fix hard coded channel numbers --- qcodes/instrument_drivers/QDev/QDac.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/qcodes/instrument_drivers/QDev/QDac.py b/qcodes/instrument_drivers/QDev/QDac.py index d2bc328e3882..420880ac2c3a 100644 --- a/qcodes/instrument_drivers/QDev/QDac.py +++ b/qcodes/instrument_drivers/QDev/QDac.py @@ -61,6 +61,7 @@ def __init__(self, name, address, num_chans=48, update_currents=True): # The following bool is used in self.write self.debugmode = False + self.num_chans = num_chans if self._get_firmware_version() < 0.170202: raise RuntimeError(''' @@ -69,7 +70,7 @@ def __init__(self, name, address, num_chans=48, update_currents=True): Contact rikke.lutge@nbi.ku.dk for an update. ''') self._update_currents = update_currents - self.num_chans = num_chans + # Assigned slopes. Entries will eventually be [chan, slope] self._slopes = [] @@ -582,14 +583,14 @@ def connect_message(self): self.visa_handle.read())) # take care of the rest of the output - for ii in range(50): + for ii in range(self.num_chans+2): self.visa_handle.read() def _get_firmware_version(self): self.write('status') FW_str = self._write_response FW_version = float(FW_str.replace('Software Version: ', '')) - for ii in range(50): + for ii in range(self.num_chans+2): self.read() return FW_version From 4af5e9a4e20c2036c295aa578c894f22fc5a545f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 18:16:19 +0000 Subject: [PATCH 35/35] Bump github/codeql-action from 3.27.0 to 3.28.5 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.27.0 to 3.28.5. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/662472033e021d55d94146f66f6058822b0b39fd...f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/scorecards.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 02a8193fbaea..189006c2a542 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,12 +39,12 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 + uses: github/codeql-action/init@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3.28.5 with: languages: ${{ matrix.language }} - name: Autobuild - uses: github/codeql-action/autobuild@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 + uses: github/codeql-action/autobuild@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3.28.5 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 + uses: github/codeql-action/analyze@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3.28.5 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index dda1c85289ff..79e25fba24ec 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -71,6 +71,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 + uses: github/codeql-action/upload-sarif@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3.28.5 with: sarif_file: results.sarif