From 704541188eb7e4a2871d2c87d1b17678dfed6954 Mon Sep 17 00:00:00 2001 From: Jens Hedegaard Nielsen Date: Mon, 12 Jun 2017 14:07:04 +0200 Subject: [PATCH 01/19] fix: refactor Instrument to avoid removing methods from channels --- qcodes/instrument/base.py | 417 ++++++++++++++++++++--------------- qcodes/instrument/channel.py | 33 +-- 2 files changed, 238 insertions(+), 212 deletions(-) diff --git a/qcodes/instrument/base.py b/qcodes/instrument/base.py index 24f1efcaa52..f65bb2cddcf 100644 --- a/qcodes/instrument/base.py +++ b/qcodes/instrument/base.py @@ -12,7 +12,241 @@ from .function import Function -class Instrument(Metadatable, DelegateAttributes): +class InstrumentBase(Metadatable, DelegateAttributes): + """ + Base class for all QCodes instruments and instrument channels + + Args: + name (str): an identifier for this instrument, particularly for + attaching it to a Station. + + metadata (Optional[Dict]): additional static metadata to add to this + instrument's JSON snapshot. + + + Attributes: + name (str): an identifier for this instrument, particularly for + attaching it to a Station. + + parameters (Dict[Parameter]): All the parameters supported by this + instrument. Usually populated via ``add_parameter`` + + functions (Dict[Function]): All the functions supported by this + instrument. Usually populated via ``add_function`` + """ + def __init__(self, name, **kwargs): + self.name = str(name) + self.parameters = {} + self.functions = {} + self.submodules = {} + super().__init__(**kwargs) + + def add_parameter(self, name, parameter_class=StandardParameter, + **kwargs): + """ + Bind one Parameter to this instrument. + + Instrument subclasses can call this repeatedly in their ``__init__`` + for every real parameter of the instrument. + + In this sense, parameters are the state variables of the instrument, + anything the user can set and/or get + + Args: + name (str): How the parameter will be stored within + ``instrument.parameters`` and also how you address it using the + shortcut methods: ``instrument.set(param_name, value)`` etc. + + parameter_class (Optional[type]): You can construct the parameter + out of any class. Default ``StandardParameter``. + + **kwargs: constructor arguments for ``parameter_class``. + + Raises: + KeyError: if this instrument already has a parameter with this + name. + """ + if name in self.parameters: + raise KeyError('Duplicate parameter name {}'.format(name)) + param = parameter_class(name=name, instrument=self, **kwargs) + self.parameters[name] = param + + def add_function(self, name, **kwargs): + """ + Bind one Function to this instrument. + + Instrument subclasses can call this repeatedly in their ``__init__`` + for every real function of the instrument. + + This functionality is meant for simple cases, principally things that + map to simple commands like '\*RST' (reset) or those with just a few + arguments. It requires a fixed argument count, and positional args + only. If your case is more complicated, you're probably better off + simply making a new method in your ``Instrument`` subclass definition. + + Args: + name (str): how the Function will be stored within + ``instrument.Functions`` and also how you address it using the + shortcut methods: ``instrument.call(func_name, *args)`` etc. + + **kwargs: constructor kwargs for ``Function`` + + Raises: + KeyError: if this instrument already has a function with this + name. + """ + if name in self.functions: + raise KeyError('Duplicate function name {}'.format(name)) + func = Function(name=name, instrument=self, **kwargs) + self.functions[name] = func + + def snapshot_base(self, update=False): + """ + State of the instrument as a JSON-compatible dict. + + Args: + update (bool): If True, update the state by querying the + instrument. If False, just use the latest values in memory. + + Returns: + dict: base snapshot + """ + snap = {'parameters': dict((name, param.snapshot(update=update)) + for name, param in self.parameters.items()), + 'functions': dict((name, func.snapshot(update=update)) + for name, func in self.functions.items()), + '__class__': full_class(self), + } + for attr in set(self._meta_attrs): + if hasattr(self, attr): + snap[attr] = getattr(self, attr) + return snap + + def print_readable_snapshot(self, update=False, max_chars=80): + """ + Prints a readable version of the snapshot. + The readable snapshot includes the name, value and unit of each + parameter. + A convenience function to quickly get an overview of the status of an instrument. + + Args: + update (bool) : If True, update the state by querying the + instrument. If False, just use the latest values in memory. + This argument gets passed to the snapshot function. + max_chars (int) : the maximum number of characters per line. The + readable snapshot will be cropped if this value is exceeded. + Defaults to 80 to be consistent with default terminal width. + """ + floating_types = (float, np.integer, np.floating) + snapshot = self.snapshot(update=update) + + par_lengths = [len(p) for p in snapshot['parameters']] + + # Min of 50 is to prevent a super long parameter name to break this + # function + par_field_len = min(max(par_lengths)+1, 50) + + print(self.name + ':') + print('{0:<{1}}'.format('\tparameter ', par_field_len) + 'value') + print('-'*80) + for par in sorted(snapshot['parameters']): + name = snapshot['parameters'][par]['name'] + msg = '{0:<{1}}:'.format(name, par_field_len) + val = snapshot['parameters'][par]['value'] + unit = snapshot['parameters'][par].get('unit', None) + if unit is None: + # this may be a multi parameter + unit = snapshot['parameters'][par].get('units', None) + if isinstance(val, floating_types): + msg += '\t{:.5g} '.format(val) + else: + msg += '\t{} '.format(val) + if unit is not '': # corresponds to no unit + msg += '({})'.format(unit) + # Truncate the message if it is longer than max length + if len(msg) > max_chars and not max_chars == -1: + msg = msg[0:max_chars-3] + '...' + print(msg) + + # + # shortcuts to parameters & setters & getters # + # + # instrument['someparam'] === instrument.parameters['someparam'] # + # instrument.someparam === instrument.parameters['someparam'] # + # instrument.get('someparam') === instrument['someparam'].get() # + # etc... # + # + delegate_attr_dicts = ['parameters', 'functions'] + + def __getitem__(self, key): + """Delegate instrument['name'] to parameter or function 'name'.""" + try: + return self.parameters[key] + except KeyError: + return self.functions[key] + + def set(self, param_name, value): + """ + Shortcut for setting a parameter from its name and new value. + + Args: + param_name (str): The name of a parameter of this instrument. + value (any): The new value to set. + """ + self.parameters[param_name].set(value) + + def get(self, param_name): + """ + Shortcut for getting a parameter from its name. + + Args: + param_name (str): The name of a parameter of this instrument. + + Returns: + any: The current value of the parameter. + """ + return self.parameters[param_name].get() + + def call(self, func_name, *args): + """ + Shortcut for calling a function from its name. + + Args: + func_name (str): The name of a function of this instrument. + *args: any arguments to the function. + + Returns: + any: The return value of the function. + """ + return self.functions[func_name].call(*args) + + def __getstate__(self): + """Prevent pickling instruments, and give a nice error message.""" + raise RuntimeError( + 'Pickling %s. qcodes Instruments should not be pickled. Likely this means you ' + 'were trying to use a local instrument (defined with ' + 'server_name=None) in a background Loop. Local instruments can ' + 'only be used in Loops with background=False.' % self.name) + + def validate_status(self, verbose=False): + """ Validate the values of all gettable parameters + + The validation is done for all parameters that have both a get and + set method. + + Arguments: + verbose (bool): If True, then information about the parameters that are being check is printed. + + """ + for k, p in self.parameters.items(): + if p.has_get and p.has_set: + value = p.get() + if verbose: + print('validate_status: param %s: %s' % (k, value)) + p.validate(value) + + +class Instrument(InstrumentBase): """ Base class for all QCodes instruments. @@ -49,7 +283,7 @@ def __init__(self, name, **kwargs): if kwargs.pop('server_name', False): warnings.warn("server_name argument not supported any more", stacklevel=0) - super().__init__(**kwargs) + super().__init__(name, **kwargs) self.parameters = {} self.functions = {} self.submodules = {} @@ -252,64 +486,6 @@ def find_instrument(cls, name, instrument_class=None): return ins - def add_parameter(self, name, parameter_class=StandardParameter, - **kwargs): - """ - Bind one Parameter to this instrument. - - Instrument subclasses can call this repeatedly in their ``__init__`` - for every real parameter of the instrument. - - In this sense, parameters are the state variables of the instrument, - anything the user can set and/or get - - Args: - name (str): How the parameter will be stored within - ``instrument.parameters`` and also how you address it using the - shortcut methods: ``instrument.set(param_name, value)`` etc. - - parameter_class (Optional[type]): You can construct the parameter - out of any class. Default ``StandardParameter``. - - **kwargs: constructor arguments for ``parameter_class``. - - Raises: - KeyError: if this instrument already has a parameter with this - name. - """ - if name in self.parameters: - raise KeyError('Duplicate parameter name {}'.format(name)) - param = parameter_class(name=name, instrument=self, **kwargs) - self.parameters[name] = param - - def add_function(self, name, **kwargs): - """ - Bind one Function to this instrument. - - Instrument subclasses can call this repeatedly in their ``__init__`` - for every real function of the instrument. - - This functionality is meant for simple cases, principally things that - map to simple commands like '\*RST' (reset) or those with just a few - arguments. It requires a fixed argument count, and positional args - only. If your case is more complicated, you're probably better off - simply making a new method in your ``Instrument`` subclass definition. - - Args: - name (str): how the Function will be stored within - ``instrument.Functions`` and also how you address it using the - shortcut methods: ``instrument.call(func_name, *args)`` etc. - - **kwargs: constructor kwargs for ``Function`` - - Raises: - KeyError: if this instrument already has a function with this - name. - """ - if name in self.functions: - raise KeyError('Duplicate function name {}'.format(name)) - func = Function(name=name, instrument=self, **kwargs) - self.functions[name] = func def add_submodule(self, name, submodule): """ @@ -365,52 +541,6 @@ def snapshot_base(self, update=False): snap[attr] = getattr(self, attr) return snap - def print_readable_snapshot(self, update=False, max_chars=80): - """ - Prints a readable version of the snapshot. - The readable snapshot includes the name, value and unit of each - parameter. - A convenience function to quickly get an overview of the status of an instrument. - - Args: - update (bool) : If True, update the state by querying the - instrument. If False, just use the latest values in memory. - This argument gets passed to the snapshot function. - max_chars (int) : the maximum number of characters per line. The - readable snapshot will be cropped if this value is exceeded. - Defaults to 80 to be consistent with default terminal width. - """ - floating_types = (float, np.integer, np.floating) - snapshot = self.snapshot(update=update) - - par_lengths = [len(p) for p in snapshot['parameters']] - - # Min of 50 is to prevent a super long parameter name to break this - # function - par_field_len = min(max(par_lengths)+1, 50) - - print(self.name + ':') - print('{0:<{1}}'.format('\tparameter ', par_field_len) + 'value') - print('-'*80) - for par in sorted(snapshot['parameters']): - name = snapshot['parameters'][par]['name'] - msg = '{0:<{1}}:'.format(name, par_field_len) - val = snapshot['parameters'][par]['value'] - unit = snapshot['parameters'][par].get('unit', None) - if unit is None: - # this may be a multi parameter - unit = snapshot['parameters'][par].get('units', None) - if isinstance(val, floating_types): - msg += '\t{:.5g} '.format(val) - else: - msg += '\t{} '.format(val) - if unit is not '': # corresponds to no unit - msg += '({})'.format(unit) - # Truncate the message if it is longer than max length - if len(msg) > max_chars and not max_chars == -1: - msg = msg[0:max_chars-3] + '...' - print(msg) - # `write_raw` and `ask_raw` are the interface to hardware # # `write` and `ask` are standard wrappers to help with error reporting # # @@ -490,80 +620,5 @@ def ask_raw(self, cmd): 'Instrument {} has not defined an ask method'.format( type(self).__name__)) - # - # shortcuts to parameters & setters & getters # - # - # instrument['someparam'] === instrument.parameters['someparam'] # - # instrument.someparam === instrument.parameters['someparam'] # - # instrument.get('someparam') === instrument['someparam'].get() # - # etc... # - # - delegate_attr_dicts = ['parameters', 'functions', 'submodules'] - def __getitem__(self, key): - """Delegate instrument['name'] to parameter or function 'name'.""" - try: - return self.parameters[key] - except KeyError: - return self.functions[key] - - def set(self, param_name, value): - """ - Shortcut for setting a parameter from its name and new value. - - Args: - param_name (str): The name of a parameter of this instrument. - value (any): The new value to set. - """ - self.parameters[param_name].set(value) - - def get(self, param_name): - """ - Shortcut for getting a parameter from its name. - - Args: - param_name (str): The name of a parameter of this instrument. - - Returns: - any: The current value of the parameter. - """ - return self.parameters[param_name].get() - - def call(self, func_name, *args): - """ - Shortcut for calling a function from its name. - - Args: - func_name (str): The name of a function of this instrument. - *args: any arguments to the function. - - Returns: - any: The return value of the function. - """ - return self.functions[func_name].call(*args) - - def __getstate__(self): - """Prevent pickling instruments, and give a nice error message.""" - raise RuntimeError( - 'Pickling %s. qcodes Instruments should not be pickled. Likely this means you ' - 'were trying to use a local instrument (defined with ' - 'server_name=None) in a background Loop. Local instruments can ' - 'only be used in Loops with background=False.' % self.name) - - def validate_status(self, verbose=False): - """ Validate the values of all gettable parameters - - The validation is done for all parameters that have both a get and - set method. - - Arguments: - verbose (bool): If True, then information about the parameters that are being check is printed. - - """ - for k, p in self.parameters.items(): - if p.has_get and p.has_set: - value = p.get() - if verbose: - print('validate_status: param %s: %s' % (k, value)) - p.validate(value) diff --git a/qcodes/instrument/channel.py b/qcodes/instrument/channel.py index f5c44110124..dab5eceb398 100644 --- a/qcodes/instrument/channel.py +++ b/qcodes/instrument/channel.py @@ -1,13 +1,13 @@ """ Base class for the channel of an instrument """ from typing import List, Tuple, Union -from .base import Instrument +from .base import InstrumentBase, Instrument from .parameter import MultiParameter, ArrayParameter from ..utils.metadata import Metadatable from ..utils.helpers import full_class -class InstrumentChannel(Instrument): +class InstrumentChannel(InstrumentBase): """ Base class for a channel in an instrument @@ -46,35 +46,6 @@ def __repr__(self): type(self._parent).__name__, self._parent.name) - # We aren't a member of the global list of instruments, don't try and remove ourself - def __del__(self): - """ Does nothing for an instrument channel """ - pass - - def close(self): - """ Doesn't make sense to just close a channel by default, raise NotImplemented """ - raise NotImplementedError("Can't close a channel. Close my parent instead.") - - @classmethod - def record_instance(cls, instance): - """ Instances should not be recorded for channels. This should happen for the parent instrument. """ - pass - - @classmethod - def instances(cls): - """ Instances should not be recorded for channels. This should happen for the parent instrument. """ - pass - - @classmethod - def remove_instances(cls, instance): - """ It doesn't make sense to remove a channel from an instrument, raise NotImplemented""" - raise NotImplementedError("Can't remove a channel.") - - # This method doesn't make sense for a channel, raise NotImplemented - @classmethod - def find_instruments(cls, name, instrument_class=None): - raise NotImplementedError("Can't find instruments in a channel") - # Pass any commands to read or write from the instrument up to the parent def write(self, cmd): return self._parent.write(cmd) From 4b93576adf435e29f18eeadd2d622aaa221f74e3 Mon Sep 17 00:00:00 2001 From: Jens Hedegaard Nielsen Date: Mon, 12 Jun 2017 14:51:44 +0200 Subject: [PATCH 02/19] Include channels in print_readable_snaphoot --- qcodes/instrument/base.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/qcodes/instrument/base.py b/qcodes/instrument/base.py index f65bb2cddcf..a168f7f91e7 100644 --- a/qcodes/instrument/base.py +++ b/qcodes/instrument/base.py @@ -168,6 +168,14 @@ def print_readable_snapshot(self, update=False, max_chars=80): msg = msg[0:max_chars-3] + '...' print(msg) + for submodule in self.submodules.values(): + if hasattr(submodule, '_channels'): + if submodule._snapshotable: + for channel in submodule._channels: + channel.print_readable_snapshot() + else: + submodule.print_readable_snapshot(update, max_chars) + # # shortcuts to parameters & setters & getters # # From 31cc34c4b61f6a0b0f604914d6fc3c7ebd2284bb Mon Sep 17 00:00:00 2001 From: Jens Hedegaard Nielsen Date: Mon, 12 Jun 2017 16:40:00 +0200 Subject: [PATCH 03/19] Fix: make locked channels a named tuple That way you can access the names on the channellist via names as soon as they are locked --- qcodes/instrument/channel.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/qcodes/instrument/channel.py b/qcodes/instrument/channel.py index dab5eceb398..805dae608d7 100644 --- a/qcodes/instrument/channel.py +++ b/qcodes/instrument/channel.py @@ -1,8 +1,10 @@ """ Base class for the channel of an instrument """ from typing import List, Tuple, Union +from collections import namedtuple from .base import InstrumentBase, Instrument from .parameter import MultiParameter, ArrayParameter +from qcodes.utils.helpers import DelegateAttributes from ..utils.metadata import Metadatable from ..utils.helpers import full_class @@ -35,6 +37,7 @@ def __init__(self, parent, name, **kwargs): self.functions = {} self.name = "{}_{}".format(parent.name, str(name)) + self.short_name = str(name) self._meta_attrs = ['name'] self._parent = parent @@ -276,7 +279,9 @@ def lock(self): """ if self._locked: return - self._channels = tuple(self._channels) + + channeltuple = namedtuple('channels', [channel.short_name for channel in self._channels]) + self._channels = channeltuple(*self._channels) self._locked = True def snapshot_base(self, update=False): @@ -310,7 +315,7 @@ def __getattr__(self, name): Params: name(str): The name of the parameter or function that we want to operate on. """ - # Check if this is a valid parameter + # Check if this is a valid parametervx if name in self._channels[0].parameters: setpoints = None setpoint_names = None @@ -362,4 +367,18 @@ def multi_func(*args, **kwargs): chan.functions[name](*args, **kwargs) return multi_func + try: + return getattr(self._channels, name) + except: + pass + raise AttributeError('\'{}\' object has no attribute \'{}\''.format(self.__class__.__name__, name)) + + def __dir__(self): + names = super().__dir__() + if self._channels: + names += list(self._channels[0].parameters.keys()) + names += list(self._channels[0].functions.keys()) + if self._locked: + names += [channel.short_name for channel in self._channels] + return sorted(set(names)) From 8734ab4d5741d3031ff1e448bb23c0ea9f0b1ccf Mon Sep 17 00:00:00 2001 From: Jens Hedegaard Nielsen Date: Tue, 13 Jun 2017 11:16:12 +0200 Subject: [PATCH 04/19] Fix: channels add support for indexing from 1 many instruments have hardware channels that are indexed and start at 1 --- qcodes/instrument/channel.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/qcodes/instrument/channel.py b/qcodes/instrument/channel.py index 805dae608d7..e34f2961097 100644 --- a/qcodes/instrument/channel.py +++ b/qcodes/instrument/channel.py @@ -143,7 +143,8 @@ class ChannelList(Metadatable): def __init__(self, parent, name, chan_type: type, chan_list=None, snapshotable=True, - multichan_paramclass: type=MultiChannelInstrumentParameter): + multichan_paramclass: type = MultiChannelInstrumentParameter, + oneindexed=False): super().__init__() self._parent = parent @@ -160,6 +161,7 @@ def __init__(self, parent, name, chan_type: type, chan_list=None, self._chan_type = chan_type self._snapshotable = snapshotable self._paramclass = multichan_paramclass + self._oneindexed = oneindexed # If a list of channels is not provided, define a list to store channels. # This will eventually become a locked tuple. @@ -180,9 +182,18 @@ def __getitem__(self, i): i (int/slice): Either a single channel index or a slice of channels to get """ if isinstance(i, slice): + if self._oneindexed: + if i.start < 1 or i.stop < 1: + raise IndexError("1 indexed channel lists only support positive indexes") + i = slice(i.start-1, i.stop-1, i.step) return ChannelList(self._parent, self._name, self._chan_type, self._channels[i], - multichan_paramclass=self._paramclass) + multichan_paramclass=self._paramclass, + oneindexed=self._oneindexed) + elif self._oneindexed: + if i<1: + raise IndexError("1 indexed channel lists only support positive indexes") + i += -1 return self._channels[i] def __iter__(self): @@ -213,7 +224,8 @@ def __add__(self, other): if self._parent != other._parent: raise ValueError("Can only add channels from the same parent together.") - return ChannelList(self._parent, self._name, self._chan_type, self._channels + other._channels) + return ChannelList(self._parent, self._name, self._chan_type, self._channels + other._channels, + oneindexed=self._oneindexed) def append(self, obj): """ @@ -253,7 +265,7 @@ def index(self, obj): Args: obj(chan_type): The object to find in the channel list. """ - return self._channels.index(obj) + return self._channels.index(obj)+ self._oneindexed def insert(self, index, obj): """ @@ -270,6 +282,11 @@ def insert(self, index, obj): raise TypeError("All items in a channel list must be of the same type." " Adding {} to a list of {}.".format(type(obj).__name__, self._chan_type.__name__)) + if self._oneindexed: + if index < 1: + raise IndexError("1 based channel lists only support positive indexes") + index += 1 + return self._channels.insert(index, obj) def lock(self): From 75b3fa6c7681b04801579655542e38b292fc0494 Mon Sep 17 00:00:00 2001 From: Jens Hedegaard Nielsen Date: Wed, 14 Jun 2017 15:12:45 +0200 Subject: [PATCH 05/19] Fix linting errors in channel --- qcodes/instrument/channel.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/qcodes/instrument/channel.py b/qcodes/instrument/channel.py index e34f2961097..a92c095a003 100644 --- a/qcodes/instrument/channel.py +++ b/qcodes/instrument/channel.py @@ -4,7 +4,6 @@ from .base import InstrumentBase, Instrument from .parameter import MultiParameter, ArrayParameter -from qcodes.utils.helpers import DelegateAttributes from ..utils.metadata import Metadatable from ..utils.helpers import full_class @@ -141,7 +140,7 @@ class ChannelList(Metadatable): """ - def __init__(self, parent, name, chan_type: type, chan_list=None, + def __init__(self, parent: Instrument, name, chan_type: type, chan_list=None, snapshotable=True, multichan_paramclass: type = MultiChannelInstrumentParameter, oneindexed=False): @@ -191,7 +190,7 @@ def __getitem__(self, i): multichan_paramclass=self._paramclass, oneindexed=self._oneindexed) elif self._oneindexed: - if i<1: + if i < 1: raise IndexError("1 indexed channel lists only support positive indexes") i += -1 return self._channels[i] @@ -265,7 +264,7 @@ def index(self, obj): Args: obj(chan_type): The object to find in the channel list. """ - return self._channels.index(obj)+ self._oneindexed + return self._channels.index(obj) + self._oneindexed def insert(self, index, obj): """ @@ -359,7 +358,7 @@ def __getattr__(self, name): if self._channels[0].parameters[name].setpoint_units: setpoint_units = tuple(chan.parameters[name].setpoint_units for chan in self._channels) else: - shapes = tuple(() for chan in self._channels) + shapes = tuple(() for _ in self._channels) param = self._paramclass(self._channels, param_name=name, @@ -386,7 +385,7 @@ def multi_func(*args, **kwargs): try: return getattr(self._channels, name) - except: + except AttributeError: pass raise AttributeError('\'{}\' object has no attribute \'{}\''.format(self.__class__.__name__, name)) From 4b1b5d0767270d06f8b93200e42ecab3b727ce30 Mon Sep 17 00:00:00 2001 From: Jens Hedegaard Nielsen Date: Fri, 16 Jun 2017 11:23:38 +0200 Subject: [PATCH 06/19] fix typo --- qcodes/instrument/channel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/instrument/channel.py b/qcodes/instrument/channel.py index a92c095a003..670f789a452 100644 --- a/qcodes/instrument/channel.py +++ b/qcodes/instrument/channel.py @@ -331,7 +331,7 @@ def __getattr__(self, name): Params: name(str): The name of the parameter or function that we want to operate on. """ - # Check if this is a valid parametervx + # Check if this is a valid parameter if name in self._channels[0].parameters: setpoints = None setpoint_names = None From 0610ee5d76dbf86aef87ddd40aa4878080954a3d Mon Sep 17 00:00:00 2001 From: Jens Hedegaard Nielsen Date: Mon, 19 Jun 2017 14:49:26 +0200 Subject: [PATCH 07/19] Move print readable snapshot to instrument class where it belongs --- qcodes/instrument/base.py | 113 ++++++++++++++++++-------------------- 1 file changed, 54 insertions(+), 59 deletions(-) diff --git a/qcodes/instrument/base.py b/qcodes/instrument/base.py index a168f7f91e7..cac6072cbd5 100644 --- a/qcodes/instrument/base.py +++ b/qcodes/instrument/base.py @@ -38,7 +38,6 @@ def __init__(self, name, **kwargs): self.name = str(name) self.parameters = {} self.functions = {} - self.submodules = {} super().__init__(**kwargs) def add_parameter(self, name, parameter_class=StandardParameter, @@ -122,60 +121,6 @@ def snapshot_base(self, update=False): snap[attr] = getattr(self, attr) return snap - def print_readable_snapshot(self, update=False, max_chars=80): - """ - Prints a readable version of the snapshot. - The readable snapshot includes the name, value and unit of each - parameter. - A convenience function to quickly get an overview of the status of an instrument. - - Args: - update (bool) : If True, update the state by querying the - instrument. If False, just use the latest values in memory. - This argument gets passed to the snapshot function. - max_chars (int) : the maximum number of characters per line. The - readable snapshot will be cropped if this value is exceeded. - Defaults to 80 to be consistent with default terminal width. - """ - floating_types = (float, np.integer, np.floating) - snapshot = self.snapshot(update=update) - - par_lengths = [len(p) for p in snapshot['parameters']] - - # Min of 50 is to prevent a super long parameter name to break this - # function - par_field_len = min(max(par_lengths)+1, 50) - - print(self.name + ':') - print('{0:<{1}}'.format('\tparameter ', par_field_len) + 'value') - print('-'*80) - for par in sorted(snapshot['parameters']): - name = snapshot['parameters'][par]['name'] - msg = '{0:<{1}}:'.format(name, par_field_len) - val = snapshot['parameters'][par]['value'] - unit = snapshot['parameters'][par].get('unit', None) - if unit is None: - # this may be a multi parameter - unit = snapshot['parameters'][par].get('units', None) - if isinstance(val, floating_types): - msg += '\t{:.5g} '.format(val) - else: - msg += '\t{} '.format(val) - if unit is not '': # corresponds to no unit - msg += '({})'.format(unit) - # Truncate the message if it is longer than max length - if len(msg) > max_chars and not max_chars == -1: - msg = msg[0:max_chars-3] + '...' - print(msg) - - for submodule in self.submodules.values(): - if hasattr(submodule, '_channels'): - if submodule._snapshotable: - for channel in submodule._channels: - channel.print_readable_snapshot() - else: - submodule.print_readable_snapshot(update, max_chars) - # # shortcuts to parameters & setters & getters # # @@ -292,12 +237,8 @@ def __init__(self, name, **kwargs): warnings.warn("server_name argument not supported any more", stacklevel=0) super().__init__(name, **kwargs) - self.parameters = {} - self.functions = {} self.submodules = {} - self.name = str(name) - self.add_parameter('IDN', get_cmd=self.get_idn, vals=Anything()) @@ -525,6 +466,60 @@ def add_submodule(self, name, submodule): raise TypeError('Submodules must be metadatable.') self.submodules[name] = submodule + def print_readable_snapshot(self, update=False, max_chars=80): + """ + Prints a readable version of the snapshot. + The readable snapshot includes the name, value and unit of each + parameter. + A convenience function to quickly get an overview of the status of an instrument. + + Args: + update (bool) : If True, update the state by querying the + instrument. If False, just use the latest values in memory. + This argument gets passed to the snapshot function. + max_chars (int) : the maximum number of characters per line. The + readable snapshot will be cropped if this value is exceeded. + Defaults to 80 to be consistent with default terminal width. + """ + floating_types = (float, np.integer, np.floating) + snapshot = self.snapshot(update=update) + + par_lengths = [len(p) for p in snapshot['parameters']] + + # Min of 50 is to prevent a super long parameter name to break this + # function + par_field_len = min(max(par_lengths)+1, 50) + + print(self.name + ':') + print('{0:<{1}}'.format('\tparameter ', par_field_len) + 'value') + print('-'*80) + for par in sorted(snapshot['parameters']): + name = snapshot['parameters'][par]['name'] + msg = '{0:<{1}}:'.format(name, par_field_len) + val = snapshot['parameters'][par]['value'] + unit = snapshot['parameters'][par].get('unit', None) + if unit is None: + # this may be a multi parameter + unit = snapshot['parameters'][par].get('units', None) + if isinstance(val, floating_types): + msg += '\t{:.5g} '.format(val) + else: + msg += '\t{} '.format(val) + if unit is not '': # corresponds to no unit + msg += '({})'.format(unit) + # Truncate the message if it is longer than max length + if len(msg) > max_chars and not max_chars == -1: + msg = msg[0:max_chars-3] + '...' + print(msg) + + for submodule in self.submodules.values(): + if hasattr(submodule, '_channels'): + if submodule._snapshotable: + for channel in submodule._channels: + channel.print_readable_snapshot() + else: + submodule.print_readable_snapshot(update, max_chars) + def snapshot_base(self, update=False): """ State of the instrument as a JSON-compatible dict. From de6d1ed464fa5678a7a7a7e2f8c0a2a5c6c91deb Mon Sep 17 00:00:00 2001 From: Jens Hedegaard Nielsen Date: Mon, 19 Jun 2017 17:03:03 +0200 Subject: [PATCH 08/19] Fix: more channel annotation --- qcodes/instrument/channel.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/qcodes/instrument/channel.py b/qcodes/instrument/channel.py index 670f789a452..5232a9de819 100644 --- a/qcodes/instrument/channel.py +++ b/qcodes/instrument/channel.py @@ -27,7 +27,7 @@ class InstrumentChannel(InstrumentBase): Usually populated via ``add_function`` """ - def __init__(self, parent, name, **kwargs): + def __init__(self, parent: Instrument, name: str, **kwargs): # Initialize base classes of Instrument. We will overwrite what we want to do # in the Instrument initializer super().__init__(name=name, **kwargs) @@ -73,7 +73,10 @@ class MultiChannelInstrumentParameter(MultiParameter): param_name(str): Name of the multichannel parameter """ - def __init__(self, channels: Union[List, Tuple], param_name, *args, **kwargs): + def __init__(self, + channels: Union[List, Tuple], + param_name: str, + *args, **kwargs): super().__init__(*args, **kwargs) self._channels = channels self._param_name = param_name @@ -140,10 +143,13 @@ class ChannelList(Metadatable): """ - def __init__(self, parent: Instrument, name, chan_type: type, chan_list=None, - snapshotable=True, + def __init__(self, parent: Instrument, + name: str, + chan_type: type, + chan_list: Union[List, Tuple, None]=None, + snapshotable: bool=True, multichan_paramclass: type = MultiChannelInstrumentParameter, - oneindexed=False): + oneindexed: bool=False): super().__init__() self._parent = parent @@ -226,7 +232,7 @@ def __add__(self, other): return ChannelList(self._parent, self._name, self._chan_type, self._channels + other._channels, oneindexed=self._oneindexed) - def append(self, obj): + def append(self, obj: InstrumentChannel): """ When initially constructing the channel list, a new channel to add to the end of the list @@ -257,7 +263,7 @@ def extend(self, objects): raise TypeError("All items in a channel list must be of the same type.") return self._channels.extend(objects) - def index(self, obj): + def index(self, obj: InstrumentChannel): """ Return the index of the given object @@ -266,7 +272,7 @@ def index(self, obj): """ return self._channels.index(obj) + self._oneindexed - def insert(self, index, obj): + def insert(self, index: int, obj): """ Insert an object into the channel list at a specific index. @@ -300,7 +306,7 @@ def lock(self): self._channels = channeltuple(*self._channels) self._locked = True - def snapshot_base(self, update=False): + def snapshot_base(self, update: bool=False): """ State of the instrument as a JSON-compatible dict. @@ -323,7 +329,7 @@ def snapshot_base(self, update=False): } return snap - def __getattr__(self, name): + def __getattr__(self, name: str): """ Return a multi-channel function or parameter that we can use to get or set all items in a channel list simultaneously. From dd471b32ed6c8cd9507e9ce0be7374caa014b4c8 Mon Sep 17 00:00:00 2001 From: Jens Hedegaard Nielsen Date: Mon, 19 Jun 2017 17:07:34 +0200 Subject: [PATCH 09/19] Improve error message --- qcodes/instrument/channel.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qcodes/instrument/channel.py b/qcodes/instrument/channel.py index 5232a9de819..c176762bb59 100644 --- a/qcodes/instrument/channel.py +++ b/qcodes/instrument/channel.py @@ -189,7 +189,7 @@ def __getitem__(self, i): if isinstance(i, slice): if self._oneindexed: if i.start < 1 or i.stop < 1: - raise IndexError("1 indexed channel lists only support positive indexes") + raise IndexError("1 indexed channel lists only support positive indices") i = slice(i.start-1, i.stop-1, i.step) return ChannelList(self._parent, self._name, self._chan_type, self._channels[i], @@ -197,7 +197,7 @@ def __getitem__(self, i): oneindexed=self._oneindexed) elif self._oneindexed: if i < 1: - raise IndexError("1 indexed channel lists only support positive indexes") + raise IndexError("1 indexed channel lists only support positive indices") i += -1 return self._channels[i] @@ -270,7 +270,7 @@ def index(self, obj: InstrumentChannel): Args: obj(chan_type): The object to find in the channel list. """ - return self._channels.index(obj) + self._oneindexed + return self._channels.index(obj) + int(self._oneindexed) def insert(self, index: int, obj): """ @@ -289,7 +289,7 @@ def insert(self, index: int, obj): self._chan_type.__name__)) if self._oneindexed: if index < 1: - raise IndexError("1 based channel lists only support positive indexes") + raise IndexError("1 based channel lists only support positive indices") index += 1 return self._channels.insert(index, obj) From c3c7d5bb600a6c09741fab1de6be9dc59b03e506 Mon Sep 17 00:00:00 2001 From: Jens Hedegaard Nielsen Date: Mon, 19 Jun 2017 17:11:15 +0200 Subject: [PATCH 10/19] pprint limit line lenght of header to max char --- qcodes/instrument/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/instrument/base.py b/qcodes/instrument/base.py index cac6072cbd5..8b36b3739af 100644 --- a/qcodes/instrument/base.py +++ b/qcodes/instrument/base.py @@ -492,7 +492,7 @@ def print_readable_snapshot(self, update=False, max_chars=80): print(self.name + ':') print('{0:<{1}}'.format('\tparameter ', par_field_len) + 'value') - print('-'*80) + print('-'*max_chars) for par in sorted(snapshot['parameters']): name = snapshot['parameters'][par]['name'] msg = '{0:<{1}}:'.format(name, par_field_len) From 032144bfb95ef3d82e39a55d5099f979fecc95de Mon Sep 17 00:00:00 2001 From: Jens Hedegaard Nielsen Date: Mon, 19 Jun 2017 17:25:37 +0200 Subject: [PATCH 11/19] pep8 line lenght --- qcodes/instrument/channel.py | 174 +++++++++++++++++++++-------------- 1 file changed, 107 insertions(+), 67 deletions(-) diff --git a/qcodes/instrument/channel.py b/qcodes/instrument/channel.py index c176762bb59..157f4e8d2d2 100644 --- a/qcodes/instrument/channel.py +++ b/qcodes/instrument/channel.py @@ -13,23 +13,24 @@ class InstrumentChannel(InstrumentBase): Base class for a channel in an instrument Args: - parent (Instrument): the instrument to which this channel should be attached + parent (Instrument): the instrument to which this channel should be + attached name (str): the name of this channel Attributes: name (str): the name of this channel - parameters (Dict[Parameter]): All the parameters supported by this channel. - Usually populated via ``add_parameter`` + parameters (Dict[Parameter]): All the parameters supported by this + channel. Usually populated via ``add_parameter`` - functions (Dict[Function]): All the functions supported by this channel. - Usually populated via ``add_function`` + functions (Dict[Function]): All the functions supported by this + channel. Usually populated via ``add_function`` """ def __init__(self, parent: Instrument, name: str, **kwargs): - # Initialize base classes of Instrument. We will overwrite what we want to do - # in the Instrument initializer + # Initialize base classes of Instrument. We will overwrite what we + # want to do in the Instrument initializer super().__init__(name=name, **kwargs) self.parameters = {} @@ -66,10 +67,12 @@ class MultiChannelInstrumentParameter(MultiParameter): """ Parameter to get or set multiple channels simultaneously. - Will normally be created by a ChannelList and not directly by anything else. + Will normally be created by a ChannelList and not directly by anything + else. Args: - channels(list[chan_type]): A list of channels which we can operate on simultaneously. + channels(list[chan_type]): A list of channels which we can operate on + simultaneously. param_name(str): Name of the multichannel parameter """ @@ -83,9 +86,11 @@ def __init__(self, def get(self): """ - Return a tuple containing the data from each of the channels in the list + Return a tuple containing the data from each of the channels in the + list """ - return tuple(chan.parameters[self._param_name].get() for chan in self._channels) + return tuple(chan.parameters[self._param_name].get() for chan + in self._channels) def set(self, value): """ @@ -100,9 +105,10 @@ def set(self, value): @property def full_names(self): - """Overwrite full_names because the instument name is already included in the name. - This happens because the instument name is included in the channel name merged into the - parameter name above. + """Overwrite full_names because the instument name is already included + in the name. This happens because the instument name is included in + the channel name merged into the + parameter name above. """ return self.names @@ -131,9 +137,9 @@ class ChannelList(Metadatable): stored inside a channel list are accessible in multiple ways and should not be repeated in an instrument snapshot. - multichan_paramclass (MultiChannelInstrumentParameter): The class of the object - to be returned by the ChanneList's __getattr__ method. Should be a - subclass of MultiChannelInstrumentParameter. + multichan_paramclass (MultiChannelInstrumentParameter): The class of + the object to be returned by the ChanneList's __getattr__ method. + Should be a subclass of MultiChannelInstrumentParameter. Raises: ValueError: If chan_type is not a subclass of InstrumentChannel @@ -159,7 +165,8 @@ def __init__(self, parent: Instrument, raise ValueError("Channel Lists can only hold instances of type" " InstrumentChannel") if (not isinstance(multichan_paramclass, type) or - not issubclass(multichan_paramclass, MultiChannelInstrumentParameter)): + not issubclass(multichan_paramclass, + MultiChannelInstrumentParameter)): raise ValueError("multichan_paramclass must be a (subclass of) " "MultiChannelInstrumentParameter") @@ -168,8 +175,8 @@ def __init__(self, parent: Instrument, self._paramclass = multichan_paramclass self._oneindexed = oneindexed - # If a list of channels is not provided, define a list to store channels. - # This will eventually become a locked tuple. + # If a list of channels is not provided, define a list to store + # channels. This will eventually become a locked tuple. if chan_list is None: self._locked = False self._channels = [] @@ -177,19 +184,23 @@ def __init__(self, parent: Instrument, self._locked = True self._channels = tuple(chan_list) if not all(isinstance(chan, chan_type) for chan in self._channels): - raise TypeError("All items in this channel list must be of type {}.".format(chan_type.__name__)) + raise TypeError("All items in this channel list must be of " + "type {}.".format(chan_type.__name__)) def __getitem__(self, i): """ - Return either a single channel, or a new ChannelList containing only the specified channels + Return either a single channel, or a new ChannelList containing only + the specified channels Args: - i (int/slice): Either a single channel index or a slice of channels to get + i (int/slice): Either a single channel index or a slice of channels + to get """ if isinstance(i, slice): if self._oneindexed: if i.start < 1 or i.stop < 1: - raise IndexError("1 indexed channel lists only support positive indices") + raise IndexError("1 indexed channel lists only support " + "positive indices") i = slice(i.start-1, i.stop-1, i.step) return ChannelList(self._parent, self._name, self._chan_type, self._channels[i], @@ -197,7 +208,8 @@ def __getitem__(self, i): oneindexed=self._oneindexed) elif self._oneindexed: if i < 1: - raise IndexError("1 indexed channel lists only support positive indices") + raise IndexError("1 indexed channel lists only support " + "positive indices") i += -1 return self._channels[i] @@ -208,33 +220,43 @@ def __len__(self): return len(self._channels) def __repr__(self): - return "ChannelList({!r}, {}, {!r})".format(self._parent, self._chan_type.__name__, self._channels) + return "ChannelList({!r}, {}, {!r})".format(self._parent, + self._chan_type.__name__, + self._channels) def __add__(self, other): """ - Return a new channel list containing the channels from both ChannelList self and r. + Return a new channel list containing the channels from both + ChannelList self and r. Both channel lists must hold the same type and have the same parent. Args: other(ChannelList): Right argument to add. """ - if not isinstance(self, ChannelList) or not isinstance(other, ChannelList): - raise TypeError("Can't add objects of type {} and {} together".format( - type(self).__name__, type(other).__name__)) + if not isinstance(self, ChannelList) or not isinstance(other, + ChannelList): + raise TypeError("Can't add objects of type" + " {} and {} together".format(type(self).__name__, + type(other).__name__)) if self._chan_type != other._chan_type: - raise TypeError("Both l and r arguments to add must contain channels of the same type." - " Adding channels of type {} and {}.".format(self._chan_type.__name__, - other._chan_type.__name__)) + raise TypeError("Both l and r arguments to add must contain " + "channels of the same type." + " Adding channels of type " + "{} and {}.".format(self._chan_type.__name__, + other._chan_type.__name__)) if self._parent != other._parent: - raise ValueError("Can only add channels from the same parent together.") + raise ValueError("Can only add channels from the same parent " + "together.") - return ChannelList(self._parent, self._name, self._chan_type, self._channels + other._channels, + return ChannelList(self._parent, self._name, self._chan_type, + self._channels + other._channels, oneindexed=self._oneindexed) def append(self, obj: InstrumentChannel): """ - When initially constructing the channel list, a new channel to add to the end of the list + When initially constructing the channel list, a new channel to add to + the end of the list Args: obj(chan_type): New channel to add to the list. @@ -242,9 +264,10 @@ def append(self, obj: InstrumentChannel): if self._locked: raise AttributeError("Cannot append to a locked channel list") if not isinstance(obj, self._chan_type): - raise TypeError("All items in a channel list must be of the same type." - " Adding {} to a list of {}.".format(type(obj).__name__, - self._chan_type.__name__)) + raise TypeError("All items in a channel list must be of the same " + "type. Adding {} to a list of {}" + ".".format(type(obj).__name__, + self._chan_type.__name__)) return self._channels.append(obj) def extend(self, objects): @@ -252,15 +275,17 @@ def extend(self, objects): Insert an iterable of objects into the list of channels. Args: - objects(Iterable[chan_type]): A list of objects to add into the ChannelList. + objects(Iterable[chan_type]): A list of objects to add into the + ChannelList. """ - # objects may be a generator but we need to iterate over it twice below so - # copy it into a tuple just in case. + # objects may be a generator but we need to iterate over it twice + # below so copy it into a tuple just in case. objects = tuple(objects) if self._locked: raise AttributeError("Cannot extend a locked channel list") if not all(isinstance(obj, self._chan_type) for obj in objects): - raise TypeError("All items in a channel list must be of the same type.") + raise TypeError("All items in a channel list must be of the same " + "type.") return self._channels.extend(objects) def index(self, obj: InstrumentChannel): @@ -284,25 +309,28 @@ def insert(self, index: int, obj): if self._locked: raise AttributeError("Cannot insert into a locked channel list") if not isinstance(obj, self._chan_type): - raise TypeError("All items in a channel list must be of the same type." - " Adding {} to a list of {}.".format(type(obj).__name__, - self._chan_type.__name__)) + raise TypeError("All items in a channel list must be of the same " + "type. Adding {} to a list of {}" + ".".format(type(obj).__name__, + self._chan_type.__name__)) if self._oneindexed: if index < 1: - raise IndexError("1 based channel lists only support positive indices") + raise IndexError("1 based channel lists only support positive" + " indices") index += 1 return self._channels.insert(index, obj) def lock(self): """ - Lock the channel list. Once this is done, the channel list is converted to a tuple - and any future changes to the list are prevented. + Lock the channel list. Once this is done, the channel list is + converted to a tuple and any future changes to the list are prevented. """ if self._locked: return - channeltuple = namedtuple('channels', [channel.short_name for channel in self._channels]) + channeltuple = namedtuple('channels', [channel.short_name for channel + in self._channels]) self._channels = channeltuple(*self._channels) self._locked = True @@ -331,11 +359,12 @@ def snapshot_base(self, update: bool=False): def __getattr__(self, name: str): """ - Return a multi-channel function or parameter that we can use to get or set all items - in a channel list simultaneously. + Return a multi-channel function or parameter that we can use to get or + set all items in a channel list simultaneously. Params: - name(str): The name of the parameter or function that we want to operate on. + name(str): The name of the parameter or function that we want to + operate on. """ # Check if this is a valid parameter if name in self._channels[0].parameters: @@ -344,25 +373,35 @@ def __getattr__(self, name: str): setpoint_labels = None setpoint_units = None # We need to construct a MultiParameter object to get each of the - # values our of each parameter in our list, we don't currently try to - # construct a multiparameter from a list of multi parameters + # values our of each parameter in our list, we don't currently try + # to construct a multiparameter from a list of multi parameters if isinstance(self._channels[0].parameters[name], MultiParameter): - raise NotImplementedError("Slicing is currently not supported for MultiParameters") - names = tuple("{}_{}".format(chan.name, name) for chan in self._channels) - labels = tuple(chan.parameters[name].label for chan in self._channels) - units = tuple(chan.parameters[name].unit for chan in self._channels) + raise NotImplementedError("Slicing is currently not " + "supported for MultiParameters") + names = tuple("{}_{}".format(chan.name, name) + for chan in self._channels) + labels = tuple(chan.parameters[name].label + for chan in self._channels) + units = tuple(chan.parameters[name].unit + for chan in self._channels) if isinstance(self._channels[0].parameters[name], ArrayParameter): - shapes = tuple(chan.parameters[name].shape for chan in self._channels) + shapes = tuple(chan.parameters[name].shape for + chan in self._channels) if self._channels[0].parameters[name].setpoints: - setpoints = tuple(chan.parameters[name].setpoints for chan in self._channels) + setpoints = tuple(chan.parameters[name].setpoints for + chan in self._channels) if self._channels[0].parameters[name].setpoint_names: - setpoint_names = tuple(chan.parameters[name].setpoint_names for chan in self._channels) + setpoint_names = tuple(chan.parameters[name].setpoint_names + for chan in self._channels) if self._channels[0].parameters[name].setpoint_labels: - setpoint_labels = tuple(chan.parameters[name].setpoint_labels for chan in self._channels) + setpoint_labels = tuple( + chan.parameters[name].setpoint_labels + for chan in self._channels) if self._channels[0].parameters[name].setpoint_units: - setpoint_units = tuple(chan.parameters[name].setpoint_units for chan in self._channels) + setpoint_units = tuple(chan.parameters[name].setpoint_units + for chan in self._channels) else: shapes = tuple(() for _ in self._channels) @@ -382,8 +421,8 @@ def __getattr__(self, name: str): # Check if this is a valid function if name in self._channels[0].functions: - # We want to return a reference to a function that would call the function - # for each of the channels in turn. + # We want to return a reference to a function that would call the + # function for each of the channels in turn. def multi_func(*args, **kwargs): for chan in self._channels: chan.functions[name](*args, **kwargs) @@ -394,7 +433,8 @@ def multi_func(*args, **kwargs): except AttributeError: pass - raise AttributeError('\'{}\' object has no attribute \'{}\''.format(self.__class__.__name__, name)) + raise AttributeError('\'{}\' object has no attribute \'{}\'' + ''.format(self.__class__.__name__, name)) def __dir__(self): names = super().__dir__() From f0b6f44accccdf5497ae8084a55d59ef63c63f44 Mon Sep 17 00:00:00 2001 From: Jens Hedegaard Nielsen Date: Wed, 21 Jun 2017 15:29:34 +0200 Subject: [PATCH 12/19] Fix: channel remove support for oneindexed channels --- qcodes/instrument/channel.py | 34 +++++++--------------------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/qcodes/instrument/channel.py b/qcodes/instrument/channel.py index 157f4e8d2d2..2f9b428e69d 100644 --- a/qcodes/instrument/channel.py +++ b/qcodes/instrument/channel.py @@ -105,10 +105,9 @@ def set(self, value): @property def full_names(self): - """Overwrite full_names because the instument name is already included - in the name. This happens because the instument name is included in - the channel name merged into the - parameter name above. + """Overwrite full_names because the instrument name is already included + in the name. This happens because the instrument name is included in + the channel name merged into the parameter name above. """ return self.names @@ -154,8 +153,7 @@ def __init__(self, parent: Instrument, chan_type: type, chan_list: Union[List, Tuple, None]=None, snapshotable: bool=True, - multichan_paramclass: type = MultiChannelInstrumentParameter, - oneindexed: bool=False): + multichan_paramclass: type = MultiChannelInstrumentParameter): super().__init__() self._parent = parent @@ -173,7 +171,6 @@ def __init__(self, parent: Instrument, self._chan_type = chan_type self._snapshotable = snapshotable self._paramclass = multichan_paramclass - self._oneindexed = oneindexed # If a list of channels is not provided, define a list to store # channels. This will eventually become a locked tuple. @@ -197,20 +194,9 @@ def __getitem__(self, i): to get """ if isinstance(i, slice): - if self._oneindexed: - if i.start < 1 or i.stop < 1: - raise IndexError("1 indexed channel lists only support " - "positive indices") - i = slice(i.start-1, i.stop-1, i.step) return ChannelList(self._parent, self._name, self._chan_type, self._channels[i], - multichan_paramclass=self._paramclass, - oneindexed=self._oneindexed) - elif self._oneindexed: - if i < 1: - raise IndexError("1 indexed channel lists only support " - "positive indices") - i += -1 + multichan_paramclass=self._paramclass) return self._channels[i] def __iter__(self): @@ -250,8 +236,7 @@ def __add__(self, other): "together.") return ChannelList(self._parent, self._name, self._chan_type, - self._channels + other._channels, - oneindexed=self._oneindexed) + self._channels + other._channels) def append(self, obj: InstrumentChannel): """ @@ -295,7 +280,7 @@ def index(self, obj: InstrumentChannel): Args: obj(chan_type): The object to find in the channel list. """ - return self._channels.index(obj) + int(self._oneindexed) + return self._channels.index(obj) def insert(self, index: int, obj): """ @@ -313,11 +298,6 @@ def insert(self, index: int, obj): "type. Adding {} to a list of {}" ".".format(type(obj).__name__, self._chan_type.__name__)) - if self._oneindexed: - if index < 1: - raise IndexError("1 based channel lists only support positive" - " indices") - index += 1 return self._channels.insert(index, obj) From 2bdf3169b16e7791f61de1b090a8c00c58cda579 Mon Sep 17 00:00:00 2001 From: Jens Hedegaard Nielsen Date: Wed, 21 Jun 2017 15:58:08 +0200 Subject: [PATCH 13/19] improve type annotation --- qcodes/instrument/channel.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/qcodes/instrument/channel.py b/qcodes/instrument/channel.py index 2f9b428e69d..4144f9e6db9 100644 --- a/qcodes/instrument/channel.py +++ b/qcodes/instrument/channel.py @@ -1,5 +1,5 @@ """ Base class for the channel of an instrument """ -from typing import List, Tuple, Union +from typing import List, Tuple, Union, Optional from collections import namedtuple from .base import InstrumentBase, Instrument @@ -68,7 +68,7 @@ class MultiChannelInstrumentParameter(MultiParameter): Parameter to get or set multiple channels simultaneously. Will normally be created by a ChannelList and not directly by anything - else. + else. Args: channels(list[chan_type]): A list of channels which we can operate on @@ -84,7 +84,7 @@ def __init__(self, self._channels = channels self._param_name = param_name - def get(self): + def get(self) -> tuple: """ Return a tuple containing the data from each of the channels in the list @@ -151,7 +151,7 @@ class ChannelList(Metadatable): def __init__(self, parent: Instrument, name: str, chan_type: type, - chan_list: Union[List, Tuple, None]=None, + chan_list: Optional[List, Tuple]=None, snapshotable: bool=True, multichan_paramclass: type = MultiChannelInstrumentParameter): super().__init__() @@ -184,7 +184,7 @@ def __init__(self, parent: Instrument, raise TypeError("All items in this channel list must be of " "type {}.".format(chan_type.__name__)) - def __getitem__(self, i): + def __getitem__(self, i: Union[int, slice]): """ Return either a single channel, or a new ChannelList containing only the specified channels @@ -210,7 +210,7 @@ def __repr__(self): self._chan_type.__name__, self._channels) - def __add__(self, other): + def __add__(self, other: 'ChannelList'): """ Return a new channel list containing the channels from both ChannelList self and r. @@ -282,7 +282,7 @@ def index(self, obj: InstrumentChannel): """ return self._channels.index(obj) - def insert(self, index: int, obj): + def insert(self, index: int, obj: InstrumentChannel): """ Insert an object into the channel list at a specific index. @@ -416,7 +416,7 @@ def multi_func(*args, **kwargs): raise AttributeError('\'{}\' object has no attribute \'{}\'' ''.format(self.__class__.__name__, name)) - def __dir__(self): + def __dir__(self) -> list: names = super().__dir__() if self._channels: names += list(self._channels[0].parameters.keys()) From a88436f0a62432d46a730b7ff8b308a53c3852b7 Mon Sep 17 00:00:00 2001 From: Jens Hedegaard Nielsen Date: Wed, 21 Jun 2017 16:13:40 +0200 Subject: [PATCH 14/19] Optional -> Union --- qcodes/instrument/channel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qcodes/instrument/channel.py b/qcodes/instrument/channel.py index 4144f9e6db9..2140bb43665 100644 --- a/qcodes/instrument/channel.py +++ b/qcodes/instrument/channel.py @@ -1,5 +1,5 @@ """ Base class for the channel of an instrument """ -from typing import List, Tuple, Union, Optional +from typing import List, Tuple, Union from collections import namedtuple from .base import InstrumentBase, Instrument @@ -151,7 +151,7 @@ class ChannelList(Metadatable): def __init__(self, parent: Instrument, name: str, chan_type: type, - chan_list: Optional[List, Tuple]=None, + chan_list: Union[List, Tuple, None]=None, snapshotable: bool=True, multichan_paramclass: type = MultiChannelInstrumentParameter): super().__init__() From 644756189860c357638371c1c1496b5e570a958d Mon Sep 17 00:00:00 2001 From: Jens Hedegaard Nielsen Date: Wed, 21 Jun 2017 16:52:34 +0200 Subject: [PATCH 15/19] add channels to api docs --- docs/api/public.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api/public.rst b/docs/api/public.rst index 4ce87432fc9..cacfa5f2529 100644 --- a/docs/api/public.rst +++ b/docs/api/public.rst @@ -101,6 +101,8 @@ Instrument IPInstrument VisaInstrument + InstrumentChannel + ChannelList Plot ~~~~ From b38ad6ed49c6fb581fc6cf362be6b1132c6d756a Mon Sep 17 00:00:00 2001 From: Jens Hedegaard Nielsen Date: Thu, 22 Jun 2017 10:42:52 +0200 Subject: [PATCH 16/19] fix: make submodules nested --- qcodes/instrument/base.py | 197 ++++++++++++++++------------------- qcodes/instrument/channel.py | 3 - 2 files changed, 87 insertions(+), 113 deletions(-) diff --git a/qcodes/instrument/base.py b/qcodes/instrument/base.py index 8b36b3739af..5776cbdb1a4 100644 --- a/qcodes/instrument/base.py +++ b/qcodes/instrument/base.py @@ -38,6 +38,7 @@ def __init__(self, name, **kwargs): self.name = str(name) self.parameters = {} self.functions = {} + self.submodules = {} super().__init__(**kwargs) def add_parameter(self, name, parameter_class=StandardParameter, @@ -99,6 +100,36 @@ def add_function(self, name, **kwargs): func = Function(name=name, instrument=self, **kwargs) self.functions[name] = func + def add_submodule(self, name, submodule): + """ + Bind one submodule to this instrument. + + Instrument subclasses can call this repeatedly in their ``__init__`` + method for every submodule of the instrument. + + Submodules can effectively be considered as instruments within the main + instrument, and should at minimum be snapshottable. For example, they can + be used to either store logical groupings of parameters, which may or may + not be repeated, or channel lists. + + Args: + name (str): how the submodule will be stored within ``instrument.submodules`` + and also how it can be addressed. + + submodule (Metadatable): The submodule to be stored. + + Raises: + KeyError: if this instrument already contains a submodule with this + name. + TypeError: if the submodule that we are trying to add is not an instance + of an Metadatable object. + """ + if name in self.submodules: + raise KeyError('Duplicate submodule name {}'.format(name)) + if not isinstance(submodule, Metadatable): + raise TypeError('Submodules must be metadatable.') + self.submodules[name] = submodule + def snapshot_base(self, update=False): """ State of the instrument as a JSON-compatible dict. @@ -114,6 +145,8 @@ def snapshot_base(self, update=False): for name, param in self.parameters.items()), 'functions': dict((name, func.snapshot(update=update)) for name, func in self.functions.items()), + 'submodules': dict((name, subm.snapshot(update=update)) + for name, subm in self.submodules.items()), '__class__': full_class(self), } for attr in set(self._meta_attrs): @@ -121,6 +154,60 @@ def snapshot_base(self, update=False): snap[attr] = getattr(self, attr) return snap + def print_readable_snapshot(self, update=False, max_chars=80): + """ + Prints a readable version of the snapshot. + The readable snapshot includes the name, value and unit of each + parameter. + A convenience function to quickly get an overview of the status of an instrument. + + Args: + update (bool) : If True, update the state by querying the + instrument. If False, just use the latest values in memory. + This argument gets passed to the snapshot function. + max_chars (int) : the maximum number of characters per line. The + readable snapshot will be cropped if this value is exceeded. + Defaults to 80 to be consistent with default terminal width. + """ + floating_types = (float, np.integer, np.floating) + snapshot = self.snapshot(update=update) + + par_lengths = [len(p) for p in snapshot['parameters']] + + # Min of 50 is to prevent a super long parameter name to break this + # function + par_field_len = min(max(par_lengths)+1, 50) + + print(self.name + ':') + print('{0:<{1}}'.format('\tparameter ', par_field_len) + 'value') + print('-'*max_chars) + for par in sorted(snapshot['parameters']): + name = snapshot['parameters'][par]['name'] + msg = '{0:<{1}}:'.format(name, par_field_len) + val = snapshot['parameters'][par]['value'] + unit = snapshot['parameters'][par].get('unit', None) + if unit is None: + # this may be a multi parameter + unit = snapshot['parameters'][par].get('units', None) + if isinstance(val, floating_types): + msg += '\t{:.5g} '.format(val) + else: + msg += '\t{} '.format(val) + if unit is not '': # corresponds to no unit + msg += '({})'.format(unit) + # Truncate the message if it is longer than max length + if len(msg) > max_chars and not max_chars == -1: + msg = msg[0:max_chars-3] + '...' + print(msg) + + for submodule in self.submodules.values(): + if hasattr(submodule, '_channels'): + if submodule._snapshotable: + for channel in submodule._channels: + channel.print_readable_snapshot() + else: + submodule.print_readable_snapshot(update, max_chars) + # # shortcuts to parameters & setters & getters # # @@ -237,7 +324,6 @@ def __init__(self, name, **kwargs): warnings.warn("server_name argument not supported any more", stacklevel=0) super().__init__(name, **kwargs) - self.submodules = {} self.add_parameter('IDN', get_cmd=self.get_idn, vals=Anything()) @@ -435,115 +521,6 @@ def find_instrument(cls, name, instrument_class=None): return ins - - def add_submodule(self, name, submodule): - """ - Bind one submodule to this instrument. - - Instrument subclasses can call this repeatedly in their ``__init__`` - method for every submodule of the instrument. - - Submodules can effectively be considered as instruments within the main - instrument, and should at minimum be snapshottable. For example, they can - be used to either store logical groupings of parameters, which may or may - not be repeated, or channel lists. - - Args: - name (str): how the submodule will be stored within ``instrument.submodules`` - and also how it can be addressed. - - submodule (Metadatable): The submodule to be stored. - - Raises: - KeyError: if this instrument already contains a submodule with this - name. - TypeError: if the submodule that we are trying to add is not an instance - of an Metadatable object. - """ - if name in self.submodules: - raise KeyError('Duplicate submodule name {}'.format(name)) - if not isinstance(submodule, Metadatable): - raise TypeError('Submodules must be metadatable.') - self.submodules[name] = submodule - - def print_readable_snapshot(self, update=False, max_chars=80): - """ - Prints a readable version of the snapshot. - The readable snapshot includes the name, value and unit of each - parameter. - A convenience function to quickly get an overview of the status of an instrument. - - Args: - update (bool) : If True, update the state by querying the - instrument. If False, just use the latest values in memory. - This argument gets passed to the snapshot function. - max_chars (int) : the maximum number of characters per line. The - readable snapshot will be cropped if this value is exceeded. - Defaults to 80 to be consistent with default terminal width. - """ - floating_types = (float, np.integer, np.floating) - snapshot = self.snapshot(update=update) - - par_lengths = [len(p) for p in snapshot['parameters']] - - # Min of 50 is to prevent a super long parameter name to break this - # function - par_field_len = min(max(par_lengths)+1, 50) - - print(self.name + ':') - print('{0:<{1}}'.format('\tparameter ', par_field_len) + 'value') - print('-'*max_chars) - for par in sorted(snapshot['parameters']): - name = snapshot['parameters'][par]['name'] - msg = '{0:<{1}}:'.format(name, par_field_len) - val = snapshot['parameters'][par]['value'] - unit = snapshot['parameters'][par].get('unit', None) - if unit is None: - # this may be a multi parameter - unit = snapshot['parameters'][par].get('units', None) - if isinstance(val, floating_types): - msg += '\t{:.5g} '.format(val) - else: - msg += '\t{} '.format(val) - if unit is not '': # corresponds to no unit - msg += '({})'.format(unit) - # Truncate the message if it is longer than max length - if len(msg) > max_chars and not max_chars == -1: - msg = msg[0:max_chars-3] + '...' - print(msg) - - for submodule in self.submodules.values(): - if hasattr(submodule, '_channels'): - if submodule._snapshotable: - for channel in submodule._channels: - channel.print_readable_snapshot() - else: - submodule.print_readable_snapshot(update, max_chars) - - def snapshot_base(self, update=False): - """ - State of the instrument as a JSON-compatible dict. - - Args: - update (bool): If True, update the state by querying the - instrument. If False, just use the latest values in memory. - - Returns: - dict: base snapshot - """ - snap = {'parameters': dict((name, param.snapshot(update=update)) - for name, param in self.parameters.items()), - 'functions': dict((name, func.snapshot(update=update)) - for name, func in self.functions.items()), - 'submodules': dict((name, subm.snapshot(update=update)) - for name, subm in self.submodules.items()), - '__class__': full_class(self), - } - for attr in set(self._meta_attrs): - if hasattr(self, attr): - snap[attr] = getattr(self, attr) - return snap - # `write_raw` and `ask_raw` are the interface to hardware # # `write` and `ask` are standard wrappers to help with error reporting # # diff --git a/qcodes/instrument/channel.py b/qcodes/instrument/channel.py index 2140bb43665..039c7568a15 100644 --- a/qcodes/instrument/channel.py +++ b/qcodes/instrument/channel.py @@ -33,9 +33,6 @@ def __init__(self, parent: Instrument, name: str, **kwargs): # want to do in the Instrument initializer super().__init__(name=name, **kwargs) - self.parameters = {} - self.functions = {} - self.name = "{}_{}".format(parent.name, str(name)) self.short_name = str(name) self._meta_attrs = ['name'] From cc56d6a8bed321cd207fa3453767233d56a9ad78 Mon Sep 17 00:00:00 2001 From: Jens Hedegaard Nielsen Date: Thu, 22 Jun 2017 10:47:27 +0200 Subject: [PATCH 17/19] add submodule attributes to base --- qcodes/instrument/base.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/qcodes/instrument/base.py b/qcodes/instrument/base.py index 5776cbdb1a4..1e15297fafa 100644 --- a/qcodes/instrument/base.py +++ b/qcodes/instrument/base.py @@ -209,14 +209,14 @@ def print_readable_snapshot(self, update=False, max_chars=80): submodule.print_readable_snapshot(update, max_chars) # - # shortcuts to parameters & setters & getters # + # shortcuts to parameters & setters & getters # # # instrument['someparam'] === instrument.parameters['someparam'] # # instrument.someparam === instrument.parameters['someparam'] # # instrument.get('someparam') === instrument['someparam'].get() # # etc... # # - delegate_attr_dicts = ['parameters', 'functions'] + delegate_attr_dicts = ['parameters', 'functions', 'submodules'] def __getitem__(self, key): """Delegate instrument['name'] to parameter or function 'name'.""" @@ -599,6 +599,3 @@ def ask_raw(self, cmd): raise NotImplementedError( 'Instrument {} has not defined an ask method'.format( type(self).__name__)) - - delegate_attr_dicts = ['parameters', 'functions', 'submodules'] - From 14ea564a9e30634dad2e45baeb011cc97f35657e Mon Sep 17 00:00:00 2001 From: Jens Hedegaard Nielsen Date: Mon, 26 Jun 2017 14:58:18 +0200 Subject: [PATCH 18/19] Replace namedtuple by dict Make it possible to get channels by name even when not locked --- qcodes/instrument/channel.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/qcodes/instrument/channel.py b/qcodes/instrument/channel.py index 039c7568a15..9868effa5af 100644 --- a/qcodes/instrument/channel.py +++ b/qcodes/instrument/channel.py @@ -1,6 +1,5 @@ """ Base class for the channel of an instrument """ from typing import List, Tuple, Union -from collections import namedtuple from .base import InstrumentBase, Instrument from .parameter import MultiParameter, ArrayParameter @@ -169,6 +168,7 @@ def __init__(self, parent: Instrument, self._snapshotable = snapshotable self._paramclass = multichan_paramclass + self._channel_mapping = {} # provide lookup of channels by name # If a list of channels is not provided, define a list to store # channels. This will eventually become a locked tuple. if chan_list is None: @@ -177,6 +177,8 @@ def __init__(self, parent: Instrument, else: self._locked = True self._channels = tuple(chan_list) + self._channel_mapping = {channel.short_name: channel + for channel in self._channels} if not all(isinstance(chan, chan_type) for chan in self._channels): raise TypeError("All items in this channel list must be of " "type {}.".format(chan_type.__name__)) @@ -250,6 +252,7 @@ def append(self, obj: InstrumentChannel): "type. Adding {} to a list of {}" ".".format(type(obj).__name__, self._chan_type.__name__)) + self._channel_mapping[obj.short_name] = obj return self._channels.append(obj) def extend(self, objects): @@ -306,9 +309,7 @@ def lock(self): if self._locked: return - channeltuple = namedtuple('channels', [channel.short_name for channel - in self._channels]) - self._channels = channeltuple(*self._channels) + self._channels = tuple(self._channels) self._locked = True def snapshot_base(self, update: bool=False): @@ -406,8 +407,8 @@ def multi_func(*args, **kwargs): return multi_func try: - return getattr(self._channels, name) - except AttributeError: + return self._channel_mapping[name] + except KeyError: pass raise AttributeError('\'{}\' object has no attribute \'{}\'' @@ -418,6 +419,5 @@ def __dir__(self) -> list: if self._channels: names += list(self._channels[0].parameters.keys()) names += list(self._channels[0].functions.keys()) - if self._locked: - names += [channel.short_name for channel in self._channels] + names += [channel.short_name for channel in self._channels] return sorted(set(names)) From 91c0c4c03617b694cc11103e5311e2d25ec72c4d Mon Sep 17 00:00:00 2001 From: Jens Hedegaard Nielsen Date: Tue, 27 Jun 2017 15:53:20 +0200 Subject: [PATCH 19/19] Add submodule to docstring --- qcodes/instrument/base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/qcodes/instrument/base.py b/qcodes/instrument/base.py index 1e15297fafa..681f835f7e9 100644 --- a/qcodes/instrument/base.py +++ b/qcodes/instrument/base.py @@ -33,6 +33,10 @@ class InstrumentBase(Metadatable, DelegateAttributes): functions (Dict[Function]): All the functions supported by this instrument. Usually populated via ``add_function`` + + submodules (Dict[Metadatable]): All the submodules of this instrument + such as channel lists or logical groupings of parameters. + Usually populated via ``add_submodule`` """ def __init__(self, name, **kwargs): self.name = str(name)