From 5597ff080ce3fb2bb32e175c78da38e594dd3a04 Mon Sep 17 00:00:00 2001 From: Dominik Vogel Date: Wed, 11 Apr 2018 16:46:47 +0200 Subject: [PATCH 001/150] untested first attempt --- qcodes/instrument/channel.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/qcodes/instrument/channel.py b/qcodes/instrument/channel.py index 26acccc9dbb..1839e6336e9 100644 --- a/qcodes/instrument/channel.py +++ b/qcodes/instrument/channel.py @@ -259,6 +259,18 @@ def append(self, obj: InstrumentChannel): self._channel_mapping[obj.short_name] = obj return self._channels.append(obj) + def remove(self, obj: InstrumentChannel): + """ + Removes obj from channellist if not locked. + Args: + obj: Channel to remove from the list. + """ + if self._locked: + raise AttributeError("Cannot remove from a locked channel list") + + self._channels.remove(obj) + self._channel_mapping.pop(obj.short_name) + def extend(self, objects): """ Insert an iterable of objects into the list of channels. From 0b6e24bae548775033c8ffb5d63c60811c9ed158 Mon Sep 17 00:00:00 2001 From: nataliejpg Date: Thu, 19 Apr 2018 14:29:20 +0200 Subject: [PATCH 002/150] awg driver to separate out make, send and load of sequence into separate functions - untested --- .../instrument_drivers/tektronix/AWG5014.py | 151 ++++++++++-------- 1 file changed, 88 insertions(+), 63 deletions(-) diff --git a/qcodes/instrument_drivers/tektronix/AWG5014.py b/qcodes/instrument_drivers/tektronix/AWG5014.py index b5988b68769..a5fa5771ff3 100644 --- a/qcodes/instrument_drivers/tektronix/AWG5014.py +++ b/qcodes/instrument_drivers/tektronix/AWG5014.py @@ -1202,19 +1202,13 @@ def load_awg_file(self, filename): # we must update the appropriate parameter(s) for the sequence self.sequence_length.set(self.sequence_length.get()) - def make_send_and_load_awg_file(self, waveforms, m1s, m2s, - nreps, trig_waits, - goto_states, jump_tos, - channels=None, - filename='customawgfile.awg', - preservechannelsettings=True): + def make_awg_file(self, waveforms, m1s, m2s, + nreps, trig_waits, + goto_states, jump_tos, + channels=None, preservechannelsettings=True): """ - Makes an .awg-file, sends it to the AWG and loads it. The .awg-file - is uploaded to C:\\\\Users\\\\OEM\\\\Documents. The waveforms appear in - the user defined waveform list with names wfm001ch1, wfm002ch1, ... - Args: - waveforms (list): A list of the waveforms to upload. The list + waveforms (list): A list of the waveforms to be packed. The list should be filled like so: [[wfm1ch1, wfm2ch1, ...], [wfm1ch2, wfm2ch2], ...] Each waveform should be a numpy array with values in the range @@ -1233,7 +1227,7 @@ def make_send_and_load_awg_file(self, waveforms, m1s, m2s, nreps (list): List of integers specifying the no. of repetions per sequence element. Allowed values: 0 to - 65536. 0 corresponds to Infinite repetions. + 65536. O corresponds to Infinite repetions. trig_waits (list): List of len(segments) of integers specifying the trigger wait state of each sequence element. @@ -1250,20 +1244,12 @@ def make_send_and_load_awg_file(self, waveforms, m1s, m2s, channels (list): List of channels to send the waveforms to. Example: [1, 3, 2] - filename (str): The name of the .awg-file. Should end with the .awg - extension. Default: 'customawgfile.awg' - preservechannelsettings (bool): If True, the current channel settings are found from the parameter history and added to - the .awg file. Else, channel settings are reset to the factory - default values. Default: True. - """ - - # by default, an unusable directory is targeted on the AWG - self.visa_handle.write('MMEMory:CDIRectory ' + - '"C:\\Users\\OEM\\Documents"') - - # waveform names and the dictionary of packed waveforms + the .awg file. Else, channel settings are not written in the + file and will be reset to factory default when the file is + loaded. Default: True. + """ packed_wfs = {} waveform_names = [] if not isinstance(waveforms[0], list): @@ -1288,11 +1274,77 @@ def make_send_and_load_awg_file(self, waveforms, m1s, m2s, channel_cfg = {} - awg_file = self.generate_awg_file(packed_wfs, - wavenamearray, - nreps, trig_waits, goto_states, - jump_tos, channel_cfg, - preservechannelsettings=preservechannelsettings) + return self.generate_awg_file( + packed_wfs, wavenamearray, nreps, trig_waits, goto_states, + jump_tos, channel_cfg, + preservechannelsettings=preservechannelsettings) + + def make_send_and_load_awg_file(self, waveforms, m1s, m2s, + nreps, trig_waits, + goto_states, jump_tos, + channels=None, + filename='customawgfile.awg', + preservechannelsettings=True): + """ + Makes an .awg-file, sends it to the AWG and loads it. The .awg-file + is uploaded to C:\\\\Users\\\\OEM\\\\Documents. The waveforms appear in + the user defined waveform list with names wfm001ch1, wfm002ch1, ... + + Args: + waveforms (list): A list of the waveforms to upload. The list + should be filled like so: + [[wfm1ch1, wfm2ch1, ...], [wfm1ch2, wfm2ch2], ...] + Each waveform should be a numpy array with values in the range + -1 to 1 (inclusive). If you do not wish to send waveforms to + channels 1 and 2, use the channels parameter. + + m1s (list): A list of marker 1's. The list should be filled + like so: + [[elem1m1ch1, elem2m1ch1, ...], [elem1m1ch2, elem2m1ch2], ...] + Each marker should be a numpy array containing only 0's and 1's + + m2s (list): A list of marker 2's. The list should be filled + like so: + [[elem1m2ch1, elem2m2ch1, ...], [elem1m2ch2, elem2m2ch2], ...] + Each marker should be a numpy array containing only 0's and 1's + + nreps (list): List of integers specifying the no. of + repetions per sequence element. Allowed values: 0 to + 65536. 0 corresponds to Infinite repetions. + + trig_waits (list): List of len(segments) of integers specifying the + trigger wait state of each sequence element. + Allowed values: 0 (OFF) or 1 (ON). + + goto_states (list): List of len(segments) of integers + specifying the goto state of each sequence + element. Allowed values: 0 to 65536 (0 means next) + + jump_tos (list): List of len(segments) of integers specifying + the logic jump state for each sequence element. Allowed values: + 0 (OFF) or 1 (ON). + + channels (list): List of channels to send the waveforms to. + Example: [1, 3, 2] + + filename (str): The name of the .awg-file. Should end with the .awg + extension. Default: 'customawgfile.awg' + + preservechannelsettings (bool): If True, the current channel + settings are found from the parameter history and added to + the .awg file. Else, channel settings are reset to the factory + default values. Default: True. + """ + + # waveform names and the dictionary of packed waveforms + awg_file = self.make_awg_file( + waveforms, m1s, m2s, nreps, trig_waits, + goto_states, jump_tos, channels=channels, + preservechannelsettings=preservechannelsettings) + + # by default, an unusable directory is targeted on the AWG + self.visa_handle.write('MMEMory:CDIRectory ' + + '"C:\\Users\\OEM\\Documents"') self.send_awg_file(filename, awg_file) currentdir = self.visa_handle.query('MMEMory:CDIRectory?') @@ -1347,46 +1399,19 @@ def make_and_save_awg_file(self, waveforms, m1s, m2s, channels (list): List of channels to send the waveforms to. Example: [1, 3, 2] - filename (str): The full path of the .awg-file. Should end with the - .awg extension. Default: 'customawgfile.awg' - preservechannelsettings (bool): If True, the current channel settings are found from the parameter history and added to the .awg file. Else, channel settings are not written in the file and will be reset to factory default when the file is loaded. Default: True. - """ - - packed_wfs = {} - waveform_names = [] - if not isinstance(waveforms[0], list): - waveforms = [waveforms] - m1s = [m1s] - m2s = [m2s] - for ii in range(len(waveforms)): - namelist = [] - for jj in range(len(waveforms[ii])): - if channels is None: - thisname = 'wfm{:03d}ch{}'.format(jj + 1, ii + 1) - else: - thisname = 'wfm{:03d}ch{}'.format(jj + 1, channels[ii]) - namelist.append(thisname) - package = self.pack_waveform(waveforms[ii][jj], - m1s[ii][jj], - m2s[ii][jj]) - packed_wfs[thisname] = package - waveform_names.append(namelist) - - wavenamearray = np.array(waveform_names, dtype='str') - - channel_cfg = {} - - awg_file = self.generate_awg_file(packed_wfs, - wavenamearray, - nreps, trig_waits, goto_states, - jump_tos, channel_cfg, - preservechannelsettings=preservechannelsettings) + filename (str): The full path of the .awg-file. Should end with the + .awg extension. Default: 'customawgfile.awg' + """ + awg_file = self.make_awg_file( + waveforms, m1s, m2s, nreps, trig_waits, + goto_states, jump_tos, channels=channels, + preservechannelsettings=preservechannelsettings) with open(filename, 'wb') as fid: fid.write(awg_file) From 6ffa24fd28b7e0ea73461dd10a2967365dc8746b Mon Sep 17 00:00:00 2001 From: dpfranke Date: Thu, 3 May 2018 10:39:18 +0200 Subject: [PATCH 003/150] bugfix qtplot png representation I haven't looked into the details, but this fixes the png representation on my machine. Probably a change in pyqtgraph? @jenshnielsen @WilliamHPNielsen --- qcodes/plots/pyqtgraph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/plots/pyqtgraph.py b/qcodes/plots/pyqtgraph.py index 05ea316b33f..9eb1a219559 100644 --- a/qcodes/plots/pyqtgraph.py +++ b/qcodes/plots/pyqtgraph.py @@ -497,7 +497,7 @@ def _repr_png_(self): buffer.open(self.rpg.QtCore.QIODevice.ReadWrite) image.save(buffer, 'PNG') buffer.close() - return bytes(byte_array._getValue()) + return bytes(byte_array) def save(self, filename=None): """ From bea9ffafec28533f5df65f5e05d1d4fa4b430427 Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Wed, 30 May 2018 13:42:06 +0200 Subject: [PATCH 004/150] Change mark_run to mark_run_complete --- qcodes/dataset/data_set.py | 16 ++++++++++++---- qcodes/dataset/sqlite_base.py | 6 ++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/qcodes/dataset/data_set.py b/qcodes/dataset/data_set.py index 9b91dd57c09..6e79254044f 100644 --- a/qcodes/dataset/data_set.py +++ b/qcodes/dataset/data_set.py @@ -25,7 +25,7 @@ get_experiments, get_last_experiment, select_one_where, length, modify_values, - add_meta_data, mark_run, + add_meta_data, mark_run_complete, modify_many_values, insert_values, insert_many_values, VALUE, VALUES, get_data, @@ -165,7 +165,7 @@ def _clean_up(self) -> None: class DataSet(Sized): - def __init__(self, path_to_db: str, conn=None) -> None: + def __init__(self, path_to_db: str, run_id: int, conn=None) -> None: # TODO: handle fail here by defaulting to # a standard db self.path_to_db = path_to_db @@ -174,7 +174,10 @@ def __init__(self, path_to_db: str, conn=None) -> None: else: self.conn = conn + self.run_id = run_id self._debug = False + self.subscribers: Dict[str, Subscriber] = {} + self._completed = self._is_completed() def _new(self, name, exp_id, specs: SPECS = None, values=None, metadata=None) -> None: @@ -187,7 +190,6 @@ def _new(self, name, exp_id, specs: SPECS = None, values=None, # this is really the UUID (an ever increasing count in the db) self.run_id = run_id - self.subscribers: Dict[str, Subscriber] = {} self._completed = False @property @@ -229,6 +231,11 @@ def exp_id(self): return select_one_where(self.conn, "runs", "exp_id", "run_id", self.run_id) + def _is_completed(self) -> bool: + comp_time = select_one_where(self.conn, "runs", "completed_timestamp", + "run_id", self.run_id) + return False if comp_time is None else True + def toggle_debug(self): """ Toggle debug mode, if debug mode is on @@ -308,7 +315,8 @@ def completed(self) -> bool: @completed.setter def completed(self, value): self._completed = value - mark_run(self.conn, self.run_id, value) + if value: + mark_run_complete(self.conn, self.run_id) def mark_complete(self) -> None: """Mark dataset as complete and thus read only and notify the diff --git a/qcodes/dataset/sqlite_base.py b/qcodes/dataset/sqlite_base.py index 8fce35316ff..f1101e221dd 100644 --- a/qcodes/dataset/sqlite_base.py +++ b/qcodes/dataset/sqlite_base.py @@ -805,7 +805,9 @@ def new_experiment(conn: sqlite3.Connection, return curr.lastrowid -def mark_run(conn: sqlite3.Connection, run_id: int, complete: bool): +# TODO(WilliamHPNielsen): we should remove the redundant +# is_completed +def mark_run_complete(conn: sqlite3.Connection, run_id: int): """ Mark run complete Args: @@ -821,7 +823,7 @@ def mark_run(conn: sqlite3.Connection, run_id: int, complete: bool): is_completed=? WHERE run_id=?; """ - atomic_transaction(conn, query, time.time(), complete, run_id) + atomic_transaction(conn, query, time.time(), True, run_id) def completed(conn: sqlite3.Connection, run_id)->bool: From 4099fe824d963f7802e477e65af7c95427f98acb Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Wed, 30 May 2018 13:44:03 +0200 Subject: [PATCH 005/150] Update load_by_counter to use run_id correctly --- qcodes/dataset/data_set.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/qcodes/dataset/data_set.py b/qcodes/dataset/data_set.py index 6e79254044f..c6f313c4b5f 100644 --- a/qcodes/dataset/data_set.py +++ b/qcodes/dataset/data_set.py @@ -637,7 +637,7 @@ def load_by_counter(counter, exp_id): Returns: the dataset """ - d = DataSet(get_DB_location()) + conn = connect(get_DB_location) sql = """ SELECT run_id FROM @@ -646,8 +646,10 @@ def load_by_counter(counter, exp_id): result_counter= ? AND exp_id = ? """ - c = transaction(d.conn, sql, counter, exp_id) - d.run_id = one(c, 'run_id') + c = transaction(conn, sql, counter, exp_id) + run_id = one(c, 'run_id') + d = DataSet(get_DB_location(), run_id=run_id) + return d From cb25aa4387af4df992fe8706868f019610e1f395 Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Wed, 30 May 2018 13:45:10 +0200 Subject: [PATCH 006/150] Update load_by_id to use run_id correctly --- qcodes/dataset/data_set.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qcodes/dataset/data_set.py b/qcodes/dataset/data_set.py index c6f313c4b5f..cd6dcc51236 100644 --- a/qcodes/dataset/data_set.py +++ b/qcodes/dataset/data_set.py @@ -622,8 +622,7 @@ def load_by_id(run_id)->DataSet: the datasets """ - d = DataSet(get_DB_location()) - d.run_id = run_id + d = DataSet(get_DB_location(), run_id=run_id) return d From 4134cfed649e2bbe9ce560e2d0eb5c3f7889b306 Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Wed, 30 May 2018 13:52:04 +0200 Subject: [PATCH 007/150] Update new_data_set --- qcodes/dataset/data_set.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/qcodes/dataset/data_set.py b/qcodes/dataset/data_set.py index cd6dcc51236..2246705327d 100644 --- a/qcodes/dataset/data_set.py +++ b/qcodes/dataset/data_set.py @@ -667,15 +667,17 @@ def new_data_set(name, exp_id: Optional[int] = None, metadata: the values to associate with the dataset """ path_to_db = get_DB_location() - d = DataSet(path_to_db, conn=conn) + conn = connect(get_DB_location()) if exp_id is None: - if len(get_experiments(d.conn)) > 0: - exp_id = get_last_experiment(d.conn) + if len(get_experiments(conn)) > 0: + exp_id = get_last_experiment(conn) else: raise ValueError("No experiments found." "You can start a new one with:" " new_experiment(name, sample_name)") + + d = DataSet(path_to_db, run_id=0, conn=conn) d._new(name, exp_id, specs, values, metadata) return d From f7ec1817ffff88046f4d7719b533b866fde9494b Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Wed, 30 May 2018 14:03:15 +0200 Subject: [PATCH 008/150] Use sqlite_base completed function --- qcodes/dataset/data_set.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/qcodes/dataset/data_set.py b/qcodes/dataset/data_set.py index 2246705327d..9054cb4a6e7 100644 --- a/qcodes/dataset/data_set.py +++ b/qcodes/dataset/data_set.py @@ -21,7 +21,8 @@ from qcodes.instrument.parameter import _BaseParameter from qcodes.dataset.sqlite_base import (atomic, atomic_transaction, transaction, add_parameter, - connect, create_run, get_parameters, + connect, create_run, completed, + get_parameters, get_experiments, get_last_experiment, select_one_where, length, modify_values, @@ -177,7 +178,7 @@ def __init__(self, path_to_db: str, run_id: int, conn=None) -> None: self.run_id = run_id self._debug = False self.subscribers: Dict[str, Subscriber] = {} - self._completed = self._is_completed() + self._completed = completed(self.conn, self.run_id) def _new(self, name, exp_id, specs: SPECS = None, values=None, metadata=None) -> None: @@ -231,10 +232,6 @@ def exp_id(self): return select_one_where(self.conn, "runs", "exp_id", "run_id", self.run_id) - def _is_completed(self) -> bool: - comp_time = select_one_where(self.conn, "runs", "completed_timestamp", - "run_id", self.run_id) - return False if comp_time is None else True def toggle_debug(self): """ @@ -676,8 +673,9 @@ def new_data_set(name, exp_id: Optional[int] = None, raise ValueError("No experiments found." "You can start a new one with:" " new_experiment(name, sample_name)") - - d = DataSet(path_to_db, run_id=0, conn=conn) + # This is admittedly a bit weird. We create a dataset, link it to some + # run in the DB and then (using _new) change what it's linked to + d = DataSet(path_to_db, run_id=1, conn=conn) d._new(name, exp_id, specs, values, metadata) return d From 4a758de1831bc9eb12c3769f48683ba450b62175 Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Wed, 30 May 2018 14:18:52 +0200 Subject: [PATCH 009/150] Fix typo --- qcodes/dataset/data_set.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/qcodes/dataset/data_set.py b/qcodes/dataset/data_set.py index 9054cb4a6e7..32cc9c46257 100644 --- a/qcodes/dataset/data_set.py +++ b/qcodes/dataset/data_set.py @@ -166,7 +166,18 @@ def _clean_up(self) -> None: class DataSet(Sized): - def __init__(self, path_to_db: str, run_id: int, conn=None) -> None: + def __init__(self, path_to_db: str, run_id: Optional[int]=None, + conn=None) -> None: + """ + Create a new DataSet object. The object can either be intended + to hold a new run or and old run. + + Args: + path_to_db: path to the sqlite file on disk + run_id: provide this when loading an existing run, leave it + as None when creating a new run + conn: connection to the DB + """ # TODO: handle fail here by defaulting to # a standard db self.path_to_db = path_to_db @@ -178,7 +189,8 @@ def __init__(self, path_to_db: str, run_id: int, conn=None) -> None: self.run_id = run_id self._debug = False self.subscribers: Dict[str, Subscriber] = {} - self._completed = completed(self.conn, self.run_id) + if run_id: + self._completed = completed(self.conn, self.run_id) def _new(self, name, exp_id, specs: SPECS = None, values=None, metadata=None) -> None: @@ -633,7 +645,7 @@ def load_by_counter(counter, exp_id): Returns: the dataset """ - conn = connect(get_DB_location) + conn = connect(get_DB_location()) sql = """ SELECT run_id FROM @@ -675,7 +687,7 @@ def new_data_set(name, exp_id: Optional[int] = None, " new_experiment(name, sample_name)") # This is admittedly a bit weird. We create a dataset, link it to some # run in the DB and then (using _new) change what it's linked to - d = DataSet(path_to_db, run_id=1, conn=conn) + d = DataSet(path_to_db, run_id=None, conn=conn) d._new(name, exp_id, specs, values, metadata) return d From 9c08e2b6e8e2d0f6f0263cf549ec99280234206c Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Wed, 30 May 2018 16:02:12 +0200 Subject: [PATCH 010/150] Fix connection issue --- qcodes/dataset/data_set.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/qcodes/dataset/data_set.py b/qcodes/dataset/data_set.py index 32cc9c46257..4d797ac2e57 100644 --- a/qcodes/dataset/data_set.py +++ b/qcodes/dataset/data_set.py @@ -79,7 +79,7 @@ def __init__(self, dataSet, sub_id: str, # whether or not this is actually thread safe I am not sure :P self.dataSet = dataSet self.table_name = dataSet.table_name - self.conn = dataSet.conn + conn = dataSet.conn self.log = logging.getLogger(f"Subscriber {self.sub_id}") self.state = state @@ -96,14 +96,14 @@ def __init__(self, dataSet, sub_id: str, param_sql = ",".join([f"NEW.{p.name}" for p in parameters]) self.callbackid = f"callback{self.sub_id}" - self.conn.create_function(self.callbackid, -1, self.cache) + conn.create_function(self.callbackid, -1, self.cache) sql = f""" CREATE TRIGGER sub{self.sub_id} AFTER INSERT ON '{self.table_name}' BEGIN SELECT {self.callbackid}({param_sql}); END;""" - atomic_transaction(self.conn, sql) + atomic_transaction(conn, sql) self.data: Queue = Queue() self._data_set_len = len(dataSet) super().__init__() @@ -656,8 +656,9 @@ def load_by_counter(counter, exp_id): """ c = transaction(conn, sql, counter, exp_id) run_id = one(c, 'run_id') + conn.close() d = DataSet(get_DB_location(), run_id=run_id) - + return d @@ -676,7 +677,11 @@ def new_data_set(name, exp_id: Optional[int] = None, metadata: the values to associate with the dataset """ path_to_db = get_DB_location() - conn = connect(get_DB_location()) + if conn is None: + tempcon = True + conn = connect(get_DB_location()) + else: + tempcon = False if exp_id is None: if len(get_experiments(conn)) > 0: @@ -687,8 +692,12 @@ def new_data_set(name, exp_id: Optional[int] = None, " new_experiment(name, sample_name)") # This is admittedly a bit weird. We create a dataset, link it to some # run in the DB and then (using _new) change what it's linked to + if tempcon: + conn.close() + conn = None d = DataSet(path_to_db, run_id=None, conn=conn) d._new(name, exp_id, specs, values, metadata) + return d From c3b9345c1362612e1ce9e8e4786a29b3c92bcf7e Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Wed, 30 May 2018 16:32:14 +0200 Subject: [PATCH 011/150] Add test for loading datasets --- qcodes/tests/dataset/test_dataset_loading.py | 58 ++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 qcodes/tests/dataset/test_dataset_loading.py diff --git a/qcodes/tests/dataset/test_dataset_loading.py b/qcodes/tests/dataset/test_dataset_loading.py new file mode 100644 index 00000000000..a0e9ab68315 --- /dev/null +++ b/qcodes/tests/dataset/test_dataset_loading.py @@ -0,0 +1,58 @@ +from hypothesis import given, settings +import hypothesis.strategies as hst +import numpy as np + +import qcodes as qc +from qcodes import ParamSpec, new_data_set, new_experiment, experiments +from qcodes import load_by_id, load_by_counter +from qcodes.dataset.sqlite_base import connect, init_db, _unicode_categories +import qcodes.dataset.data_set +from qcodes.dataset.sqlite_base import get_user_version, set_user_version, atomic_transaction +from qcodes.dataset.data_set import CompletedError +from qcodes.dataset.database import initialise_database + +import qcodes.dataset.experiment_container +import pytest +import tempfile +import os + + +@pytest.fixture(scope="function") +def empty_temp_db(): + # create a temp database for testing + with tempfile.TemporaryDirectory() as tmpdirname: + qc.config["core"]["db_location"] = os.path.join(tmpdirname, 'temp.db') + qc.config["core"]["db_debug"] = True + initialise_database() + yield + + +@pytest.fixture(scope='function') +def experiment(empty_temp_db): + e = new_experiment("test-experiment", sample_name="test-sample") + yield e + e.conn.close() + + +@pytest.fixture(scope='function') +def dataset(experiment): + dataset = new_data_set("test-dataset") + yield dataset + dataset.conn.close() + + +def test_load_by_id(experiment): + ds = new_data_set("test-dataset") + run_id = ds.run_id + ds.mark_complete() + + loaded_ds = load_by_id(run_id) + assert loaded_ds.completed == True + assert loaded_ds.exp_id == 1 + + ds = new_data_set("test-dataset-unfinished") + run_id = ds.run_id + + loaded_ds = load_by_id(run_id) + assert loaded_ds.completed == False + assert loaded_ds.exp_id == 1 \ No newline at end of file From 26952344b60933e281b4c0f548c4c7fec18e6fd5 Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Wed, 30 May 2018 16:32:46 +0200 Subject: [PATCH 012/150] Remove whitespace --- qcodes/dataset/data_set.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qcodes/dataset/data_set.py b/qcodes/dataset/data_set.py index 4d797ac2e57..9dca5e66431 100644 --- a/qcodes/dataset/data_set.py +++ b/qcodes/dataset/data_set.py @@ -177,7 +177,7 @@ def __init__(self, path_to_db: str, run_id: Optional[int]=None, run_id: provide this when loading an existing run, leave it as None when creating a new run conn: connection to the DB - """ + """ # TODO: handle fail here by defaulting to # a standard db self.path_to_db = path_to_db @@ -658,7 +658,7 @@ def load_by_counter(counter, exp_id): run_id = one(c, 'run_id') conn.close() d = DataSet(get_DB_location(), run_id=run_id) - + return d @@ -695,9 +695,9 @@ def new_data_set(name, exp_id: Optional[int] = None, if tempcon: conn.close() conn = None - d = DataSet(path_to_db, run_id=None, conn=conn) + d = DataSet(path_to_db, run_id=None, conn=conn) d._new(name, exp_id, specs, values, metadata) - + return d From 0f1162363bdff1c3424686338acd994d95a9c1ff Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Wed, 30 May 2018 16:37:24 +0200 Subject: [PATCH 013/150] Clean up imports --- qcodes/tests/dataset/test_dataset_loading.py | 30 ++++++-------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/qcodes/tests/dataset/test_dataset_loading.py b/qcodes/tests/dataset/test_dataset_loading.py index a0e9ab68315..c12b04451e1 100644 --- a/qcodes/tests/dataset/test_dataset_loading.py +++ b/qcodes/tests/dataset/test_dataset_loading.py @@ -1,20 +1,12 @@ -from hypothesis import given, settings -import hypothesis.strategies as hst -import numpy as np - -import qcodes as qc -from qcodes import ParamSpec, new_data_set, new_experiment, experiments -from qcodes import load_by_id, load_by_counter -from qcodes.dataset.sqlite_base import connect, init_db, _unicode_categories -import qcodes.dataset.data_set -from qcodes.dataset.sqlite_base import get_user_version, set_user_version, atomic_transaction -from qcodes.dataset.data_set import CompletedError -from qcodes.dataset.database import initialise_database +import os -import qcodes.dataset.experiment_container import pytest import tempfile -import os + +import qcodes as qc +from qcodes.dataset.database import initialise_database +from qcodes.dataset.experiment_container import new_experiment +from qcodes.dataset.data_set import new_data_set, load_by_id @pytest.fixture(scope="function") @@ -34,13 +26,6 @@ def experiment(empty_temp_db): e.conn.close() -@pytest.fixture(scope='function') -def dataset(experiment): - dataset = new_data_set("test-dataset") - yield dataset - dataset.conn.close() - - def test_load_by_id(experiment): ds = new_data_set("test-dataset") run_id = ds.run_id @@ -55,4 +40,5 @@ def test_load_by_id(experiment): loaded_ds = load_by_id(run_id) assert loaded_ds.completed == False - assert loaded_ds.exp_id == 1 \ No newline at end of file + assert loaded_ds.exp_id == 1 + From b57fac15be82921b93e9ecc52df2c7394abf8b57 Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Wed, 30 May 2018 16:41:26 +0200 Subject: [PATCH 014/150] Add test for load_by_counter --- qcodes/tests/dataset/test_dataset_loading.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/qcodes/tests/dataset/test_dataset_loading.py b/qcodes/tests/dataset/test_dataset_loading.py index c12b04451e1..4d64bf1c016 100644 --- a/qcodes/tests/dataset/test_dataset_loading.py +++ b/qcodes/tests/dataset/test_dataset_loading.py @@ -6,7 +6,7 @@ import qcodes as qc from qcodes.dataset.database import initialise_database from qcodes.dataset.experiment_container import new_experiment -from qcodes.dataset.data_set import new_data_set, load_by_id +from qcodes.dataset.data_set import new_data_set, load_by_id, load_by_counter @pytest.fixture(scope="function") @@ -32,13 +32,26 @@ def test_load_by_id(experiment): ds.mark_complete() loaded_ds = load_by_id(run_id) - assert loaded_ds.completed == True + assert loaded_ds.completed is True assert loaded_ds.exp_id == 1 ds = new_data_set("test-dataset-unfinished") run_id = ds.run_id loaded_ds = load_by_id(run_id) - assert loaded_ds.completed == False + assert loaded_ds.completed is False assert loaded_ds.exp_id == 1 + +def test_load_by_counter(empty_temp_db): + exp = new_experiment(name="for_loading", sample_name="no_sample") + ds = new_data_set("my_first_ds") + + loaded_ds = load_by_counter(exp.exp_id, 1) + + assert loaded_ds.completed is False + + ds.mark_complete() + loaded_ds = load_by_counter(exp.exp_id, 1) + + assert loaded_ds.completed is True From c9fff69e84d73afd9f1ea737927ee153d4a8c69e Mon Sep 17 00:00:00 2001 From: sohail chatoor Date: Tue, 5 Jun 2018 17:12:06 +0200 Subject: [PATCH 015/150] made context manager for set --- qcodes/instrument/parameter.py | 28 ++++++++++++++++++++++++++++ qcodes/tests/test_parameter.py | 22 ++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/qcodes/instrument/parameter.py b/qcodes/instrument/parameter.py index 85377f6f3fb..99a382befd1 100644 --- a/qcodes/instrument/parameter.py +++ b/qcodes/instrument/parameter.py @@ -78,6 +78,29 @@ Number = Union[float, int] + +class _SetParamContext: + """ + This class is returned by the set method of parameters + + Example usage: + >>> v = dac.voltage() + >>> with dac.voltage(-1): + ... # Do stuff with the DAC output set to -1 V. + ... + >>> assert abs(dac.voltage() - v) <= tolerance + """ + def __init__(self, parameter): + self._parameter = parameter + self._original_value = parameter.get() + + def __enter__(self): + pass + + def __exit__(self, typ, value, traceback): + self._parameter.set(self._original_value) + + class _BaseParameter(Metadatable): """ Shared behavior for all parameters. Not intended to be used @@ -385,6 +408,8 @@ def set_wrapper(value, **kwargs): try: self.validate(value) + set_context_manager = _SetParamContext(self) + # In some cases intermediate sweep values must be used. # Unless `self.step` is defined, get_sweep_values will return # a list containing only `value`. @@ -449,6 +474,9 @@ def set_wrapper(value, **kwargs): if t_elapsed < self.post_delay: # Sleep until total time is larger than self.post_delay time.sleep(self.post_delay - t_elapsed) + + return set_context_manager + except Exception as e: e.args = e.args + ('setting {} to {}'.format(self, value),) raise e diff --git a/qcodes/tests/test_parameter.py b/qcodes/tests/test_parameter.py index d1baf3c44cd..a0975577a77 100644 --- a/qcodes/tests/test_parameter.py +++ b/qcodes/tests/test_parameter.py @@ -902,3 +902,25 @@ def tearDown(self): self.d.close() del self.a del self.d + + +class TestSetContextManager(TestCase): + + def setUp(self): + self.instrument = DummyInstrument('dummy_holder') + self.instrument.add_parameter( + "a", + set_cmd=None, + get_cmd=None + ) + + self.instrument.a.set(2) + + def tearDown(self): + self.instrument.close() + del self.instrument + + def test_context(self): + with self.instrument.a.set(3): + assert self.instrument.a.get() == 3 + assert self.instrument.a.get() == 2 From 4491ce05e6f72582790d6087950469a8bdeb9724 Mon Sep 17 00:00:00 2001 From: sohail chatoor Date: Wed, 6 Jun 2018 10:41:55 +0200 Subject: [PATCH 016/150] tests pass now --- qcodes/instrument/parameter.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/qcodes/instrument/parameter.py b/qcodes/instrument/parameter.py index 99a382befd1..af6ba23d977 100644 --- a/qcodes/instrument/parameter.py +++ b/qcodes/instrument/parameter.py @@ -92,10 +92,12 @@ class _SetParamContext: """ def __init__(self, parameter): self._parameter = parameter - self._original_value = parameter.get() + self._original_value = self._parameter._latest["value"] def __enter__(self): - pass + if not hasattr(self._parameter, "set"): + raise AttributeError("Can only use settable parameter as " + "context manager") def __exit__(self, typ, value, traceback): self._parameter.set(self._original_value) From d031b8ed8c9160237b3b44f0b32216d934a7fcf3 Mon Sep 17 00:00:00 2001 From: sohail chatoor Date: Wed, 6 Jun 2018 10:44:38 +0200 Subject: [PATCH 017/150] more extensive testing --- qcodes/tests/test_parameter.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/qcodes/tests/test_parameter.py b/qcodes/tests/test_parameter.py index a0975577a77..47af8e4eed1 100644 --- a/qcodes/tests/test_parameter.py +++ b/qcodes/tests/test_parameter.py @@ -914,13 +914,19 @@ def setUp(self): get_cmd=None ) - self.instrument.a.set(2) - def tearDown(self): self.instrument.close() del self.instrument + def test_none_value(self): + with self.instrument.a.set(3): + assert self.instrument.a.get() == 3 + assert self.instrument.a.get() is None + def test_context(self): + self.instrument.a.set(1) + self.instrument.a.set(2) + with self.instrument.a.set(3): assert self.instrument.a.get() == 3 assert self.instrument.a.get() == 2 From 5d206fa3b9c8d5a132450e1f71618f3e290e0dbc Mon Sep 17 00:00:00 2001 From: sohail chatoor Date: Wed, 6 Jun 2018 11:07:31 +0200 Subject: [PATCH 018/150] removed unneeded line from test --- qcodes/tests/test_parameter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qcodes/tests/test_parameter.py b/qcodes/tests/test_parameter.py index 47af8e4eed1..30a8f00b984 100644 --- a/qcodes/tests/test_parameter.py +++ b/qcodes/tests/test_parameter.py @@ -924,7 +924,6 @@ def test_none_value(self): assert self.instrument.a.get() is None def test_context(self): - self.instrument.a.set(1) self.instrument.a.set(2) with self.instrument.a.set(3): From 8597c032400ac6e1c98ebb4bac725d7a83f2669b Mon Sep 17 00:00:00 2001 From: sohail chatoor Date: Wed, 6 Jun 2018 14:44:31 +0200 Subject: [PATCH 019/150] there is no point in having a check in the set param context manager to check if there is a set attribute --- qcodes/instrument/parameter.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/qcodes/instrument/parameter.py b/qcodes/instrument/parameter.py index af6ba23d977..ae9f123c8ae 100644 --- a/qcodes/instrument/parameter.py +++ b/qcodes/instrument/parameter.py @@ -95,9 +95,7 @@ def __init__(self, parameter): self._original_value = self._parameter._latest["value"] def __enter__(self): - if not hasattr(self._parameter, "set"): - raise AttributeError("Can only use settable parameter as " - "context manager") + pass def __exit__(self, typ, value, traceback): self._parameter.set(self._original_value) From ee59340975f6138ee7ecc70d0955cac8168215aa Mon Sep 17 00:00:00 2001 From: sohail chatoor Date: Thu, 7 Jun 2018 11:01:53 +0200 Subject: [PATCH 020/150] fixed --- qcodes/dataset/measurements.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qcodes/dataset/measurements.py b/qcodes/dataset/measurements.py index 2f020768315..9ad48289c0a 100644 --- a/qcodes/dataset/measurements.py +++ b/qcodes/dataset/measurements.py @@ -116,6 +116,8 @@ def add_result(self, input_size = array_size elif any(isinstance(value, t) for t in non_array_like_types): pass + elif value is None: + pass else: raise ValueError('Wrong value type received. ' f'Got {type(value)}, but only int, float, ' From 5a220bd3a1ece42a756ec1a76e7d914a56ad261a Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Thu, 7 Jun 2018 11:13:13 +0200 Subject: [PATCH 021/150] Fix indexing errors --- qcodes/instrument_drivers/tektronix/AWG70000A.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/qcodes/instrument_drivers/tektronix/AWG70000A.py b/qcodes/instrument_drivers/tektronix/AWG70000A.py index cfb90d3991b..c5e0c5cc1a7 100644 --- a/qcodes/instrument_drivers/tektronix/AWG70000A.py +++ b/qcodes/instrument_drivers/tektronix/AWG70000A.py @@ -1115,7 +1115,9 @@ def makeSEQXFile(trig_waits: Sequence[int], flat_wfmxs += [AWG70000A.makeWFMXFile(wfm, amplitude) for wfm in wfm_lst] - flat_wfm_names = [name for lst in wfm_names for name in lst] + # This unfortunately assumes no subsequences + flat_wfm_names = list(np.reshape(np.array(wfm_names).transpose(), + (chans*elms,))) sml_file = AWG70000A._makeSMLFile(trig_waits, nreps, event_jumps, event_jump_to, @@ -1228,7 +1230,7 @@ def _makeSMLFile(trig_waits: Sequence[int], ' number of sequencing steps.') N = lstlens[0] - chans = np.shape(elem_names)[0] + chans = np.shape(elem_names)[1] # form the timestamp string timezone = time.timezone From 882f70ca84632d0297065b0c3fafcc9587360c17 Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Thu, 7 Jun 2018 11:26:49 +0200 Subject: [PATCH 022/150] Allow for N markers (not just 0 or 2) --- .../instrument_drivers/tektronix/AWG70000A.py | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/qcodes/instrument_drivers/tektronix/AWG70000A.py b/qcodes/instrument_drivers/tektronix/AWG70000A.py index c5e0c5cc1a7..43269725939 100644 --- a/qcodes/instrument_drivers/tektronix/AWG70000A.py +++ b/qcodes/instrument_drivers/tektronix/AWG70000A.py @@ -576,7 +576,7 @@ def makeWFMXFile(data: np.ndarray, amplitude: float) -> bytes: if len(shape) == 1: N = shape[0] markers_included = False - elif len(shape) == 2: + elif len(shape) in [2, 3, 4]: N = shape[1] markers_included = True else: @@ -807,8 +807,8 @@ def _makeWFMXFileBinaryData(data: np.ndarray, amplitude: float) -> bytes: Args: data: Either a shape (N,) array with only a waveform or - a shape (3, N) array with waveform, marker1, marker2, i.e. - data = np.array([wfm, m1, m2]). The waveform data is assumed + a shape (M, N) array with waveform, marker1, marker2, marker3, i.e. + data = np.array([wfm, m1, ...]). The waveform data is assumed to be in V. amplitude: The peak-to-peak amplitude (V) assumed to be set on the channel that will play this waveform. This information is @@ -830,12 +830,9 @@ def _makeWFMXFileBinaryData(data: np.ndarray, amplitude: float) -> bytes: N = shape[1] M = shape[0] wfm = data[0, :] - if M == 2: - markers = data[1, :] - elif M == 3: - m1 = data[1, :] - m2 = data[2, :] - markers = m1+2*m2 # This is how one byte encodes both markers + markers = data[1, :] + for i in range(1, M-1): + markers += data[i+1, :] * (2**i) markers = markers.astype(int) fmt = N*'B' # endian-ness doesn't matter for one byte binary_marker = struct.pack(fmt, *markers) @@ -925,15 +922,14 @@ def make_SEQX_from_forged_sequence( for pos1 in seq.keys(): for pos2 in seq[pos1]['content'].keys(): for ch, data in seq[pos1]['content'][pos2]['data'].items(): - # TODO: Add support for more than two markers wfm = data['wfm'] - # TODO: can we add support for single markers? - if 'm1' or 'm2' in data.keys(): - m1 = data.get('m1', np.zeros(len(wfm))) - m2 = data.get('m2', np.zeros(len(wfm))) - wfm_data = np.stack((wfm, m1, m2)) - else: - wfm_data = wfm + + markerdata = [] + for mkey in ['m1', 'm2', 'm3', 'm4']: + if mkey in data.keys(): + markerdata.append(data.get(mkey)) + wfm_data = np.stack((wfm, *markerdata)) + awgchan = channel_mapping[ch] wfmx = AWG70000A.makeWFMXFile(wfm_data, amplitudes[awgchan-1]) From 7db159c7f535ea80e56fdd191f7f48520bf0b1d6 Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Thu, 7 Jun 2018 11:27:12 +0200 Subject: [PATCH 023/150] Fix typo --- qcodes/instrument_drivers/tektronix/AWG70000A.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/instrument_drivers/tektronix/AWG70000A.py b/qcodes/instrument_drivers/tektronix/AWG70000A.py index 43269725939..4d47bb9622d 100644 --- a/qcodes/instrument_drivers/tektronix/AWG70000A.py +++ b/qcodes/instrument_drivers/tektronix/AWG70000A.py @@ -963,7 +963,7 @@ def make_SEQX_from_forged_sequence( seqings.append(pos_seqs) ss_wfm_names.append([n for n in wfmx_filenames - if f'wfm_{pos1}_{pos2}' in n]) + if f'wfm_{pos1}_{pos2}' in n]) seqing = {k: [d[k] for d in seqings] for k in seqings[0].keys()} From f7004c143b66845cad39ae73dbe234c566dc670b Mon Sep 17 00:00:00 2001 From: sohail chatoor Date: Thu, 7 Jun 2018 11:31:58 +0200 Subject: [PATCH 024/150] white space remove --- qcodes/dataset/measurements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/dataset/measurements.py b/qcodes/dataset/measurements.py index 9ad48289c0a..7eebbf63b93 100644 --- a/qcodes/dataset/measurements.py +++ b/qcodes/dataset/measurements.py @@ -117,7 +117,7 @@ def add_result(self, elif any(isinstance(value, t) for t in non_array_like_types): pass elif value is None: - pass + pass else: raise ValueError('Wrong value type received. ' f'Got {type(value)}, but only int, float, ' From 5cac97f27b0b86ade44a96f5a10a5a7655396e3f Mon Sep 17 00:00:00 2001 From: sohail chatoor Date: Thu, 7 Jun 2018 15:36:45 +0200 Subject: [PATCH 025/150] Added in the docstring that add_results method of data set now can handle missing keys. None values are inserted for those keys --- qcodes/dataset/data_set.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/qcodes/dataset/data_set.py b/qcodes/dataset/data_set.py index 9b91dd57c09..bbaf6e6b9e4 100644 --- a/qcodes/dataset/data_set.py +++ b/qcodes/dataset/data_set.py @@ -360,8 +360,9 @@ def add_results(self, results: List[Dict[str, VALUE]]) -> int: Args: - list of name, value dictionaries where each - dictionary provides the values for all of the parameters in - that result. + dictionary provides the values for of the parameters in + that result. If some parameters are missing the corresponding + values are assumed to be None Returns: - the index in the DataSet that the **first** result was stored at @@ -370,9 +371,11 @@ def add_results(self, results: List[Dict[str, VALUE]]) -> int: the name of a parameter in this DataSet. It is an error to add results to a completed DataSet. """ - values = [list(val.values()) for val in results] + expected_keys = frozenset.union(*[frozenset(d) for d in results]) + values = [[d.get(k, None) for k in expected_keys] for d in results] + len_before_add = length(self.conn, self.table_name) - insert_many_values(self.conn, self.table_name, list(results[0].keys()), + insert_many_values(self.conn, self.table_name, expected_keys, values) # TODO: should this not be made atomic? self.conn.commit() From b19e1720dd979a0cd1446a21c475e5fb5310a924 Mon Sep 17 00:00:00 2001 From: sohail chatoor Date: Thu, 7 Jun 2018 16:44:33 +0200 Subject: [PATCH 026/150] cast expected_keys to list --- qcodes/dataset/data_set.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/dataset/data_set.py b/qcodes/dataset/data_set.py index bbaf6e6b9e4..6e1c3000ae6 100644 --- a/qcodes/dataset/data_set.py +++ b/qcodes/dataset/data_set.py @@ -375,7 +375,7 @@ def add_results(self, results: List[Dict[str, VALUE]]) -> int: values = [[d.get(k, None) for k in expected_keys] for d in results] len_before_add = length(self.conn, self.table_name) - insert_many_values(self.conn, self.table_name, expected_keys, + insert_many_values(self.conn, self.table_name, list(expected_keys), values) # TODO: should this not be made atomic? self.conn.commit() From 4b0af1b3fa0a44d4b520073e6d77fd12005566b0 Mon Sep 17 00:00:00 2001 From: dpfranke Date: Thu, 7 Jun 2018 18:02:46 +0200 Subject: [PATCH 027/150] Update pyqtgraph.py --- qcodes/plots/pyqtgraph.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/qcodes/plots/pyqtgraph.py b/qcodes/plots/pyqtgraph.py index 9eb1a219559..05369032b1b 100644 --- a/qcodes/plots/pyqtgraph.py +++ b/qcodes/plots/pyqtgraph.py @@ -497,7 +497,11 @@ def _repr_png_(self): buffer.open(self.rpg.QtCore.QIODevice.ReadWrite) image.save(buffer, 'PNG') buffer.close() - return bytes(byte_array) + + if hasattr(byte_array, '_getValue'): + return bytes(byte_array._getValue()) + else: + return bytes(byte_array) def save(self, filename=None): """ From c596978a9ca7b6699e539f0e05bd564573dd4f58 Mon Sep 17 00:00:00 2001 From: sohail chatoor Date: Thu, 7 Jun 2018 18:22:31 +0200 Subject: [PATCH 028/150] added test to verify the correct working of missing keys when calling add_results on a data set --- qcodes/tests/dataset/test_dataset_basic.py | 47 ++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/qcodes/tests/dataset/test_dataset_basic.py b/qcodes/tests/dataset/test_dataset_basic.py index ed04d788b46..ef713308ede 100644 --- a/qcodes/tests/dataset/test_dataset_basic.py +++ b/qcodes/tests/dataset/test_dataset_basic.py @@ -1,6 +1,7 @@ from hypothesis import given, settings import hypothesis.strategies as hst import numpy as np +import itertools import qcodes as qc from qcodes import ParamSpec, new_data_set, new_experiment, experiments @@ -372,3 +373,49 @@ def test_numpy_nan(dataset): dataset.add_results(data_dict) retrieved = dataset.get_data("m") assert np.isnan(retrieved[1]) + + +def test_missing_keys(dataset): + """ + Test that we can now have partial results with keys missing. This is for + example handy when having an interleaved 1D and 2D sweep. + """ + + x = ParamSpec("x", paramtype='numeric') + y = ParamSpec("y", paramtype='numeric') + a = ParamSpec("a", paramtype='numeric', depends_on=[x]) + b = ParamSpec("b", paramtype='numeric', depends_on=[x, y]) + + dataset.add_parameter(x) + dataset.add_parameter(y) + dataset.add_parameter(a) + dataset.add_parameter(b) + + def fa(xv): + return xv + 1 + + def fb(xv, yv): + return xv + 2 - yv * 3 + + results = [] + xvals = [1, 2, 3] + yvals = [2, 3, 4] + + for xv in xvals: + results.append({"x": xv, "a": fa(xv)}) + for yv in yvals: + results.append({"x": xv, "y": yv, "b": fb(xv, yv)}) + + dataset.add_results(results) + + assert dataset.get_values("x") == [[r["x"]] for r in results] + assert dataset.get_values("y") == [[r["y"]] for r in results if "y" in r] + assert dataset.get_values("a") == [[r["a"]] for r in results if "a" in r] + assert dataset.get_values("b") == [[r["b"]] for r in results if "b" in r] + + assert dataset.get_setpoints("a") == [[[xv] for xv in xvals]] + + tmp = [list(t) for t in zip(*(itertools.product(xvals, yvals)))] + expected_setpoints = [[[v] for v in vals] for vals in tmp] + + assert dataset.get_setpoints("b") == expected_setpoints From edd7ed4a5db11c8e7b8e99e192b09d7f7f565d6c Mon Sep 17 00:00:00 2001 From: MIkhail Astafev Date: Fri, 8 Jun 2018 12:27:20 +0200 Subject: [PATCH 029/150] qcodes-1124 Add explicit flushing to avoid instability of testing --- qcodes/tests/dataset/test_measurement_context_manager.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/qcodes/tests/dataset/test_measurement_context_manager.py b/qcodes/tests/dataset/test_measurement_context_manager.py index 2c8bbe16183..0c4117483ef 100644 --- a/qcodes/tests/dataset/test_measurement_context_manager.py +++ b/qcodes/tests/dataset/test_measurement_context_manager.py @@ -341,6 +341,10 @@ def action(lst, word): def test_subscriptions(experiment, DAC, DMM): + """ + Here the following is tested: subscribers should be called at the moment the data is flushed to the database, + i.e. after new result is added. For the purpose of testing, flush_data_to_database method is called explicitly. + """ def subscriber1(results, length, state): """ @@ -383,8 +387,10 @@ def subscriber2(results, length, state): (a, b) = as_and_bs[num] expected_list += [c for c in (a, b) if c > 7] - sleep(1.2*meas.write_period) + datasaver.add_result((DAC.ch1, a), (DMM.v1, b)) + datasaver.flush_data_to_database() + assert lt7s == expected_list assert list(res_dict.keys()) == [n for n in range(1, num+2)] From be8a13a2d6f86e3847c7ea20c02aa58692a043ae Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Fri, 8 Jun 2018 13:04:38 +0200 Subject: [PATCH 030/150] Handle channels better in SML file generation --- .../instrument_drivers/tektronix/AWG70000A.py | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/qcodes/instrument_drivers/tektronix/AWG70000A.py b/qcodes/instrument_drivers/tektronix/AWG70000A.py index 4d47bb9622d..9478207a9a2 100644 --- a/qcodes/instrument_drivers/tektronix/AWG70000A.py +++ b/qcodes/instrument_drivers/tektronix/AWG70000A.py @@ -940,6 +940,9 @@ def make_SEQX_from_forged_sequence( # STEP 2: # Make all subsequence .sml files + print('Waveforms done') + print(wfmx_filenames) + subseqsml_files: List[str] = [] subseqsml_filenames: List[str] = [] @@ -970,13 +973,16 @@ def make_SEQX_from_forged_sequence( subseqname = f'subsequence_{pos1}' + print(ss_wfm_names) + subseqsml = AWG70000A._makeSMLFile(trig_waits=seqing['twait'], nreps=seqing['nrep'], event_jumps=seqing['jump_input'], event_jump_to=seqing['jump_target'], go_to=seqing['goto'], elem_names=ss_wfm_names, - seqname=subseqname) + seqname=subseqname, + chans=len(channel_mapping)) subseqsml_files.append(subseqsml) subseqsml_filenames.append(f'{subseqname}') @@ -1006,6 +1012,9 @@ def make_SEQX_from_forged_sequence( if f'wfm_{pos1}' in wn]) seqing = {k: [d[k] for d in seqings] for k in seqings[0].keys()} + print('True debug') + print(asset_names) + mainseqname = seqname mainseqsml = AWG70000A._makeSMLFile(trig_waits=seqing['twait'], nreps=seqing['nrep'], @@ -1014,6 +1023,7 @@ def make_SEQX_from_forged_sequence( go_to=seqing['goto'], elem_names=asset_names, seqname=mainseqname, + chans=len(channel_mapping), subseq_positions=subseq_positions) ########## @@ -1117,7 +1127,9 @@ def makeSEQXFile(trig_waits: Sequence[int], sml_file = AWG70000A._makeSMLFile(trig_waits, nreps, event_jumps, event_jump_to, - go_to, wfm_names, seqname) + go_to, wfm_names, + seqname, + chans) user_file = b'' setup_file = AWG70000A._makeSetupFile(seqname) @@ -1179,6 +1191,7 @@ def _makeSMLFile(trig_waits: Sequence[int], go_to: Sequence[int], elem_names: Sequence[Sequence[str]], seqname: str, + chans: int, subseq_positions: List[int]=[]) -> str: """ Make an xml file describing a sequence. @@ -1201,6 +1214,9 @@ def _makeSMLFile(trig_waits: Sequence[int], [wfmpos3ch1, wfmpos3ch2, ...], ...] seqname: The name of the sequence. This name will appear in the sequence list of the instrument. + chans: The number of channels. Can not be inferred in the case + of a sequence containing only subsequences, so must be provided + up front. subseq_positions: The positions (step numbers) occupied by subsequences @@ -1226,7 +1242,6 @@ def _makeSMLFile(trig_waits: Sequence[int], ' number of sequencing steps.') N = lstlens[0] - chans = np.shape(elem_names)[1] # form the timestamp string timezone = time.timezone From 38dd5d4b825c3ed5b91d3e63efc97b822c8b3adc Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Fri, 8 Jun 2018 13:05:29 +0200 Subject: [PATCH 031/150] Update test to handle chans better --- qcodes/tests/drivers/test_tektronix_AWG70000A.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/tests/drivers/test_tektronix_AWG70000A.py b/qcodes/tests/drivers/test_tektronix_AWG70000A.py index 27bf4402104..83346a7dd23 100644 --- a/qcodes/tests/drivers/test_tektronix_AWG70000A.py +++ b/qcodes/tests/drivers/test_tektronix_AWG70000A.py @@ -121,7 +121,7 @@ def test_SML_successful_generation_vary_length(N): seqname = 'seq' smlstring = AWG70000A._makeSMLFile(tw, nreps, ejs, ejt, goto, - wfm_names, seqname) + wfm_names, seqname, chans=3) # This line will raise an exception if the XML is not valid etree.parse(StringIO(smlstring)) From 5c8cf3cdfb784810772250c8d254f26066e95a4e Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Fri, 8 Jun 2018 13:12:59 +0200 Subject: [PATCH 032/150] Indent correctly (or suffer the consequences!) --- qcodes/instrument_drivers/tektronix/AWG70000A.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/instrument_drivers/tektronix/AWG70000A.py b/qcodes/instrument_drivers/tektronix/AWG70000A.py index 9478207a9a2..10cbbac79cf 100644 --- a/qcodes/instrument_drivers/tektronix/AWG70000A.py +++ b/qcodes/instrument_drivers/tektronix/AWG70000A.py @@ -833,7 +833,7 @@ def _makeWFMXFileBinaryData(data: np.ndarray, amplitude: float) -> bytes: markers = data[1, :] for i in range(1, M-1): markers += data[i+1, :] * (2**i) - markers = markers.astype(int) + markers = markers.astype(int) fmt = N*'B' # endian-ness doesn't matter for one byte binary_marker = struct.pack(fmt, *markers) From 1e7c0744707381b9f980871c2376cc425ba8155d Mon Sep 17 00:00:00 2001 From: MIkhail Astafev Date: Fri, 8 Jun 2018 13:40:54 +0200 Subject: [PATCH 033/150] qcodes-1124 Make test function docstring PEP8 style --- .../tests/dataset/test_measurement_context_manager.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/qcodes/tests/dataset/test_measurement_context_manager.py b/qcodes/tests/dataset/test_measurement_context_manager.py index 0c4117483ef..a35fafc55e4 100644 --- a/qcodes/tests/dataset/test_measurement_context_manager.py +++ b/qcodes/tests/dataset/test_measurement_context_manager.py @@ -342,8 +342,15 @@ def action(lst, word): def test_subscriptions(experiment, DAC, DMM): """ - Here the following is tested: subscribers should be called at the moment the data is flushed to the database, - i.e. after new result is added. For the purpose of testing, flush_data_to_database method is called explicitly. + Test that subscribers are called at the moment that data is flushed to database + + Note that for the purpose of this test, flush_data_to_database method is called explicitly instead of waiting for + the data to be flushed automatically after the write_period passes after a add_result call. + + Args: + experiment (qcodes.dataset.experiment_container.Experiment) : qcodes experiment object + DAC (qcodes.instrument.base.Instrument) : dummy instrument object + DMM (qcodes.instrument.base.Instrument) : another dummy instrument object """ def subscriber1(results, length, state): From 5169a937ad65304ff6f9438b308a800482675b62 Mon Sep 17 00:00:00 2001 From: sohail chatoor Date: Fri, 8 Jun 2018 15:24:24 +0200 Subject: [PATCH 034/150] We do not allow None types to be added to the data saver --- qcodes/dataset/measurements.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/qcodes/dataset/measurements.py b/qcodes/dataset/measurements.py index 7eebbf63b93..2f020768315 100644 --- a/qcodes/dataset/measurements.py +++ b/qcodes/dataset/measurements.py @@ -116,8 +116,6 @@ def add_result(self, input_size = array_size elif any(isinstance(value, t) for t in non_array_like_types): pass - elif value is None: - pass else: raise ValueError('Wrong value type received. ' f'Got {type(value)}, but only int, float, ' From 1b3275b0523a797c2b0819fc3969c344a62daa1f Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sun, 10 Jun 2018 21:25:05 +0200 Subject: [PATCH 035/150] Close_all catch all exceptions and log them In this special case it's better to catch catch bare exceptions since this is teardown code and any exception will stop closeing the rest of the instruments --- qcodes/instrument/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qcodes/instrument/base.py b/qcodes/instrument/base.py index 2c88743fc37..578ad6e8b1f 100644 --- a/qcodes/instrument/base.py +++ b/qcodes/instrument/base.py @@ -489,8 +489,8 @@ def close_all(cls) -> None: inst = cls.find_instrument(inststr) log.info(f"Closing {inststr}") inst.close() - except KeyError: - log.info(f"Failed to close {inststr}, ignored") + except: + log.exception(f"Failed to close {inststr}, ignored") pass @classmethod From 74d8b3313918efb90761bd3b62334b1d8dc55339 Mon Sep 17 00:00:00 2001 From: sohail chatoor Date: Mon, 11 Jun 2018 09:45:49 +0200 Subject: [PATCH 036/150] corrected grammer error in the add_results docstring --- qcodes/dataset/data_set.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/dataset/data_set.py b/qcodes/dataset/data_set.py index 6e1c3000ae6..109c78e69e7 100644 --- a/qcodes/dataset/data_set.py +++ b/qcodes/dataset/data_set.py @@ -360,7 +360,7 @@ def add_results(self, results: List[Dict[str, VALUE]]) -> int: Args: - list of name, value dictionaries where each - dictionary provides the values for of the parameters in + dictionary provides the values for the parameters in that result. If some parameters are missing the corresponding values are assumed to be None From 625eb59fc3b48d0ea897aea041c0d32b7e941c1c Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 7 Jun 2018 20:51:20 +0200 Subject: [PATCH 037/150] make register_parameter match add_result for array_parameter --- qcodes/dataset/measurements.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qcodes/dataset/measurements.py b/qcodes/dataset/measurements.py index 2f020768315..90d0ecf31d7 100644 --- a/qcodes/dataset/measurements.py +++ b/qcodes/dataset/measurements.py @@ -422,8 +422,8 @@ def register_parameter( if isinstance(parameter, ArrayParameter): parameter = cast(ArrayParameter, parameter) spname_parts = [] - if parameter.root_instrument is not None: - inst_name = parameter.root_instrument.name + if parameter._instrument is not None: + inst_name = parameter._instrument.name if inst_name is not None: spname_parts.append(inst_name) if parameter.setpoint_names is not None: From e5c78d6d7a4640ef4f651b70a0f4c7e00c0a5cf9 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 11 Jun 2018 09:39:01 +0200 Subject: [PATCH 038/150] add instrument property --- qcodes/instrument/parameter.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/qcodes/instrument/parameter.py b/qcodes/instrument/parameter.py index 85377f6f3fb..8451429d5e0 100644 --- a/qcodes/instrument/parameter.py +++ b/qcodes/instrument/parameter.py @@ -657,8 +657,21 @@ def set_validator(self, vals): else: raise TypeError('vals must be a Validator') + @property + def instument(self) -> Optional['InstrumentBase']: + """ + Return the first instrument that this parameter is bound to + """ + return self._instrument + @property def root_instrument(self) -> Optional['InstrumentBase']: + """ + Return the fundamental instrument that this parameter belongs too. + E.g if the parameter is bound to a channel this will return the + fundamental instrument that that channel belongs to. + + """ if self._instrument is not None: return self._instrument.root_instrument else: From 531655f7876ac5486b1297bea068a9ec596bc862 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 11 Jun 2018 09:41:48 +0200 Subject: [PATCH 039/150] use instrument property and not private member --- qcodes/dataset/measurements.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qcodes/dataset/measurements.py b/qcodes/dataset/measurements.py index 90d0ecf31d7..3e8873d83ab 100644 --- a/qcodes/dataset/measurements.py +++ b/qcodes/dataset/measurements.py @@ -422,8 +422,8 @@ def register_parameter( if isinstance(parameter, ArrayParameter): parameter = cast(ArrayParameter, parameter) spname_parts = [] - if parameter._instrument is not None: - inst_name = parameter._instrument.name + if parameter.instrument is not None: + inst_name = parameter.instrument.name if inst_name is not None: spname_parts.append(inst_name) if parameter.setpoint_names is not None: From 57b2515c327c44d8487c0e737ada36d0f75ea3b0 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 11 Jun 2018 09:57:26 +0200 Subject: [PATCH 040/150] Add test for array parameters in channels --- .../test_measurement_context_manager.py | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/qcodes/tests/dataset/test_measurement_context_manager.py b/qcodes/tests/dataset/test_measurement_context_manager.py index a35fafc55e4..d1de1c95fc7 100644 --- a/qcodes/tests/dataset/test_measurement_context_manager.py +++ b/qcodes/tests/dataset/test_measurement_context_manager.py @@ -11,7 +11,7 @@ import qcodes as qc from qcodes.dataset.measurements import Measurement from qcodes.dataset.experiment_container import new_experiment -from qcodes.tests.instrument_mocks import DummyInstrument +from qcodes.tests.instrument_mocks import DummyInstrument, DummyChannelInstrument from qcodes.dataset.param_spec import ParamSpec from qcodes.dataset.sqlite_base import connect, init_db from qcodes.instrument.parameter import ArrayParameter @@ -50,6 +50,11 @@ def DMM(): yield dmm dmm.close() +@pytest.fixture +def channel_array_instrument(): + channelarrayinstrument = DummyChannelInstrument('dummy_channel_inst') + yield channelarrayinstrument + channelarrayinstrument.close() @pytest.fixture def SpectrumAnalyzer(): @@ -707,6 +712,41 @@ def test_datasaver_arrayparams_tuples(experiment, SpectrumAnalyzer, DAC, N, M): assert datasaver.points_written == N*M +@settings(max_examples=5, deadline=None) +@given(N=hst.integers(min_value=5, max_value=500)) +def test_datasaver_array_parameters_channel(experiment, channel_array_instrument, + DAC, N): + + meas = Measurement() + + array_param = channel_array_instrument.A.dummy_array_parameter + + meas.register_parameter(array_param) + + assert len(meas.parameters) == 2 + dependency_name = 'dummy_channel_inst_ChanA_this_setpoint' + assert meas.parameters[str(array_param)].depends_on == dependency_name + assert meas.parameters[str(array_param)].type == 'numeric' + assert meas.parameters[dependency_name].type == 'numeric' + + # Now for a real measurement + + meas = Measurement() + + meas.register_parameter(DAC.ch1) + meas.register_parameter(array_param, setpoints=[DAC.ch1]) + + assert len(meas.parameters) == 3 + + M = array_param.shape[0] + + with meas.run() as datasaver: + for set_v in np.linspace(0, 0.01, N): + datasaver.add_result((DAC.ch1, set_v), + (array_param, array_param.get())) + assert datasaver.points_written == N * M + + def test_load_legacy_files_2D(experiment): location = 'fixtures/2018-01-17/#002_2D_test_15-43-14' dir = os.path.dirname(__file__) From f98ab69491d2de7198ea3d7149f4a50968074fb7 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 11 Jun 2018 09:59:23 +0200 Subject: [PATCH 041/150] Correct stupid typo --- qcodes/instrument/parameter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/instrument/parameter.py b/qcodes/instrument/parameter.py index 8451429d5e0..7395e656343 100644 --- a/qcodes/instrument/parameter.py +++ b/qcodes/instrument/parameter.py @@ -658,7 +658,7 @@ def set_validator(self, vals): raise TypeError('vals must be a Validator') @property - def instument(self) -> Optional['InstrumentBase']: + def instrument(self) -> Optional['InstrumentBase']: """ Return the first instrument that this parameter is bound to """ From b091b51b2b635b2958ffee779d2e97946588998b Mon Sep 17 00:00:00 2001 From: ThorvaldLarsen Date: Mon, 11 Jun 2018 13:07:20 +0200 Subject: [PATCH 042/150] Remove fig width limit --- qcodes/plots/qcmatplotlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/plots/qcmatplotlib.py b/qcodes/plots/qcmatplotlib.py index aefba536454..350d7dd0e69 100644 --- a/qcodes/plots/qcmatplotlib.py +++ b/qcodes/plots/qcmatplotlib.py @@ -222,7 +222,7 @@ def default_figsize(subplots): """ if not isinstance(subplots, tuple): raise TypeError('Subplots {} must be a tuple'.format(subplots)) - return (min(3 + 3 * subplots[1], 12), 1 + 3 * subplots[0]) + return (3 + 3 * subplots[1], 1 + 3 * subplots[0]) def update_plot(self): """ From d9669ba0ea4cea3a5f6e570179a813bb84065b88 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 11 Jun 2018 13:04:46 +0200 Subject: [PATCH 043/150] correcty typo in typing import --- qcodes/instrument/base.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/qcodes/instrument/base.py b/qcodes/instrument/base.py index 578ad6e8b1f..ec842e8c052 100644 --- a/qcodes/instrument/base.py +++ b/qcodes/instrument/base.py @@ -7,7 +7,7 @@ import numpy as np if TYPE_CHECKING: - from qcodes.instrumet.channel import ChannelList + from qcodes.instrument.channel import ChannelList from qcodes.utils.helpers import DelegateAttributes, strip_attrs, full_class from qcodes.utils.metadata import Metadatable from qcodes.utils.validators import Anything @@ -46,9 +46,10 @@ def __init__(self, name: str, metadata: Optional[Dict]=None, **kwargs) -> None: self.name = str(name) - self.parameters = {} # type: Dict[str, _BaseParameter] - self.functions = {} # type: Dict[str, Function] - self.submodules = {} # type: Dict[str, Union['InstrumentBase', 'ChannelList']] + self.parameters: Dict[str, _BaseParameter] = {} + self.functions: Dict[str, Function] = {} + self.submodules: Dict[str, Union['InstrumentBase', + 'ChannelList']] = {} super().__init__(**kwargs) def add_parameter(self, name: str, @@ -243,6 +244,7 @@ def print_readable_snapshot(self, update: bool=False, for submodule in self.submodules.values(): if hasattr(submodule, '_channels'): + submodule = cast('ChannelList', submodule) if submodule._snapshotable: for channel in submodule._channels: channel.print_readable_snapshot() From fac102d270e5600566000770be2197630952efb3 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 11 Jun 2018 13:13:40 +0200 Subject: [PATCH 044/150] Better typing of channels --- qcodes/instrument/channel.py | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/qcodes/instrument/channel.py b/qcodes/instrument/channel.py index 26acccc9dbb..2d78049031d 100644 --- a/qcodes/instrument/channel.py +++ b/qcodes/instrument/channel.py @@ -77,7 +77,7 @@ class MultiChannelInstrumentParameter(MultiParameter): param_name(str): Name of the multichannel parameter """ def __init__(self, - channels: Union[List, Tuple], + channels: Sequence[InstrumentChannel], param_name: str, *args, **kwargs) -> None: super().__init__(*args, **kwargs) @@ -175,15 +175,21 @@ def __init__(self, parent: Instrument, # 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. + self._channels: Sequence[InstrumentChannel] if chan_list is None: self._locked = False - self._channels: Union[List[InstrumentChannel],Tuple[InstrumentChannel, ...]] = [] + self._channels = [] else: self._locked = True self._channels = tuple(chan_list) + # At this stage mypy (0.610) is convinced that self._channels is + # None. Creating a local variable seems to resolve this + channels = cast(Tuple[InstrumentChannel, ...], self._channels) + if channels is None: + raise RuntimeError("Empty channel 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): + for channel in channels} + if not all(isinstance(chan, chan_type) for chan in channels): raise TypeError("All items in this channel list must be of " "type {}.".format(chan_type.__name__)) @@ -257,9 +263,10 @@ def append(self, obj: InstrumentChannel): ".".format(type(obj).__name__, self._chan_type.__name__)) self._channel_mapping[obj.short_name] = obj + self._channels = cast(List[InstrumentChannel], self._channels) return self._channels.append(obj) - def extend(self, objects): + def extend(self, objects: Sequence[InstrumentChannel]): """ Insert an iterable of objects into the list of channels. @@ -269,13 +276,15 @@ def extend(self, objects): """ # 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) + objects_tuple = 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): + if not all(isinstance(obj, self._chan_type) for obj in objects_tuple): raise TypeError("All items in a channel list must be of the same " "type.") - return self._channels.extend(objects) + channels = cast(List[InstrumentChannel], self._channels) + channels.extend(objects_tuple) + self._channels = channels def index(self, obj: InstrumentChannel): """ @@ -286,7 +295,7 @@ def index(self, obj: InstrumentChannel): """ return self._channels.index(obj) - def insert(self, index: int, obj: InstrumentChannel): + def insert(self, index: int, obj: InstrumentChannel) -> None: """ Insert an object into the channel list at a specific index. @@ -302,8 +311,8 @@ def insert(self, index: int, obj: InstrumentChannel): "type. Adding {} to a list of {}" ".".format(type(obj).__name__, self._chan_type.__name__)) - - return self._channels.insert(index, obj) + self._channels = cast(List[InstrumentChannel], self._channels) + self._channels.insert(index, obj) def get_validator(self): """ @@ -314,7 +323,7 @@ def get_validator(self): raise AttributeError("Cannot create a validator for an unlocked channel list") return ChannelListValidator(self) - def lock(self): + def lock(self) -> None: """ 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. @@ -461,7 +470,7 @@ def __init__(self, channel_list: ChannelList) -> None: "to create a validator") self._channel_list = channel_list - def validate(self, value, context=''): + def validate(self, value, context: str='') -> None: """ Checks to see that value is a member of the channel list referenced by this validator From d630a5a7c863b12ec8a3a86095365bf7c9b46172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luka=20Drmi=C4=87?= <13066652+kNalj@users.noreply.github.com> Date: Mon, 11 Jun 2018 13:24:37 +0200 Subject: [PATCH 045/150] Update loops.py --- qcodes/loops.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/qcodes/loops.py b/qcodes/loops.py index 406e9459880..05bd2a75fd0 100644 --- a/qcodes/loops.py +++ b/qcodes/loops.py @@ -831,6 +831,10 @@ def _run_loop(self, first_delay=0, action_indices=(), 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) From 1b964bc35fed979385d1da2d8d78648fc4293002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luka=20Drmi=C4=87?= <13066652+kNalj@users.noreply.github.com> Date: Mon, 11 Jun 2018 13:49:01 +0200 Subject: [PATCH 046/150] Update loops.py --- qcodes/loops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/loops.py b/qcodes/loops.py index 05bd2a75fd0..be087ea2ced 100644 --- a/qcodes/loops.py +++ b/qcodes/loops.py @@ -831,7 +831,7 @@ def _run_loop(self, first_delay=0, action_indices=(), tprint('loop %s: %d/%d (%.1f [s])' % ( self.sweep_values.name, i, imax, time.time() - t0), dt=self.progress_interval, tag='outerloop') - if i: + if i: tprint("Estimated finish time: %s" % ( time.asctime(time.localtime(t0 + ((time.time() - t0) * imax / i)))), dt=self.progress_interval, tag="finish") From 1a5bb0be4e9d725119f3c1672f092bcb29ebb913 Mon Sep 17 00:00:00 2001 From: ThorvaldLarsen Date: Mon, 11 Jun 2018 14:43:14 +0200 Subject: [PATCH 047/150] Allow tuple input --- qcodes/instrument/channel.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/qcodes/instrument/channel.py b/qcodes/instrument/channel.py index 26acccc9dbb..1212560806c 100644 --- a/qcodes/instrument/channel.py +++ b/qcodes/instrument/channel.py @@ -187,7 +187,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: Union[int, slice]): + def __getitem__(self, i: Union[int, slice, tuple]): """ Return either a single channel, or a new ChannelList containing only the specified channels @@ -200,6 +200,10 @@ def __getitem__(self, i: Union[int, slice]): return ChannelList(self._parent, self._name, self._chan_type, self._channels[i], multichan_paramclass=self._paramclass) + elif isinstance(i, tuple): + return ChannelList(self._parent, self._name, self._chan_type, + [self._channels[j] for j in i], + multichan_paramclass=self._paramclass) return self._channels[i] def __iter__(self): From 01a22266275eb5c842713bee1ab56c11908af9b8 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 11 Jun 2018 14:49:35 +0200 Subject: [PATCH 048/150] improve docstring sligthly --- qcodes/instrument/parameter.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/qcodes/instrument/parameter.py b/qcodes/instrument/parameter.py index 7395e656343..d237a61bad2 100644 --- a/qcodes/instrument/parameter.py +++ b/qcodes/instrument/parameter.py @@ -660,7 +660,10 @@ def set_validator(self, vals): @property def instrument(self) -> Optional['InstrumentBase']: """ - Return the first instrument that this parameter is bound to + Return the first instrument that this parameter is bound to. + E.g if this is bound to a channel it will return the channel + and not the instrument that the channel is bound too. Use + :meth:`root_instrument` to get the real instrument. """ return self._instrument @@ -669,8 +672,8 @@ def root_instrument(self) -> Optional['InstrumentBase']: """ Return the fundamental instrument that this parameter belongs too. E.g if the parameter is bound to a channel this will return the - fundamental instrument that that channel belongs to. - + fundamental instrument that that channel belongs to. Use + :meth:`instrument` to get the channel. """ if self._instrument is not None: return self._instrument.root_instrument From f6c6ed85a305dbe6c668b45f113bfb35b2cf4888 Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Mon, 11 Jun 2018 16:00:36 +0200 Subject: [PATCH 049/150] Remove whitespace --- qcodes/plots/pyqtgraph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/plots/pyqtgraph.py b/qcodes/plots/pyqtgraph.py index 05369032b1b..3a9962492e1 100644 --- a/qcodes/plots/pyqtgraph.py +++ b/qcodes/plots/pyqtgraph.py @@ -497,7 +497,7 @@ def _repr_png_(self): buffer.open(self.rpg.QtCore.QIODevice.ReadWrite) image.save(buffer, 'PNG') buffer.close() - + if hasattr(byte_array, '_getValue'): return bytes(byte_array._getValue()) else: From 37d2a19d3ac9dc9212adbe95a00b66cd427a1eb1 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 11 Jun 2018 16:09:02 +0200 Subject: [PATCH 050/150] add tests for slice and tuple access --- qcodes/tests/test_channels.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/qcodes/tests/test_channels.py b/qcodes/tests/test_channels.py index dd088ad2a0b..f8347ad6d5d 100644 --- a/qcodes/tests/test_channels.py +++ b/qcodes/tests/test_channels.py @@ -4,6 +4,7 @@ from qcodes.tests.instrument_mocks import DummyChannelInstrument, DummyChannel from qcodes.utils.validators import Numbers from qcodes.instrument.parameter import Parameter +from qcodes.instrument.channel import ChannelList from hypothesis import given, settings import hypothesis.strategies as hst @@ -124,6 +125,35 @@ def test_combine_channels(self, setpoints): expected = tuple(setpoints[0:2] + [0, 0] + setpoints[2:]) self.assertEquals(self.instrument.channels.temperature(), expected) + @given(start=hst.integers(-8,7), stop=hst.integers(-8,7), step=hst.integers(1,7)) + def test_access_channels_by_slice(self, start, stop, step): + names = ('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H') + channels = tuple(DummyChannel(self.instrument, + 'Chan'+name, name) for name in names) + chlist = ChannelList(self.instrument, 'channels', + DummyChannel, channels) + if stop < start: + step = -step + myslice = slice(start, stop, step) + mychans = chlist[myslice] + expected_channels = names[myslice] + for chan, exp_chan in zip(mychans, expected_channels): + assert chan.name == f'testchanneldummy_Chan{exp_chan}' + + + @given(myindexs=hst.lists(elements=hst.integers(-8,7), min_size=1)) + def test_access_channels_by_tuple(self, myindexs): + names = ('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H') + mytuple = tuple(myindexs) + channels = tuple(DummyChannel(self.instrument, + 'Chan'+name, name) for name in names) + chlist = ChannelList(self.instrument, 'channels', + DummyChannel, channels) + + mychans = chlist[mytuple] + for chan, chanindex in zip(mychans, mytuple): + assert chan.name == f'testchanneldummy_Chan{names[chanindex]}' + class TestChannelsLoop(TestCase): From c607f2a4dea150aab12de431b64e72b7c96c8b35 Mon Sep 17 00:00:00 2001 From: sohail chatoor Date: Tue, 12 Jun 2018 10:16:22 +0200 Subject: [PATCH 051/150] normal set command does not use context manager. Use set_to to use context manager --- qcodes/instrument/parameter.py | 20 ++++++++++++++++---- qcodes/tests/test_parameter.py | 4 ++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/qcodes/instrument/parameter.py b/qcodes/instrument/parameter.py index 719cf5cba0b..8e785ba3b12 100644 --- a/qcodes/instrument/parameter.py +++ b/qcodes/instrument/parameter.py @@ -408,8 +408,6 @@ def set_wrapper(value, **kwargs): try: self.validate(value) - set_context_manager = _SetParamContext(self) - # In some cases intermediate sweep values must be used. # Unless `self.step` is defined, get_sweep_values will return # a list containing only `value`. @@ -475,8 +473,6 @@ def set_wrapper(value, **kwargs): # Sleep until total time is larger than self.post_delay time.sleep(self.post_delay - t_elapsed) - return set_context_manager - except Exception as e: e.args = e.args + ('setting {} to {}'.format(self, value),) raise e @@ -708,6 +704,22 @@ def root_instrument(self) -> Optional['InstrumentBase']: else: return None + def set_to(self, value): + """ + Use a context manager to temporarily set the value of a parameter to + a value. Example: + + >>> from qcodes import Parameter + >>> p = Parameter("p", set_cmd=None, get_cmd=None) + >>> with p.set_to(3): + ... print(f"p value in with block {p.get()}") + >>> print(f"p value outside with block {p.get()}") + """ + context_manager = _SetParamContext(self) + self.set(value) + return context_manager + + class Parameter(_BaseParameter): """ A parameter that represents a single degree of freedom. diff --git a/qcodes/tests/test_parameter.py b/qcodes/tests/test_parameter.py index 30a8f00b984..4092c5a779c 100644 --- a/qcodes/tests/test_parameter.py +++ b/qcodes/tests/test_parameter.py @@ -919,13 +919,13 @@ def tearDown(self): del self.instrument def test_none_value(self): - with self.instrument.a.set(3): + with self.instrument.a.set_to(3): assert self.instrument.a.get() == 3 assert self.instrument.a.get() is None def test_context(self): self.instrument.a.set(2) - with self.instrument.a.set(3): + with self.instrument.a.set_to(3): assert self.instrument.a.get() == 3 assert self.instrument.a.get() == 2 From d8c8553211511fa943f7dcedb786bc58967ab4a0 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 13 Jun 2018 09:59:52 +0200 Subject: [PATCH 052/150] add short name to parameter --- qcodes/instrument/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qcodes/instrument/base.py b/qcodes/instrument/base.py index 578ad6e8b1f..e8ff4cd33b5 100644 --- a/qcodes/instrument/base.py +++ b/qcodes/instrument/base.py @@ -45,6 +45,7 @@ class InstrumentBase(Metadatable, DelegateAttributes): def __init__(self, name: str, metadata: Optional[Dict]=None, **kwargs) -> None: self.name = str(name) + self.short_name = str(name) self.parameters = {} # type: Dict[str, _BaseParameter] self.functions = {} # type: Dict[str, Function] From 298d8663fffe390065f3dc966d990556abc366fb Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 13 Jun 2018 13:47:00 +0200 Subject: [PATCH 053/150] Add full_name to both channel and instrument --- qcodes/instrument/base.py | 1 + qcodes/instrument/channel.py | 1 + 2 files changed, 2 insertions(+) diff --git a/qcodes/instrument/base.py b/qcodes/instrument/base.py index e8ff4cd33b5..7a956d958f5 100644 --- a/qcodes/instrument/base.py +++ b/qcodes/instrument/base.py @@ -46,6 +46,7 @@ def __init__(self, name: str, metadata: Optional[Dict]=None, **kwargs) -> None: self.name = str(name) self.short_name = str(name) + self.full_name = str(name) self.parameters = {} # type: Dict[str, _BaseParameter] self.functions = {} # type: Dict[str, Function] diff --git a/qcodes/instrument/channel.py b/qcodes/instrument/channel.py index 1212560806c..110c83136eb 100644 --- a/qcodes/instrument/channel.py +++ b/qcodes/instrument/channel.py @@ -35,6 +35,7 @@ def __init__(self, parent: Instrument, name: str, **kwargs) -> None: self.name = "{}_{}".format(parent.name, str(name)) self.short_name = str(name) + self.full_name = self.name self._meta_attrs = ['name'] self._parent = parent From 1f8f7c6bba60152d90662cf9df2c7ff26bf920d0 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 13 Jun 2018 13:47:18 +0200 Subject: [PATCH 054/150] add short_name to parameter --- qcodes/instrument/parameter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qcodes/instrument/parameter.py b/qcodes/instrument/parameter.py index 8e785ba3b12..ad0b0cf2919 100644 --- a/qcodes/instrument/parameter.py +++ b/qcodes/instrument/parameter.py @@ -200,6 +200,7 @@ def __init__(self, name: str, delay: Optional[Union[int, float]]=None) -> None: super().__init__(metadata) self.name = str(name) + self.short_name = str(name) self._instrument = instrument self._snapshot_get = snapshot_get self._snapshot_value = snapshot_value From 8b74e2ac33c015ebba7b712b4a4a9196631c158c Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 13 Jun 2018 13:47:38 +0200 Subject: [PATCH 055/150] add consistent names test --- qcodes/tests/test_channels.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/qcodes/tests/test_channels.py b/qcodes/tests/test_channels.py index f8347ad6d5d..a7f77b82c80 100644 --- a/qcodes/tests/test_channels.py +++ b/qcodes/tests/test_channels.py @@ -155,6 +155,41 @@ def test_access_channels_by_tuple(self, myindexs): assert chan.name == f'testchanneldummy_Chan{names[chanindex]}' + def test_names(self): + ex_inst_name = 'testchanneldummy' + for channel in self.instrument.channels: + sub_channel = DummyChannel(channel, 'subchannel', 'subchannel') + channel.add_submodule('somesubchannel', sub_channel) + assert self.instrument.name == ex_inst_name + assert self.instrument.full_name == ex_inst_name + assert self.instrument.short_name == ex_inst_name + # Parameters directly on instrument + assert self.instrument.IDN.name == 'IDN' + assert self.instrument.IDN.full_name == f"{ex_inst_name}_IDN" + for chan, name in zip(self.instrument.channels, + ['A', 'B', 'C', 'D', 'E', 'F']): + ex_chan_name = f"Chan{name}" + ex_chan_full_name = f"{ex_inst_name}_{ex_chan_name}" + assert chan.short_name == ex_chan_name + assert chan.name == ex_chan_full_name + assert chan.full_name == ex_chan_full_name + + ex_param_name = 'temperature' + assert chan.temperature.name == ex_param_name + assert chan.temperature.full_name == f'{ex_chan_full_name}_{ex_param_name}' + assert chan.temperature.short_name == ex_param_name + + ex_subchan_name = f"subchannel" + ex_subchan_full_name = f"{ex_chan_full_name}_{ex_subchan_name}" + + assert chan.somesubchannel.short_name == ex_subchan_name + assert chan.somesubchannel.name == ex_subchan_full_name + assert chan.somesubchannel.full_name == ex_subchan_full_name + + assert chan.somesubchannel.temperature.name == ex_param_name + assert chan.somesubchannel.temperature.full_name == f'{ex_subchan_full_name}_{ex_param_name}' + assert chan.somesubchannel.temperature.short_name == ex_param_name + class TestChannelsLoop(TestCase): def setUp(self): From 02b3bed8fc2315726fefd3170ded8f2a3a183577 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 13 Jun 2018 10:10:08 +0200 Subject: [PATCH 056/150] add parent to channels --- qcodes/instrument/base.py | 8 ++++++++ qcodes/instrument/channel.py | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/qcodes/instrument/base.py b/qcodes/instrument/base.py index 7a956d958f5..cc28f76b032 100644 --- a/qcodes/instrument/base.py +++ b/qcodes/instrument/base.py @@ -251,6 +251,14 @@ def print_readable_snapshot(self, update: bool=False, else: submodule.print_readable_snapshot(update, max_chars) + @property + def parent(self): + """ + Returns the parent instrument. By default this is None + Any SubInstrument should subclass this to return the parent instrument. + """ + return None + @property def root_instrument(self) -> 'InstrumentBase': return self diff --git a/qcodes/instrument/channel.py b/qcodes/instrument/channel.py index 110c83136eb..001920ebbbb 100644 --- a/qcodes/instrument/channel.py +++ b/qcodes/instrument/channel.py @@ -60,6 +60,10 @@ def ask(self, cmd): def ask_raw(self, cmd): return self._parent.ask_raw(cmd) + @property + def parent(self) -> InstrumentBase: + return self._parent + @property def root_instrument(self) -> InstrumentBase: return self._parent.root_instrument From b652a30fe3455a9c46252e37505f42dbce9b73ac Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 13 Jun 2018 10:11:39 +0200 Subject: [PATCH 057/150] add names parts function --- qcodes/instrument/parameter.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/qcodes/instrument/parameter.py b/qcodes/instrument/parameter.py index ad0b0cf2919..ffc6473a762 100644 --- a/qcodes/instrument/parameter.py +++ b/qcodes/instrument/parameter.py @@ -720,6 +720,21 @@ def set_to(self, value): self.set(value) return context_manager + @property + def name_parts(self) -> List[str]: + name_parts = [self.name] + parent_inst = self._instrument + while parent_inst is not None: + name_parts.append(parent_inst.short_name) + parent_inst = parent_inst.parent + name_parts.reverse() + return name_parts + + @property + def qualified_name(self): + return "_".join(self.name_parts[1:]) + # TODO what should this be called + class Parameter(_BaseParameter): """ From 841cf986f93396cfc91bf6f7873eae4fe907caaa Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 13 Jun 2018 14:06:42 +0200 Subject: [PATCH 058/150] add name_parts function to channel and inst --- qcodes/instrument/base.py | 5 +++++ qcodes/instrument/channel.py | 10 ++++++++++ qcodes/instrument/parameter.py | 7 +------ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/qcodes/instrument/base.py b/qcodes/instrument/base.py index cc28f76b032..17d95ae05fb 100644 --- a/qcodes/instrument/base.py +++ b/qcodes/instrument/base.py @@ -262,6 +262,11 @@ def parent(self): @property def root_instrument(self) -> 'InstrumentBase': return self + + @property + def name_parts(self) -> List[str]: + name_parts = [self.short_name] + return name_parts # # shortcuts to parameters & setters & getters # # diff --git a/qcodes/instrument/channel.py b/qcodes/instrument/channel.py index 001920ebbbb..e2f3f76fc82 100644 --- a/qcodes/instrument/channel.py +++ b/qcodes/instrument/channel.py @@ -68,6 +68,16 @@ def parent(self) -> InstrumentBase: def root_instrument(self) -> InstrumentBase: return self._parent.root_instrument + @property + def name_parts(self) -> List[str]: + name_parts = [self.short_name] + parent_inst = self._parent + while parent_inst is not None: + name_parts.append(parent_inst.short_name) + parent_inst = parent_inst.parent + name_parts.reverse() + return name_parts + class MultiChannelInstrumentParameter(MultiParameter): """ Parameter to get or set multiple channels simultaneously. diff --git a/qcodes/instrument/parameter.py b/qcodes/instrument/parameter.py index ffc6473a762..b595a452754 100644 --- a/qcodes/instrument/parameter.py +++ b/qcodes/instrument/parameter.py @@ -722,7 +722,7 @@ def set_to(self, value): @property def name_parts(self) -> List[str]: - name_parts = [self.name] + name_parts = [self.short_name] parent_inst = self._instrument while parent_inst is not None: name_parts.append(parent_inst.short_name) @@ -730,11 +730,6 @@ def name_parts(self) -> List[str]: name_parts.reverse() return name_parts - @property - def qualified_name(self): - return "_".join(self.name_parts[1:]) - # TODO what should this be called - class Parameter(_BaseParameter): """ From 011459a8e61c8de274b0b56586983ae552c44e31 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 13 Jun 2018 14:06:56 +0200 Subject: [PATCH 059/150] test name_parts too --- qcodes/tests/test_channels.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/qcodes/tests/test_channels.py b/qcodes/tests/test_channels.py index a7f77b82c80..c34b9e3b14e 100644 --- a/qcodes/tests/test_channels.py +++ b/qcodes/tests/test_channels.py @@ -163,6 +163,8 @@ def test_names(self): assert self.instrument.name == ex_inst_name assert self.instrument.full_name == ex_inst_name assert self.instrument.short_name == ex_inst_name + assert self.instrument.name_parts == [ex_inst_name] + # Parameters directly on instrument assert self.instrument.IDN.name == 'IDN' assert self.instrument.IDN.full_name == f"{ex_inst_name}_IDN" @@ -170,14 +172,18 @@ def test_names(self): ['A', 'B', 'C', 'D', 'E', 'F']): ex_chan_name = f"Chan{name}" ex_chan_full_name = f"{ex_inst_name}_{ex_chan_name}" + assert chan.short_name == ex_chan_name assert chan.name == ex_chan_full_name assert chan.full_name == ex_chan_full_name + assert chan.name_parts == [ex_inst_name, ex_chan_name] ex_param_name = 'temperature' assert chan.temperature.name == ex_param_name assert chan.temperature.full_name == f'{ex_chan_full_name}_{ex_param_name}' assert chan.temperature.short_name == ex_param_name + assert chan.temperature.name_parts == [ex_inst_name, ex_chan_name, + ex_param_name] ex_subchan_name = f"subchannel" ex_subchan_full_name = f"{ex_chan_full_name}_{ex_subchan_name}" @@ -185,11 +191,17 @@ def test_names(self): assert chan.somesubchannel.short_name == ex_subchan_name assert chan.somesubchannel.name == ex_subchan_full_name assert chan.somesubchannel.full_name == ex_subchan_full_name + assert chan.somesubchannel.name_parts == [ex_inst_name, + ex_chan_name, + ex_subchan_name] assert chan.somesubchannel.temperature.name == ex_param_name assert chan.somesubchannel.temperature.full_name == f'{ex_subchan_full_name}_{ex_param_name}' assert chan.somesubchannel.temperature.short_name == ex_param_name - + assert chan.somesubchannel.temperature.name_parts == [ex_inst_name, + ex_chan_name, + ex_subchan_name, + ex_param_name] class TestChannelsLoop(TestCase): def setUp(self): From 43f57a43130772e5fa6d1b87790beec0baf557dc Mon Sep 17 00:00:00 2001 From: Dominik Vogel Date: Wed, 13 Jun 2018 14:27:40 +0200 Subject: [PATCH 060/150] generate_awg_file marked as deprecated --- qcodes/instrument_drivers/tektronix/AWG5014.py | 18 ++++++++++++++---- qcodes/utils/deprecate.py | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 qcodes/utils/deprecate.py diff --git a/qcodes/instrument_drivers/tektronix/AWG5014.py b/qcodes/instrument_drivers/tektronix/AWG5014.py index a5fa5771ff3..9b7efa642df 100644 --- a/qcodes/instrument_drivers/tektronix/AWG5014.py +++ b/qcodes/instrument_drivers/tektronix/AWG5014.py @@ -7,8 +7,11 @@ from time import sleep, localtime from io import BytesIO +from functools import wraps, WRAPPER_ASSIGNMENTS + from qcodes import VisaInstrument, validators as vals +from qcodes.utils.deprecate import deprecate from pyvisa.errors import VisaIOError @@ -903,7 +906,7 @@ def generate_channel_cfg(self): been changed from their default value and put them in a dictionary that can easily be written into an awg file, so as to prevent said awg file from falling back to default values. - (See self.generate_awg_file and self.AWG_FILE_FORMAT_CHANNEL) + (See self.make_awg_file and self.AWG_FILE_FORMAT_CHANNEL) NOTE: This only works for settings changed via the corresponding QCoDeS parameter. @@ -1022,7 +1025,8 @@ def mrkdeltrans(x): return AWG_channel_cfg - def generate_awg_file(self, + + def _generate_awg_file(self, packed_waveforms, wfname_l, nrep, trig_wait, goto_state, jump_to, channel_cfg, sequence_cfg=None, @@ -1163,6 +1167,11 @@ def generate_awg_file(self, wf_record_str.getvalue() + seq_record_str.getvalue()) return awg_file + @deprecate(alternative='make_awg_file, _generate_awg_file') + @wraps(_generate_awg_file, assigned=(v for v in WRAPPER_ASSIGNMENTS if v != '__name__')) + def generate_awg_file(self, *args, **kwargs): + self._generate_awg_file(*args, **kwargs) + def send_awg_file(self, filename, awg_file, verbose=False): """ Writes an .awg-file onto the disk of the AWG. @@ -1172,7 +1181,7 @@ def send_awg_file(self, filename, awg_file, verbose=False): filename (str): The name that the file will get on the AWG. awg_file (bytes): A byte sequence containing the awg_file. - Usually the output of self.generate_awg_file. + Usually the output of self.make_awg_file. verbose (bool): A boolean to allow/suppress printing of messages about the status of the filw writing. Default: False. """ @@ -1274,7 +1283,7 @@ def make_awg_file(self, waveforms, m1s, m2s, channel_cfg = {} - return self.generate_awg_file( + return self._generate_awg_file( packed_wfs, wavenamearray, nreps, trig_waits, goto_states, jump_tos, channel_cfg, preservechannelsettings=preservechannelsettings) @@ -1640,3 +1649,4 @@ def clear_message_queue(self, verbose=False): except VisaIOError: gotexception = True self.visa_handle.timeout = original_timeout + diff --git a/qcodes/utils/deprecate.py b/qcodes/utils/deprecate.py new file mode 100644 index 00000000000..46758512b54 --- /dev/null +++ b/qcodes/utils/deprecate.py @@ -0,0 +1,18 @@ +from functools import wraps +import warnings + +def deprecate(reason=None, alternative=None): + def actual_decorator(func): + @wraps(func) + def decorated_func(*args, **kwargs): + msg = f'The function \"{func.__name__}\" is deprecated' + if reason is not None: + msg += f', because {reason}' + else: + msg += '.' + if alternative is not None: + msg += f' Use \"{alternative}\" as an alternative.' + warnings.warn(msg, DeprecationWarning, stacklevel=2) + return func(*args, **kwargs) + return decorated_func + return actual_decorator From 5895276f9b355368f9e0f3b84b024934a00a6680 Mon Sep 17 00:00:00 2001 From: Dominik Vogel Date: Wed, 13 Jun 2018 14:33:41 +0200 Subject: [PATCH 061/150] cleanup of whitespace --- qcodes/instrument_drivers/tektronix/AWG5014.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qcodes/instrument_drivers/tektronix/AWG5014.py b/qcodes/instrument_drivers/tektronix/AWG5014.py index 9b7efa642df..7b6a608ef88 100644 --- a/qcodes/instrument_drivers/tektronix/AWG5014.py +++ b/qcodes/instrument_drivers/tektronix/AWG5014.py @@ -1649,4 +1649,3 @@ def clear_message_queue(self, verbose=False): except VisaIOError: gotexception = True self.visa_handle.timeout = original_timeout - From b15d80cfa34fb7676989788d740ad2fe16e93ece Mon Sep 17 00:00:00 2001 From: Dominik Vogel Date: Wed, 13 Jun 2018 14:56:54 +0200 Subject: [PATCH 062/150] made generator a tuple to make mypy happier --- qcodes/instrument_drivers/tektronix/AWG5014.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/instrument_drivers/tektronix/AWG5014.py b/qcodes/instrument_drivers/tektronix/AWG5014.py index 7b6a608ef88..23826e8fcd8 100644 --- a/qcodes/instrument_drivers/tektronix/AWG5014.py +++ b/qcodes/instrument_drivers/tektronix/AWG5014.py @@ -1168,7 +1168,7 @@ def _generate_awg_file(self, return awg_file @deprecate(alternative='make_awg_file, _generate_awg_file') - @wraps(_generate_awg_file, assigned=(v for v in WRAPPER_ASSIGNMENTS if v != '__name__')) + @wraps(_generate_awg_file, assigned=tuple(v for v in WRAPPER_ASSIGNMENTS if v != '__name__')) def generate_awg_file(self, *args, **kwargs): self._generate_awg_file(*args, **kwargs) From 5f655abf4378260ec1f83922f470ccb2d09a0029 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 13 Jun 2018 15:21:27 +0200 Subject: [PATCH 063/150] simplify logig for nameparts --- qcodes/instrument/channel.py | 8 ++------ qcodes/instrument/parameter.py | 12 ++++++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/qcodes/instrument/channel.py b/qcodes/instrument/channel.py index e2f3f76fc82..21194beb4b8 100644 --- a/qcodes/instrument/channel.py +++ b/qcodes/instrument/channel.py @@ -70,12 +70,8 @@ def root_instrument(self) -> InstrumentBase: @property def name_parts(self) -> List[str]: - name_parts = [self.short_name] - parent_inst = self._parent - while parent_inst is not None: - name_parts.append(parent_inst.short_name) - parent_inst = parent_inst.parent - name_parts.reverse() + name_parts = self._parent.name_parts + name_parts.append(self.short_name) return name_parts class MultiChannelInstrumentParameter(MultiParameter): diff --git a/qcodes/instrument/parameter.py b/qcodes/instrument/parameter.py index b595a452754..413b5856a41 100644 --- a/qcodes/instrument/parameter.py +++ b/qcodes/instrument/parameter.py @@ -722,12 +722,12 @@ def set_to(self, value): @property def name_parts(self) -> List[str]: - name_parts = [self.short_name] - parent_inst = self._instrument - while parent_inst is not None: - name_parts.append(parent_inst.short_name) - parent_inst = parent_inst.parent - name_parts.reverse() + if self.instrument is not None: + name_parts = self.instrument.name_parts + else: + name_parts = [] + + name_parts.append(self.short_name) return name_parts From 500555464de841d46e9d521416814f8fe9195716 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 13 Jun 2018 15:31:47 +0200 Subject: [PATCH 064/150] generate full_name from name_parts --- qcodes/instrument/base.py | 5 ++++- qcodes/instrument/channel.py | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/qcodes/instrument/base.py b/qcodes/instrument/base.py index 17d95ae05fb..a2cc7db1ddd 100644 --- a/qcodes/instrument/base.py +++ b/qcodes/instrument/base.py @@ -46,7 +46,6 @@ def __init__(self, name: str, metadata: Optional[Dict]=None, **kwargs) -> None: self.name = str(name) self.short_name = str(name) - self.full_name = str(name) self.parameters = {} # type: Dict[str, _BaseParameter] self.functions = {} # type: Dict[str, Function] @@ -267,6 +266,10 @@ def root_instrument(self) -> 'InstrumentBase': def name_parts(self) -> List[str]: name_parts = [self.short_name] return name_parts + + @property + def full_name(self): + return "_".join(self.name_parts) # # shortcuts to parameters & setters & getters # # diff --git a/qcodes/instrument/channel.py b/qcodes/instrument/channel.py index 21194beb4b8..af65b7cd6d2 100644 --- a/qcodes/instrument/channel.py +++ b/qcodes/instrument/channel.py @@ -35,7 +35,6 @@ def __init__(self, parent: Instrument, name: str, **kwargs) -> None: self.name = "{}_{}".format(parent.name, str(name)) self.short_name = str(name) - self.full_name = self.name self._meta_attrs = ['name'] self._parent = parent From 5afb9be25c618e8925eaae34cfaf764ec2eeacff Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 13 Jun 2018 15:34:43 +0200 Subject: [PATCH 065/150] Use name_parts in parameter too --- qcodes/instrument/parameter.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/qcodes/instrument/parameter.py b/qcodes/instrument/parameter.py index 413b5856a41..12ddf0e3345 100644 --- a/qcodes/instrument/parameter.py +++ b/qcodes/instrument/parameter.py @@ -661,12 +661,7 @@ def inter_delay(self, inter_delay): # Deprecated @property def full_name(self): -# This can fully be replaced by str(parameter) in the future we -# may want to deprecate this but the current dataset makes heavy use -# of it in more complicated ways so keep it for now. -# warnings.warn('Attribute `full_name` is deprecated, please use ' -# 'str(parameter)') - return str(self) + return "_".join(self.name_parts) def set_validator(self, vals): """ From bdda9c59c5ffb3a2f5f9e111d2eb9b3f9317fd82 Mon Sep 17 00:00:00 2001 From: Dominik Vogel Date: Wed, 13 Jun 2018 16:12:42 +0200 Subject: [PATCH 066/150] made pack waveform private --- qcodes/instrument_drivers/tektronix/AWG5014.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/qcodes/instrument_drivers/tektronix/AWG5014.py b/qcodes/instrument_drivers/tektronix/AWG5014.py index 23826e8fcd8..79e84541c06 100644 --- a/qcodes/instrument_drivers/tektronix/AWG5014.py +++ b/qcodes/instrument_drivers/tektronix/AWG5014.py @@ -1435,7 +1435,12 @@ def get_error(self): """ return self.ask('SYSTEM:ERRor:NEXT?') - def pack_waveform(self, wf, m1, m2): + @deprecate(reason='this function is for private use only.') + @wraps(_pack_waveform, assigned=tuple(v for v in WRAPPER_ASSIGNMENTS if v != '__name__')) + def pack_waveform(self, *args, **kwargs): + self._pack_waveform(*args, **kwargs) + + def _pack_waveform(self, wf, m1, m2): """ Converts/packs a waveform and two markers into a 16-bit format according to the AWG Integer format specification. From b6c77e7ba861fbb2968f87f5ee8fd2de3b157336 Mon Sep 17 00:00:00 2001 From: Dominik Vogel Date: Wed, 13 Jun 2018 16:30:21 +0200 Subject: [PATCH 067/150] added check for tuple --- 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 1839e6336e9..fb7c5c475a5 100644 --- a/qcodes/instrument/channel.py +++ b/qcodes/instrument/channel.py @@ -265,11 +265,11 @@ def remove(self, obj: InstrumentChannel): Args: obj: Channel to remove from the list. """ - if self._locked: + if isinstance(self._channels, tuple) or self._locked: raise AttributeError("Cannot remove from a locked channel list") - - self._channels.remove(obj) - self._channel_mapping.pop(obj.short_name) + else: + self._channels.remove(obj) + self._channel_mapping.pop(obj.short_name) def extend(self, objects): """ From 5b82ef13443e451cb2092c84bf2a87dc214f209a Mon Sep 17 00:00:00 2001 From: Dominik Vogel Date: Thu, 14 Jun 2018 10:27:43 +0200 Subject: [PATCH 068/150] added type cast --- qcodes/instrument/channel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qcodes/instrument/channel.py b/qcodes/instrument/channel.py index 06a590425e2..a5491ae9d1e 100644 --- a/qcodes/instrument/channel.py +++ b/qcodes/instrument/channel.py @@ -282,6 +282,7 @@ def remove(self, obj: InstrumentChannel): if isinstance(self._channels, tuple) or self._locked: raise AttributeError("Cannot remove from a locked channel list") else: + self._channels = cast(List[InstrumentChannel], self._channels) self._channels.remove(obj) self._channel_mapping.pop(obj.short_name) From 8ec4ce0631a185812b5daef3b8d14c18e67759ee Mon Sep 17 00:00:00 2001 From: Dominik Vogel Date: Thu, 14 Jun 2018 10:32:45 +0200 Subject: [PATCH 069/150] Revert "Merge branch 'master' of https://github.com/QCoDeS/Qcodes into feat/remove_channels_from_channellist" This reverts commit 354c034a1e9cccab2696686e02d8262de1d5e32e, reversing changes made to 416fda49380e5e80ea7df4cf1279eff1cf6ba983. --- qcodes/instrument/channel.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/qcodes/instrument/channel.py b/qcodes/instrument/channel.py index 06a590425e2..af65b7cd6d2 100644 --- a/qcodes/instrument/channel.py +++ b/qcodes/instrument/channel.py @@ -273,18 +273,6 @@ def append(self, obj: InstrumentChannel): self._channel_mapping[obj.short_name] = obj return self._channels.append(obj) - def remove(self, obj: InstrumentChannel): - """ - Removes obj from channellist if not locked. - Args: - obj: Channel to remove from the list. - """ - if isinstance(self._channels, tuple) or self._locked: - raise AttributeError("Cannot remove from a locked channel list") - else: - self._channels.remove(obj) - self._channel_mapping.pop(obj.short_name) - def extend(self, objects): """ Insert an iterable of objects into the list of channels. From 70d243e1eaf846f2b5310493b44bce37e712aaee Mon Sep 17 00:00:00 2001 From: dpfranke Date: Thu, 14 Jun 2018 10:57:09 +0200 Subject: [PATCH 070/150] bug fix active_channels wasn't returning the correct channels --- qcodes/instrument_drivers/Spectrum/M4i.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qcodes/instrument_drivers/Spectrum/M4i.py b/qcodes/instrument_drivers/Spectrum/M4i.py index e9715be247d..fb97a7535ee 100644 --- a/qcodes/instrument_drivers/Spectrum/M4i.py +++ b/qcodes/instrument_drivers/Spectrum/M4i.py @@ -592,8 +592,8 @@ def _set_compensation(self, i, value): def active_channels(self): """ Return a list with the indices of the active channels """ - x = bin(self.enable_channels())[2:] - return [i for i in range(len(x)) if x[i]] + x = bin(self.enable_channels())[2:][::-1] + return [i for i in range(len(x)) if int(x[i])] def get_idn(self): return dict(zip(('vendor', 'model', 'serial', 'firmware'), ('Spectrum_GMBH', szTypeToName(self.get_card_type()), self.serial_number(), ' '))) From b05c98966fc89f70374af17f1a768f1b8e5ce72a Mon Sep 17 00:00:00 2001 From: ThorvaldLarsen Date: Thu, 14 Jun 2018 13:13:20 +0200 Subject: [PATCH 071/150] Change loglevel for send/recv in ip.py --- qcodes/instrument/ip.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qcodes/instrument/ip.py b/qcodes/instrument/ip.py index 9de2118ce64..b1b10ef7772 100644 --- a/qcodes/instrument/ip.py +++ b/qcodes/instrument/ip.py @@ -145,12 +145,12 @@ def set_terminator(self, terminator): def _send(self, cmd): data = cmd + self._terminator - log.info(f"Writing {data} to instrument {self.name}") + log.debug(f"Writing {data} to instrument {self.name}") self._socket.sendall(data.encode()) def _recv(self): result = self._socket.recv(self._buffer_size) - log.info(f"Got {result} from instrument {self.name}") + log.debug(f"Got {result} from instrument {self.name}") if result == b'': log.warning("Got empty response from Socket recv() " "Connection broken.") From 25272af063dce85a92d4d03d199954cd4e2c5b90 Mon Sep 17 00:00:00 2001 From: Dominik Vogel Date: Thu, 14 Jun 2018 15:25:58 +0200 Subject: [PATCH 072/150] added extensive testing --- qcodes/tests/test_channels.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/qcodes/tests/test_channels.py b/qcodes/tests/test_channels.py index c34b9e3b14e..241f00549b2 100644 --- a/qcodes/tests/test_channels.py +++ b/qcodes/tests/test_channels.py @@ -107,6 +107,37 @@ def test_insert_channel(self): self.instrument.channels.insert(2, channel) self.assertEqual(len(self.instrument.channels), n_channels + 1) + def test_remove_channel(self): + channels = self.instrument.channels + chanA = self.instrument.A + original_length = len(channels.temperature()) + channels.remove(chanA) + with self.assertRaises(AttributeError): + getattr(channels, chanA.short_name) + self.assertEqual(len(channels), original_length-1) + self.assertEqual(len(channels.temperature()), original_length-1) + + def test_remove_locked_channel(self): + channels = self.instrument.channels + chanA = self.instrument.A + channels.lock() + with self.assertRaises(AttributeError): + channels.remove(chanA) + + def test_remove_tupled_channel(self): + channel_tuple = tuple( + DummyChannel(self.instrument, f'Chan{C}', C) + for C in ('A', 'B', 'C', 'D', 'E', 'F') + ) + channels = ChannelList(self.instrument, + "TempSensorsTuple", + DummyChannel, + channel_tuple, + snapshotable=False) + chanA = channels.ChanA + with self.assertRaises(AttributeError): + channels.remove(chanA) + @given(setpoints=hst.lists(hst.floats(0, 300), min_size=4, max_size=4)) def test_combine_channels(self, setpoints): self.assertEqual(len(self.instrument.channels), 6) From 483a433d0db20adf499bb4905d62a634c24c2f86 Mon Sep 17 00:00:00 2001 From: sohailc Date: Thu, 14 Jun 2018 15:28:15 +0200 Subject: [PATCH 073/150] - add caprture_one_sample_per_trigger that captures a given number of samples per each trigger signal; a callable argument starts the trigger pulses train - add start_capture_at_trigger that captures a given number of samples once it receives a trigger signal - add parameter for the source of trigger signal - add parameter for the source of reference signal --- .../stanford_research/SR86x.py | 114 ++++++++++++++++-- 1 file changed, 101 insertions(+), 13 deletions(-) diff --git a/qcodes/instrument_drivers/stanford_research/SR86x.py b/qcodes/instrument_drivers/stanford_research/SR86x.py index 766c245c6fd..2851b5604af 100644 --- a/qcodes/instrument_drivers/stanford_research/SR86x.py +++ b/qcodes/instrument_drivers/stanford_research/SR86x.py @@ -1,7 +1,7 @@ import numpy as np import time import logging -from typing import Optional, Sequence, Dict +from typing import Optional, Sequence, Dict, Callable from qcodes import VisaInstrument from qcodes.instrument.channel import InstrumentChannel @@ -124,7 +124,8 @@ def __init__(self, parent: 'SR86x', name: str) ->None: "count_capture_bytes", label="capture bytes", get_cmd="CAPTUREBYTES?", - unit="B" + unit="B", + get_parser=int ) self.add_parameter( @@ -221,6 +222,23 @@ def stop_capture(self): """ self.write("CAPTURESTOP") + def _calc_capture_size_in_kb(self, sample_count: int) ->int: + """ + Given the number of samples to capture and the capture configuration, calculate the number of kb + """ + capture_variables = self.capture_config().split(",") + n_variables = len(capture_variables) + + total_size_in_kb = int(np.ceil(n_variables * sample_count * self.bytes_per_sample / 1024)) + # The number of kb always needs to be even + if total_size_in_kb % 2 == 1: + total_size_in_kb += 1 + + if total_size_in_kb > 64: + raise ValueError("Number of samples specified is larger then the buffer size") + + return total_size_in_kb + def get_capture_data(self, sample_count: int) -> dict: """ Read capture data from the buffer. @@ -232,23 +250,20 @@ def get_capture_data(self, sample_count: int) -> dict: data (dict): The keys in the dictionary is the variables we have captures. For instance, if before the capture we specify 'capture_config("X,Y")', then the keys will be "X" and "Y". """ - capture_variables = self.capture_config().split(",") - n_variables = len(capture_variables) - - total_size_in_kb = int(np.ceil(n_variables * sample_count * self.bytes_per_sample / 1024)) - # We will samples one kb more then strictly speaking required and trim the data length to the requested - # sample count. In this way, we are always sure that the user requested number of samples is returned. - total_size_in_kb += 1 - - if total_size_in_kb > 64: - raise ValueError("Number of samples specified is larger then the buffer size") + total_size_in_kb = self._calc_capture_size_in_kb(sample_count) values = self._parent.visa_handle.query_binary_values("CAPTUREGET? 0,{}".format(total_size_in_kb), datatype='f', is_big_endian=False) values = np.array(values) values = values[values != 0] - data = {k: v for k, v in zip(capture_variables, values.reshape((-1, n_variables)).T)} + capture_variables = self.capture_config().split(",") + n_variables = len(capture_variables) + + values = values.reshape((-1, n_variables)).T + values = values[:, :sample_count] + + data = {k: v for k, v in zip(capture_variables, values)} for capture_variable in capture_variables: buffer_parameter = getattr(self, capture_variable) @@ -276,6 +291,47 @@ def capture_samples(self, sample_count: int) ->dict: return self.get_capture_data(sample_count) + def capture_one_sample_per_trigger(self, trigger_count: int, start_triggers_pulsetrain: Callable) ->dict: + """ + Capture one sample for each trigger and return when the specified number of triggers has been received + + Args: + trigger_count (int) + start_triggers_pulsetrain (callable) + By calling this *non-blocking* function we start a pulse train + """ + total_size_in_kb = self._calc_capture_size_in_kb(trigger_count) + + self.capture_length_in_kb(total_size_in_kb) + self.start_capture("ONE", "SAMP") + start_triggers_pulsetrain() + + n_bytes_captured = 0 + while n_bytes_captured < trigger_count * self.bytes_per_sample: + n_bytes_captured = self.count_capture_bytes() + + self.stop_capture() + + return self.get_capture_data(trigger_count) + + def start_capture_at_trigger(self, sample_count: int) ->dict: + """ + Capture a number of samples after a trigger has been received. Please refer to page 135 of the manual + for details + """ + total_size_in_kb = self._calc_capture_size_in_kb(sample_count) + + self.capture_length_in_kb(total_size_in_kb) + self.start_capture("ONE", "TRIG") + + n_bytes_captured = 0 + while n_bytes_captured < total_size_in_kb * 1024: + n_bytes_captured = self.count_capture_bytes() + + self.stop_capture() + + return self.get_capture_data(sample_count) + class SR86x(VisaInstrument): """ @@ -447,6 +503,34 @@ def __init__(self, name, address, max_frequency, reset=False, **kwargs): 100: 16, 300: 17, 1e3: 18, 3e3: 19, 10e3: 20, 30e3: 21}) + + self.add_parameter( + name="trigger_reference", + label="Trigger Reference", + get_cmd="RTRG?", + set_cmd="RTRG {}", + val_mapping={ + "SIN": 0, + "POS": 1, + "POSTTL": 1, + "NEGTTL": 2, + "NEG": 2 + } + ) + + self.add_parameter( + name="source", + label="Source", + get_cmd="RSRC?", + set_cmd="RSRC {}", + val_mapping={ + "INT": 0, + "EXT": 1, + "DUAL": 2, + "CHOP": 3 + } + ) + # Auto functions self.add_function('auto_range', call_cmd='ARNG') self.add_function('auto_scale', call_cmd='ASCL') @@ -518,6 +602,7 @@ def __init__(self, name, address, max_frequency, reset=False, **kwargs): val_mapping={'OFF': 0, 'X10': 1, 'X100': 2}) + # Aux input/output for i in [0, 1, 2, 3]: self.add_parameter('aux_in{}'.format(i), @@ -545,6 +630,9 @@ def __init__(self, name, address, max_frequency, reset=False, **kwargs): self.input_config() self.connect_message() + + + def _set_units(self, unit): for param in [self.X, self.Y, self.R, self.sensitivity]: param.unit = unit From 9037372d86065feae4e0c5c5f4a5e0fdf9218496 Mon Sep 17 00:00:00 2001 From: dpfranke Date: Thu, 14 Jun 2018 16:48:14 +0200 Subject: [PATCH 074/150] apply get_raw recommendation ... and get rid of the corresponding warning --- qcodes/instrument_drivers/stanford_research/SR830.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/instrument_drivers/stanford_research/SR830.py b/qcodes/instrument_drivers/stanford_research/SR830.py index 0e4447fb4f8..f2b4acb5f10 100644 --- a/qcodes/instrument_drivers/stanford_research/SR830.py +++ b/qcodes/instrument_drivers/stanford_research/SR830.py @@ -84,7 +84,7 @@ def prepare_buffer_readout(self): else: self._instrument._buffer2_ready = True - def get(self): + def get_raw(self): """ Get command. Returns numpy array """ From 3e22e21c0d2991e8fc97f1b1fc3f825f30ae39cd Mon Sep 17 00:00:00 2001 From: sohailc Date: Thu, 14 Jun 2018 16:52:37 +0200 Subject: [PATCH 075/150] Fix maximum frequency for SRS860 - it is 500 kHz --- qcodes/instrument_drivers/stanford_research/SR860.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/instrument_drivers/stanford_research/SR860.py b/qcodes/instrument_drivers/stanford_research/SR860.py index 45449dab524..8ee9c6142e4 100644 --- a/qcodes/instrument_drivers/stanford_research/SR860.py +++ b/qcodes/instrument_drivers/stanford_research/SR860.py @@ -6,4 +6,4 @@ class SR860(SR86x): The SR860 instrument is almost equal to the SR865, except for the max frequency """ def __init__(self, name: str, address: str, reset: bool=False, **kwargs: str) ->None: - super().__init__(name, address, max_frequency=5E3, reset=reset, **kwargs) + super().__init__(name, address, max_frequency=500e3, reset=reset, **kwargs) From aa55bd24409a42b0650b2a2c2a365b09e2098c69 Mon Sep 17 00:00:00 2001 From: sohailc Date: Thu, 14 Jun 2018 16:53:19 +0200 Subject: [PATCH 076/150] add start_triggers_pulsetrain callable to start_capture_at_trigger as well --- qcodes/instrument_drivers/stanford_research/SR86x.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/qcodes/instrument_drivers/stanford_research/SR86x.py b/qcodes/instrument_drivers/stanford_research/SR86x.py index 2851b5604af..960c35f627d 100644 --- a/qcodes/instrument_drivers/stanford_research/SR86x.py +++ b/qcodes/instrument_drivers/stanford_research/SR86x.py @@ -304,6 +304,7 @@ def capture_one_sample_per_trigger(self, trigger_count: int, start_triggers_puls self.capture_length_in_kb(total_size_in_kb) self.start_capture("ONE", "SAMP") + start_triggers_pulsetrain() n_bytes_captured = 0 @@ -314,16 +315,23 @@ def capture_one_sample_per_trigger(self, trigger_count: int, start_triggers_puls return self.get_capture_data(trigger_count) - def start_capture_at_trigger(self, sample_count: int) ->dict: + def start_capture_at_trigger(self, sample_count: int, start_triggers_pulsetrain: Callable) ->dict: """ Capture a number of samples after a trigger has been received. Please refer to page 135 of the manual for details + + Args: + trigger_count (int) + start_triggers_pulsetrain (callable) + By calling this *non-blocking* function we start a pulse train """ total_size_in_kb = self._calc_capture_size_in_kb(sample_count) self.capture_length_in_kb(total_size_in_kb) self.start_capture("ONE", "TRIG") + start_triggers_pulsetrain() + n_bytes_captured = 0 while n_bytes_captured < total_size_in_kb * 1024: n_bytes_captured = self.count_capture_bytes() From c484dc29711acfc76ee45be4e93bdafb1c7470f2 Mon Sep 17 00:00:00 2001 From: Dominik Vogel Date: Thu, 14 Jun 2018 16:59:31 +0200 Subject: [PATCH 077/150] corrected order of wrapper for deprecation, test sphinx link --- qcodes/instrument_drivers/tektronix/AWG5014.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/qcodes/instrument_drivers/tektronix/AWG5014.py b/qcodes/instrument_drivers/tektronix/AWG5014.py index 79e84541c06..7ab414fdb71 100644 --- a/qcodes/instrument_drivers/tektronix/AWG5014.py +++ b/qcodes/instrument_drivers/tektronix/AWG5014.py @@ -906,7 +906,7 @@ def generate_channel_cfg(self): been changed from their default value and put them in a dictionary that can easily be written into an awg file, so as to prevent said awg file from falling back to default values. - (See self.make_awg_file and self.AWG_FILE_FORMAT_CHANNEL) + (See :meth:`~make_awg_file` and :meth:`qcodes.instrument_drivers.tektronix.AWG5014.Tektronix_AWG5014.AWG_FILE_FORMAT_CHANNEL`) NOTE: This only works for settings changed via the corresponding QCoDeS parameter. @@ -1435,11 +1435,6 @@ def get_error(self): """ return self.ask('SYSTEM:ERRor:NEXT?') - @deprecate(reason='this function is for private use only.') - @wraps(_pack_waveform, assigned=tuple(v for v in WRAPPER_ASSIGNMENTS if v != '__name__')) - def pack_waveform(self, *args, **kwargs): - self._pack_waveform(*args, **kwargs) - def _pack_waveform(self, wf, m1, m2): """ Converts/packs a waveform and two markers into a 16-bit format @@ -1487,6 +1482,11 @@ def _pack_waveform(self, wf, m1, m2): print(np.where(packed_wf == -1)) return packed_wf + @deprecate(reason='this function is for private use only.') + @wraps(_pack_waveform, assigned=tuple(v for v in WRAPPER_ASSIGNMENTS if v != '__name__')) + def pack_waveform(self, *args, **kwargs): + self._pack_waveform(*args, **kwargs) + ########################### # Waveform file functions # ########################### From d91702d26acd994e673d70c7da10799c2a062772 Mon Sep 17 00:00:00 2001 From: Dar Dahlen Date: Tue, 19 Jun 2018 22:04:44 -0700 Subject: [PATCH 078/150] Added support for N5183B Signal Generator --- qcodes/instrument_drivers/Keysight/N51x1.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/qcodes/instrument_drivers/Keysight/N51x1.py b/qcodes/instrument_drivers/Keysight/N51x1.py index 12155494364..cccc55aea94 100644 --- a/qcodes/instrument_drivers/Keysight/N51x1.py +++ b/qcodes/instrument_drivers/Keysight/N51x1.py @@ -6,7 +6,7 @@ class N51x1(VisaInstrument): """ This is the qcodes driver for Keysight/Agilent scalar RF sources. - It has been tested with N5171B, N5181A, N5171B + It has been tested with N5171B, N5181A, N5171B, N5183B """ def __init__(self, name, address, **kwargs): @@ -21,7 +21,8 @@ def __init__(self, name, address, **kwargs): vals=Numbers(min_value=-144,max_value=19)) # Query the instrument to see what frequency range was purchased - freq_dict = {'501':1e9, '503':3e9, '505':6e9} + freq_dict = {'501':1e9, '503':3e9, '505':6e9, '520':20e9} + max_freq = freq_dict[self.ask('*OPT?')] self.add_parameter('frequency', label='Frequency', @@ -42,7 +43,7 @@ def __init__(self, name, address, **kwargs): self.add_parameter('rf_output', get_cmd='OUTP:STAT?', set_cmd='OUTP:STAT ' + '{:s}', - val_mapping={'on': 1, 'off': 0}) + val_mapping={'on': '1', 'off': '0'}) self.connect_message() From f958986d94b9248a0dba33d8b8468591fd1b7bd1 Mon Sep 17 00:00:00 2001 From: Dar Dahlen Date: Wed, 20 Jun 2018 10:39:33 -0700 Subject: [PATCH 079/150] Added N5183B file which relabels the N51x1 device Changed string formatting in N51x1 --- qcodes/instrument_drivers/Keysight/Keysight_N5183B.py | 3 +++ qcodes/instrument_drivers/Keysight/N51x1.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 qcodes/instrument_drivers/Keysight/Keysight_N5183B.py diff --git a/qcodes/instrument_drivers/Keysight/Keysight_N5183B.py b/qcodes/instrument_drivers/Keysight/Keysight_N5183B.py new file mode 100644 index 00000000000..909005e9909 --- /dev/null +++ b/qcodes/instrument_drivers/Keysight/Keysight_N5183B.py @@ -0,0 +1,3 @@ +from qcodes.instrument_drivers.Keysight.N51x1 import N51x1 as N5183B +# N5183B has the same interface as N51x1 devices +# This file is to allow for improved device labeling. diff --git a/qcodes/instrument_drivers/Keysight/N51x1.py b/qcodes/instrument_drivers/Keysight/N51x1.py index cccc55aea94..be3863b53f6 100644 --- a/qcodes/instrument_drivers/Keysight/N51x1.py +++ b/qcodes/instrument_drivers/Keysight/N51x1.py @@ -42,8 +42,8 @@ def __init__(self, name, address, **kwargs): self.add_parameter('rf_output', get_cmd='OUTP:STAT?', - set_cmd='OUTP:STAT ' + '{:s}', - val_mapping={'on': '1', 'off': '0'}) + set_cmd='OUTP:STAT {}', + val_mapping={'on': 1, 'off': 0}) self.connect_message() From e955d9dd24f92de9dbe9d5b699d83886570e8ffa Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Fri, 22 Jun 2018 11:18:17 +0200 Subject: [PATCH 080/150] Add barebones visasim test for AWG5014 --- .../instrument/sims/Tektronix_AWG5014C.yaml | 43 +++++++++++++++++++ .../tests/drivers/test_tektronix_AWG5014C.py | 24 +++++++++++ 2 files changed, 67 insertions(+) create mode 100644 qcodes/instrument/sims/Tektronix_AWG5014C.yaml create mode 100644 qcodes/tests/drivers/test_tektronix_AWG5014C.py diff --git a/qcodes/instrument/sims/Tektronix_AWG5014C.yaml b/qcodes/instrument/sims/Tektronix_AWG5014C.yaml new file mode 100644 index 00000000000..c00cbf87375 --- /dev/null +++ b/qcodes/instrument/sims/Tektronix_AWG5014C.yaml @@ -0,0 +1,43 @@ +# SIMULATED INSTRUMENT FOR TEKTRONIX AWG 5014C +spec: "1.0" + +devices: + device 1: # AWG5014C + eom: + GPIB INSTR: + q: "\n" + r: "\n" + error: ERROR + dialogues: + - q: "*IDN?" + r: "QCoDeS, AWG5014C, 1000, 0.1" + + properties: + + current_directory: + default: "\\Users\\OEM\\Documents" + getter: + q: "MMEMory:CDIRectory?" + r: "{}" + setter: + q: "MMEMory:CDIRectory {}" + + clock frequency: + default: 1000000000 + getter: + q: "SOURce:FREQuency?" + r: "{}" + setter: + q: "SOURce:FREQuency {}" + + trigger impedance: + default: 50 + getter: + q: "TRIGger:IMPedance?" + r: "{}" + setter: + q: "TRIGger:IMPedance {}" + +resources: + GPIB::1::INSTR: + device: device 1 diff --git a/qcodes/tests/drivers/test_tektronix_AWG5014C.py b/qcodes/tests/drivers/test_tektronix_AWG5014C.py new file mode 100644 index 00000000000..373aa209a0d --- /dev/null +++ b/qcodes/tests/drivers/test_tektronix_AWG5014C.py @@ -0,0 +1,24 @@ +import pytest + +from qcodes.instrument_drivers.tektronix.AWG5014 import Tektronix_AWG5014 +import qcodes.instrument.sims as sims +visalib = sims.__file__.replace('__init__.py', 'Tektronix_AWG5014C.yaml@sim') + + +@pytest.fixture(scope='function') +def awg(): + awg_sim = Tektronix_AWG5014('awg_sim', + address='GPIB0::1::65535::INSTR', + timeout=1, + terminator='\n', + visalib=visalib) + yield awg_sim + + awg_sim.close() + + +def test_init_awg(awg): + + idn_dict = awg.IDN() + + assert idn_dict['vendor'] == 'QCoDeS' From 54be6aa9f3b1faf8f9605539d09294f92851cc37 Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Fri, 22 Jun 2018 11:35:30 +0200 Subject: [PATCH 081/150] Fix indentation --- qcodes/instrument_drivers/tektronix/AWG5014.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/qcodes/instrument_drivers/tektronix/AWG5014.py b/qcodes/instrument_drivers/tektronix/AWG5014.py index 7ab414fdb71..8dde66625e6 100644 --- a/qcodes/instrument_drivers/tektronix/AWG5014.py +++ b/qcodes/instrument_drivers/tektronix/AWG5014.py @@ -1025,12 +1025,11 @@ def mrkdeltrans(x): return AWG_channel_cfg - def _generate_awg_file(self, - packed_waveforms, wfname_l, nrep, trig_wait, - goto_state, jump_to, channel_cfg, - sequence_cfg=None, - preservechannelsettings=False): + packed_waveforms, wfname_l, nrep, trig_wait, + goto_state, jump_to, channel_cfg, + sequence_cfg=None, + preservechannelsettings=False): """ This function generates an .awg-file for uploading to the AWG. The .awg-file contains a waveform list, full sequencing information From 9777d466e41edb3a182da67fa42fdbcbf866503e Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Fri, 22 Jun 2018 12:10:57 +0200 Subject: [PATCH 082/150] Refactor two queries to parameters --- qcodes/instrument_drivers/tektronix/AWG5014.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/qcodes/instrument_drivers/tektronix/AWG5014.py b/qcodes/instrument_drivers/tektronix/AWG5014.py index 8dde66625e6..d1059ae2056 100644 --- a/qcodes/instrument_drivers/tektronix/AWG5014.py +++ b/qcodes/instrument_drivers/tektronix/AWG5014.py @@ -867,11 +867,9 @@ def generate_sequence_cfg(self): AWG_sequence_cfg = { 'SAMPLING_RATE': self.get('clock_freq'), - 'CLOCK_SOURCE': (1 if self.ask('AWGControl:CLOCk:' + - 'SOURce?').startswith('INT') + 'CLOCK_SOURCE': (1 if self.clock_source().startswith('INT') else 2), # Internal | External - 'REFERENCE_SOURCE': (1 if self.ask('SOURce1:ROSCillator:' + - 'SOURce?').startswith('INT') + 'REFERENCE_SOURCE': (1 if self.ref_source().startswith('INT') else 2), # Internal | External 'EXTERNAL_REFERENCE_TYPE': 1, # Fixed | Variable 'REFERENCE_CLOCK_FREQUENCY_SELECTION': 1, From c10d8e06d7460cc7e7fb91a1866c99cf9095d277 Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Fri, 22 Jun 2018 19:40:40 +0200 Subject: [PATCH 083/150] Update yaml file with all sequence_settings params --- .../instrument/sims/Tektronix_AWG5014C.yaml | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/qcodes/instrument/sims/Tektronix_AWG5014C.yaml b/qcodes/instrument/sims/Tektronix_AWG5014C.yaml index c00cbf87375..e0af9f6e714 100644 --- a/qcodes/instrument/sims/Tektronix_AWG5014C.yaml +++ b/qcodes/instrument/sims/Tektronix_AWG5014C.yaml @@ -38,6 +38,54 @@ devices: setter: q: "TRIGger:IMPedance {}" + clock source: + default: "INT" + getter: + q: "AWGControl:CLOCk:SOURce?" + r: "{}" + setter: + q: "AWGControl:CLOCk:SOURce {}" + + reference source: + default: "INT" + getter: + q: "SOURce1:ROSCillator:SOURce?" + r: "{}" + setter: + q: "SOURce1:ROSCillator:SOURce {}" + + trigger source: + default: "INT" + getter: + q: "TRIGger:SOURce?" + r: "{}" + setter: + q: "TRIGger:SOURce {}" + + trigger level: + default: 0 + getter: + q: "TRIGger:LEVel?" + r: "{}" + setter: + q: "TRIGger:LEVel {}" + + event impedance: + default: 50 + getter: + q: "EVENt:IMPedance?" + r: "{}" + setter: + q: "EVENt:IMPedance {}" + + event level: + default: 0 + getter: + q: "EVENt:LEVel?" + r: "{}" + setter: + q: "EVENt:LEVel {}" + resources: GPIB::1::INSTR: device: device 1 From 689fc542ff27e719fc2fc1a319d4952e9d95e9a3 Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Fri, 22 Jun 2018 20:00:35 +0200 Subject: [PATCH 084/150] Extend tests slightly --- .../tests/drivers/test_tektronix_AWG5014C.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/qcodes/tests/drivers/test_tektronix_AWG5014C.py b/qcodes/tests/drivers/test_tektronix_AWG5014C.py index 373aa209a0d..52460c4aa60 100644 --- a/qcodes/tests/drivers/test_tektronix_AWG5014C.py +++ b/qcodes/tests/drivers/test_tektronix_AWG5014C.py @@ -1,4 +1,5 @@ import pytest +import numpy as np from qcodes.instrument_drivers.tektronix.AWG5014 import Tektronix_AWG5014 import qcodes.instrument.sims as sims @@ -22,3 +23,40 @@ def test_init_awg(awg): idn_dict = awg.IDN() assert idn_dict['vendor'] == 'QCoDeS' + + +def test_pack_waveform(awg): + + N = 25 + + waveform = np.random.rand(N) + m1 = np.random.randint(0, 2, N) + m2 = np.random.randint(0, 2, N) + + package = awg._pack_waveform(waveform, m1, m2) + + assert package is not None + + +def test_make_awg_file(awg): + + N = 25 + + waveforms = [[np.random.rand(N)]] + m1s = [[np.random.randint(0, 2, N)]] + m2s = [[np.random.randint(0, 2, N)]] + nreps = [1] + trig_waits = [0] + goto_states = [0] + jump_tos = [0] + + awgfile = awg.make_awg_file(waveforms, + m1s, + m2s, + nreps, + trig_waits, + goto_states, + jump_tos, + preservechannelsettings=False) + + assert len(awgfile) > 0 From d92db2f568d47f7e3d1a79814a5b220c1f1482e6 Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Fri, 22 Jun 2018 20:01:08 +0200 Subject: [PATCH 085/150] Don't use deprecated (and now broken) public pack_waveform --- qcodes/instrument_drivers/tektronix/AWG5014.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/qcodes/instrument_drivers/tektronix/AWG5014.py b/qcodes/instrument_drivers/tektronix/AWG5014.py index d1059ae2056..c69f8b995f2 100644 --- a/qcodes/instrument_drivers/tektronix/AWG5014.py +++ b/qcodes/instrument_drivers/tektronix/AWG5014.py @@ -1270,9 +1270,11 @@ def make_awg_file(self, waveforms, m1s, m2s, else: thisname = 'wfm{:03d}ch{}'.format(jj + 1, channels[ii]) namelist.append(thisname) - package = self.pack_waveform(waveforms[ii][jj], - m1s[ii][jj], - m2s[ii][jj]) + + package = self._pack_waveform(waveforms[ii][jj], + m1s[ii][jj], + m2s[ii][jj]) + packed_wfs[thisname] = package waveform_names.append(namelist) From a68f7256de232adbccae24da1e86402cd375c6d6 Mon Sep 17 00:00:00 2001 From: MIkhail Astafev Date: Fri, 22 Jun 2018 20:01:11 +0200 Subject: [PATCH 086/150] qcodes-1124 initial fix of the problem, extra test; to-be-made-better --- qcodes/dataset/measurements.py | 4 +- .../test_measurement_context_manager.py | 37 ++++++++++++++++++- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/qcodes/dataset/measurements.py b/qcodes/dataset/measurements.py index 3e8873d83ab..1d76e1e4682 100644 --- a/qcodes/dataset/measurements.py +++ b/qcodes/dataset/measurements.py @@ -292,12 +292,12 @@ def __exit__(self, exception_type, exception_value, traceback) -> None: for func, args in self.exitactions: func(*args) - self.ds.unsubscribe_all() - # and finally mark the dataset as closed, thus # finishing the measurement self.ds.mark_complete() + self.ds.unsubscribe_all() + class Measurement: """ diff --git a/qcodes/tests/dataset/test_measurement_context_manager.py b/qcodes/tests/dataset/test_measurement_context_manager.py index d1de1c95fc7..8943ba21da9 100644 --- a/qcodes/tests/dataset/test_measurement_context_manager.py +++ b/qcodes/tests/dataset/test_measurement_context_manager.py @@ -401,14 +401,47 @@ def subscriber2(results, length, state): expected_list += [c for c in (a, b) if c > 7] datasaver.add_result((DAC.ch1, a), (DMM.v1, b)) - datasaver.flush_data_to_database() + datasaver.flush_data_to_database() # this is flaky, the same way. we can only assert once we are sure that subscriber callbacks have been executed, which happens if in _loop the queue is more than min_count; finally if there is a lot of stuff ghoing on on pc that the line on the calls to subscribers have not yet been execited while now the main thread tries to assert. assert lt7s == expected_list assert list(res_dict.keys()) == [n for n in range(1, num+2)] - assert len(datasaver._dataset.subscribers) == 0 + assert len(datasaver._dataset.subscribers) == 0 # tests that after runner cntx mng is exited, dataset is unsubscribed from subscribers +def test_subscriptions_stop_before_queue_flushed(experiment, DAC): + + def sub_get_x_vals(results, length, state): + """ + A list of all x values + """ + state += [res[0] for res in results] + + meas = Measurement(exp=experiment) + meas.register_parameter(DAC.ch1) + + xvals = [] + + meas.add_subscriber(sub_get_x_vals, state=xvals) + + given_xvals = [0, 1, 2, 3] + + with meas.run() as datasaver: + + subscriber_obj = list(datasaver.dataset.subscribers.values())[0] + subscriber_obj.min_count = int(len(given_xvals) + 1) + + for x in given_xvals: + datasaver.add_result((DAC.ch1, x)) + + print(f"xvals: {xvals}") + print(f"given_xvals: {list(given_xvals)}") + + assert xvals == list(given_xvals) + + + +# @reproduce_failure('3.60.1', b'AAJf') @settings(deadline=None, max_examples=25) @given(N=hst.integers(min_value=2000, max_value=3000)) def test_subscriptions_getting_all_points(experiment, DAC, DMM, N): From d5b0437edca4fd566b4c8cebc4a6a3a22c446666 Mon Sep 17 00:00:00 2001 From: WilliamHPNielsen Date: Fri, 22 Jun 2018 22:18:01 +0200 Subject: [PATCH 087/150] Add missing returns --- qcodes/instrument_drivers/tektronix/AWG5014.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qcodes/instrument_drivers/tektronix/AWG5014.py b/qcodes/instrument_drivers/tektronix/AWG5014.py index c69f8b995f2..2a016eb2ce5 100644 --- a/qcodes/instrument_drivers/tektronix/AWG5014.py +++ b/qcodes/instrument_drivers/tektronix/AWG5014.py @@ -1167,7 +1167,7 @@ def _generate_awg_file(self, @deprecate(alternative='make_awg_file, _generate_awg_file') @wraps(_generate_awg_file, assigned=tuple(v for v in WRAPPER_ASSIGNMENTS if v != '__name__')) def generate_awg_file(self, *args, **kwargs): - self._generate_awg_file(*args, **kwargs) + return self._generate_awg_file(*args, **kwargs) def send_awg_file(self, filename, awg_file, verbose=False): """ @@ -1484,7 +1484,7 @@ def _pack_waveform(self, wf, m1, m2): @deprecate(reason='this function is for private use only.') @wraps(_pack_waveform, assigned=tuple(v for v in WRAPPER_ASSIGNMENTS if v != '__name__')) def pack_waveform(self, *args, **kwargs): - self._pack_waveform(*args, **kwargs) + return self._pack_waveform(*args, **kwargs) ########################### # Waveform file functions # From 559030e1ad26cb2effcf4941691bb5a5b4c31ce1 Mon Sep 17 00:00:00 2001 From: MIkhail Astafev Date: Mon, 25 Jun 2018 11:50:43 +0200 Subject: [PATCH 088/150] qcodes-1124 Refactor Subscriber class for readability --- qcodes/dataset/data_set.py | 123 ++++++++++++++++++++----------------- 1 file changed, 68 insertions(+), 55 deletions(-) diff --git a/qcodes/dataset/data_set.py b/qcodes/dataset/data_set.py index 80aa103cdf5..0d53f3cb7c2 100644 --- a/qcodes/dataset/data_set.py +++ b/qcodes/dataset/data_set.py @@ -60,66 +60,79 @@ class CompletedError(RuntimeError): pass -class Subscriber(Thread): +class _Subscriber(Thread): """ Class to add a subscriber to a DataSet. The subscriber gets called every time an insert is made to the results_table. - The Subscriber is not meant to be instantiated directly, but rather used + The _Subscriber is not meant to be instantiated directly, but rather used via the 'subscribe' method of the DataSet. - NOTE: A subscriber should be added *after* all parameters have added. + NOTE: A subscriber should be added *after* all parameters have been added. + + NOTE: Special care shall be taken when using the *state* object: it is the user's + responsibility to operate with it in a thread-safe way. """ - def __init__(self, dataSet, sub_id: str, + def __init__(self, + dataSet, + id_: str, callback: Callable[..., None], - state: Optional[Any] = None, min_wait: int = 100, - min_count: int = 1, - callback_kwargs: Optional[Dict[str, Any]]=None) -> None: - self.sub_id = sub_id - # whether or not this is actually thread safe I am not sure :P + state: Optional[Any] = None, + loop_sleep_time: int = 0, # in seconds + min_queue_length: int = 1, + callback_kwargs: Optional[Dict[str, Any]]=None + ) -> None: + super().__init__() + + self._id = id_ + self.dataSet = dataSet self.table_name = dataSet.table_name - conn = dataSet.conn - self.log = logging.getLogger(f"Subscriber {self.sub_id}") + self._data_set_len = len(dataSet) self.state = state - self.min_wait = min_wait - self.min_count = min_count - self._send_queue: int = 0 + + self.data_queue: Queue = Queue() + self._queue_length: int = 0 + self._stop_signal: bool = False + self._loop_sleep_time = loop_sleep_time / 1000 # convert seconds to minutes + self.min_queue_length = min_queue_length + if callback_kwargs is None: self.callback = callback else: self.callback = functools.partial(callback, **callback_kwargs) - self._stop_signal: bool = False - parameters = dataSet.get_parameters() - param_sql = ",".join([f"NEW.{p.name}" for p in parameters]) - self.callbackid = f"callback{self.sub_id}" + self.callback_id = f"callback{self._id}" - conn.create_function(self.callbackid, -1, self.cache) - sql = f""" - CREATE TRIGGER sub{self.sub_id} + conn = dataSet.conn + + conn.create_function(self.callback_id, -1, self._cache_data_to_queue) + + parameters = dataSet.get_parameters() + sql_param_list = ",".join([f"NEW.{p.name}" for p in parameters]) + sql_create_trigger_for_callback = f""" + CREATE TRIGGER sub{self._id} AFTER INSERT ON '{self.table_name}' BEGIN - SELECT {self.callbackid}({param_sql}); + SELECT {self.callback_id}({sql_param_list}); END;""" - atomic_transaction(conn, sql) - self.data: Queue = Queue() - self._data_set_len = len(dataSet) - super().__init__() + atomic_transaction(conn, sql_create_trigger_for_callback) + + self.log = logging.getLogger(f"_Subscriber {self._id}") - def cache(self, *args) -> None: - self.log.debug(f"Args:{args} put into queue for {self.callbackid}") - self.data.put(args) + def _cache_data_to_queue(self, *args) -> None: + self.log.debug(f"Args:{args} put into queue for {self.callback_id}") + self.data_queue.put(args) self._data_set_len += 1 - self._send_queue += 1 + self._queue_length += 1 def run(self) -> None: self.log.debug("Starting subscriber") self._loop() @staticmethod - def _exhaust_queue(queue) -> List: + def _exhaust_queue(queue: Queue) -> List: result_list = [] while True: try: @@ -128,40 +141,38 @@ def _exhaust_queue(queue) -> List: break return result_list - def _send(self) -> List: - result_list = self._exhaust_queue(self.data) + def _call_callback_on_queue_data(self) -> None: + result_list = self._exhaust_queue(self.data_queue) self.callback(result_list, self._data_set_len, self.state) self.log.debug(f"{self.callback} called with " f"result_list: {result_list}.") - # TODO (WilliamHPNielsen): why does this method return smth? - return result_list def _loop(self) -> None: while True: if self._stop_signal: self._clean_up() break - if self._send_queue >= self.min_count: - self._send() - self._send_queue = 0 - # if nothing happens we let the word go foward - time.sleep(self.min_wait / 1000) + if self._queue_length >= self.min_queue_length: + self._call_callback_on_queue_data() + self._queue_length = 0 + + time.sleep(self._loop_sleep_time) + if self.dataSet.completed: - self._send() + self._call_callback_on_queue_data() break def done_callback(self) -> None: self.log.debug("Done callback") - self._send() + self._call_callback_on_queue_data() - def schedule_stop(self): + def schedule_stop(self) -> None: if not self._stop_signal: self.log.debug("Scheduling stop") self._stop_signal = True def _clean_up(self) -> None: - # TODO: just a temp implemation (remove?) self.log.debug("Stopped subscriber") @@ -188,7 +199,7 @@ def __init__(self, path_to_db: str, run_id: Optional[int]=None, self.run_id = run_id self._debug = False - self.subscribers: Dict[str, Subscriber] = {} + self.subscribers: Dict[str, _Subscriber] = {} if run_id: self._completed = completed(self.conn, self.run_id) @@ -564,17 +575,19 @@ def get_setpoints(self, param_name: str) -> List[List[Any]]: return setpoints # NEED to pass Any for some reason - def subscribe(self, callback: Callable[[Any, int, Optional[Any]], None], - min_wait: int = 0, min_count: int = 1, + def subscribe(self, + callback: Callable[[Any, int, Optional[Any]], None], + min_wait: int = 0, + min_count: int = 1, state: Optional[Any] = None, - callback_kwargs: Optional[Dict[str, Any]] = None, - subscriber_class=Subscriber) -> str: - sub_id = uuid.uuid4().hex - sub = Subscriber(self, sub_id, callback, state, min_wait, min_count, - callback_kwargs) - self.subscribers[sub_id] = sub - sub.start() - return sub.sub_id + callback_kwargs: Optional[Dict[str, Any]] = None + ) -> str: + subscriber_id = uuid.uuid4().hex + subscriber = _Subscriber(self, subscriber_id, callback, state, min_wait, min_count, + callback_kwargs) + self.subscribers[subscriber_id] = subscriber + subscriber.start() + return subscriber_id def unsubscribe(self, uuid: str) -> None: """ From 555d458089031289abf18e5146ce569da356f853 Mon Sep 17 00:00:00 2001 From: MIkhail Astafev Date: Mon, 25 Jun 2018 14:58:53 +0200 Subject: [PATCH 089/150] qcodes-1124 refactor subscriber tests for readability --- .../test_measurement_context_manager.py | 101 ++++++++++-------- 1 file changed, 59 insertions(+), 42 deletions(-) diff --git a/qcodes/tests/dataset/test_measurement_context_manager.py b/qcodes/tests/dataset/test_measurement_context_manager.py index 8943ba21da9..56622dea398 100644 --- a/qcodes/tests/dataset/test_measurement_context_manager.py +++ b/qcodes/tests/dataset/test_measurement_context_manager.py @@ -347,7 +347,7 @@ def action(lst, word): def test_subscriptions(experiment, DAC, DMM): """ - Test that subscribers are called at the moment that data is flushed to database + Test that subscribers are called at the moment the data is flushed to database Note that for the purpose of this test, flush_data_to_database method is called explicitly instead of waiting for the data to be flushed automatically after the write_period passes after a add_result call. @@ -358,93 +358,110 @@ def test_subscriptions(experiment, DAC, DMM): DMM (qcodes.instrument.base.Instrument) : another dummy instrument object """ - def subscriber1(results, length, state): + def collect_all_results(results, length, state): """ - A dict of all results + Updates the *state* to contain all the *results* acquired during the experiment run """ + # Due to the fact that by default subscribers only hold 1 data value in their internal queue, + # this assignment should work (i.e. not overwrite values in the "state" object) assuming that at + # the start of the experiment both the dataset and the *state* objects have the same length. state[length] = results - def subscriber2(results, length, state): + def collect_values_larger_than_7(results, length, state): """ - A list of all parameter values larger than 7 + Appends to the *state* only the values from *results* that are larger than 7 """ - for res in results: - state += [pres for pres in res if pres > 7] + for result_tuple in results: + state += [value for value in result_tuple if value > 7] meas = Measurement(exp=experiment) meas.register_parameter(DAC.ch1) meas.register_parameter(DMM.v1, setpoints=(DAC.ch1,)) - res_dict = {} - lt7s = [] + all_results_dict = {} # key is the number of the result tuple, value is the result tuple itself + values_larger_than_7 = [] - meas.add_subscriber(subscriber1, state=res_dict) + meas.add_subscriber(collect_all_results, state=all_results_dict) assert len(meas.subscribers) == 1 - meas.add_subscriber(subscriber2, state=lt7s) + meas.add_subscriber(collect_values_larger_than_7, state=values_larger_than_7) assert len(meas.subscribers) == 2 meas.write_period = 0.2 - expected_list = [] - with meas.run() as datasaver: + # Assert that the measurement, runner, and datasaver have added subscribers to the dataset assert len(datasaver._dataset.subscribers) == 2 - assert res_dict == {} - assert lt7s == [] - as_and_bs = list(zip(range(5), range(3, 8))) + assert all_results_dict == {} + assert values_larger_than_7 == [] - for num in range(5): + dac_vals_and_dmm_vals = list(zip(range(5), range(3, 8))) + values_larger_than_7__expected = [] - (a, b) = as_and_bs[num] - expected_list += [c for c in (a, b) if c > 7] + for num in range(5): + (dac_val, dmm_val) = dac_vals_and_dmm_vals[num] + values_larger_than_7__expected += [val for val in (dac_val, dmm_val) if val > 7] - datasaver.add_result((DAC.ch1, a), (DMM.v1, b)) - datasaver.flush_data_to_database() # this is flaky, the same way. we can only assert once we are sure that subscriber callbacks have been executed, which happens if in _loop the queue is more than min_count; finally if there is a lot of stuff ghoing on on pc that the line on the calls to subscribers have not yet been execited while now the main thread tries to assert. + datasaver.add_result((DAC.ch1, dac_val), (DMM.v1, dmm_val)) - assert lt7s == expected_list - assert list(res_dict.keys()) == [n for n in range(1, num+2)] + # Ensure that data is flushed to the database despite the write period, so that the database triggers + # are executed, which in turn add data to the queues within the subscribers + datasaver.flush_data_to_database() - assert len(datasaver._dataset.subscribers) == 0 # tests that after runner cntx mng is exited, dataset is unsubscribed from subscribers + # In order to make this test deterministic, we need to ensure that just enough time has passed between + # the moment the data is flushed to database and the "state" object (that is passed to subscriber + # constructor) has been updated by the corresponding subscriber's callback function. + # At the moment, there is no robust way to ensure this. The reason is that the subscribers have internal + # queue which is populated via a trigger call from the SQL database, hence from this "main" thread it is + # difficult to say whether the queue is empty because the subscriber callbacks have already been executed + # or because the triggers of the SQL database has not been executed yet. + assert values_larger_than_7 == values_larger_than_7__expected + assert list(all_results_dict.keys()) == [result_number for result_number in range(1, num+1+1)] + # Ensure that after exiting the "run()" context, all subscribers get unsubscribed from the dataset + assert len(datasaver._dataset.subscribers) == 0 -def test_subscriptions_stop_before_queue_flushed(experiment, DAC): - def sub_get_x_vals(results, length, state): +def test_subscribers_called_at_exiting_context_if_queue_is_not_empty(experiment, DAC): + """ + Upon quitting the "run()" context, verify that in case the queue is not empty, the subscriber's callback is + still called on that data. This situation is created by setting the minimum length of the queue to a number that is + larger than the number of value written to the dataset. + """ + def collect_x_vals(results, length, state): """ - A list of all x values + Collects first elements of results tuples in *state* """ - state += [res[0] for res in results] + index_of_x = 0 + state += [res[index_of_x] for res in results] meas = Measurement(exp=experiment) meas.register_parameter(DAC.ch1) - xvals = [] + collected_x_vals = [] - meas.add_subscriber(sub_get_x_vals, state=xvals) + meas.add_subscriber(collect_x_vals, state=collected_x_vals) - given_xvals = [0, 1, 2, 3] + given_x_vals = [0, 1, 2, 3] with meas.run() as datasaver: + # Set the minimum queue size of the subscriber to more that the total number of + # values being added to the dataset; this way the subscriber callback is not called before + # we exit the "run()" context. + subscriber = list(datasaver.dataset.subscribers.values())[0] + subscriber.min_queue_length = int(len(given_x_vals) + 1) - subscriber_obj = list(datasaver.dataset.subscribers.values())[0] - subscriber_obj.min_count = int(len(given_xvals) + 1) - - for x in given_xvals: + for x in given_x_vals: datasaver.add_result((DAC.ch1, x)) + assert collected_x_vals == [] # Ensure that the subscriber callback is not called yet - print(f"xvals: {xvals}") - print(f"given_xvals: {list(given_xvals)}") - - assert xvals == list(given_xvals) - + assert collected_x_vals == given_x_vals -# @reproduce_failure('3.60.1', b'AAJf') @settings(deadline=None, max_examples=25) @given(N=hst.integers(min_value=2000, max_value=3000)) -def test_subscriptions_getting_all_points(experiment, DAC, DMM, N): +def test_subscribers_called_for_all_data_points(experiment, DAC, DMM, N): def sub_get_x_vals(results, length, state): """ From 064c66bde90fa47b87750c6e3c9ebd1cf4f2746a Mon Sep 17 00:00:00 2001 From: MIkhail Astafev Date: Mon, 25 Jun 2018 15:27:51 +0200 Subject: [PATCH 090/150] Fix bug: removing triggers for subscribers from sql database now works --- qcodes/dataset/data_set.py | 2 +- qcodes/tests/dataset/test_measurement_context_manager.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/qcodes/dataset/data_set.py b/qcodes/dataset/data_set.py index 0d53f3cb7c2..b5e5423e8f5 100644 --- a/qcodes/dataset/data_set.py +++ b/qcodes/dataset/data_set.py @@ -601,7 +601,7 @@ def unsubscribe(self, uuid: str) -> None: del self.subscribers[uuid] def _remove_trigger(self, name): - transaction(self.conn, f"DROP TRIGGER IF EXISTS name;") + transaction(self.conn, f"DROP TRIGGER IF EXISTS {name};") def unsubscribe_all(self): """ diff --git a/qcodes/tests/dataset/test_measurement_context_manager.py b/qcodes/tests/dataset/test_measurement_context_manager.py index 56622dea398..df7f2080f4a 100644 --- a/qcodes/tests/dataset/test_measurement_context_manager.py +++ b/qcodes/tests/dataset/test_measurement_context_manager.py @@ -13,7 +13,7 @@ from qcodes.dataset.experiment_container import new_experiment from qcodes.tests.instrument_mocks import DummyInstrument, DummyChannelInstrument from qcodes.dataset.param_spec import ParamSpec -from qcodes.dataset.sqlite_base import connect, init_db +from qcodes.dataset.sqlite_base import atomic_transaction from qcodes.instrument.parameter import ArrayParameter from qcodes.dataset.legacy_import import import_dat_file from qcodes.dataset.data_set import load_by_id @@ -422,6 +422,11 @@ def collect_values_larger_than_7(results, length, state): # Ensure that after exiting the "run()" context, all subscribers get unsubscribed from the dataset assert len(datasaver._dataset.subscribers) == 0 + # Ensure that the triggers for each subscriber have been removed from the database + get_triggers_sql = "SELECT * FROM sqlite_master WHERE TYPE = 'trigger';" + triggers = atomic_transaction(datasaver._dataset.conn, get_triggers_sql).fetchall() + assert len(triggers) == 0 + def test_subscribers_called_at_exiting_context_if_queue_is_not_empty(experiment, DAC): """ From 0ea2f1f95b6da6029d917feac7f097b6b3fc76fa Mon Sep 17 00:00:00 2001 From: MIkhail Astafev Date: Mon, 25 Jun 2018 16:07:22 +0200 Subject: [PATCH 091/150] Fix typo in unit for the loop sleep time --- qcodes/dataset/data_set.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qcodes/dataset/data_set.py b/qcodes/dataset/data_set.py index b5e5423e8f5..d444ba58d96 100644 --- a/qcodes/dataset/data_set.py +++ b/qcodes/dataset/data_set.py @@ -78,7 +78,7 @@ def __init__(self, id_: str, callback: Callable[..., None], state: Optional[Any] = None, - loop_sleep_time: int = 0, # in seconds + loop_sleep_time: int = 0, # in milliseconds min_queue_length: int = 1, callback_kwargs: Optional[Dict[str, Any]]=None ) -> None: @@ -95,7 +95,7 @@ def __init__(self, self.data_queue: Queue = Queue() self._queue_length: int = 0 self._stop_signal: bool = False - self._loop_sleep_time = loop_sleep_time / 1000 # convert seconds to minutes + self._loop_sleep_time = loop_sleep_time / 1000 # convert milliseconds to seconds self.min_queue_length = min_queue_length if callback_kwargs is None: From a77c65dafa4adc5339181c2f1abf3a787b5457eb Mon Sep 17 00:00:00 2001 From: MIkhail Astafev Date: Mon, 25 Jun 2018 18:57:26 +0200 Subject: [PATCH 092/150] Add call-it-until-does-not-throw decorator, for use in tests --- qcodes/tests/common.py | 56 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/qcodes/tests/common.py b/qcodes/tests/common.py index 46b0dd900b8..c49b8c2ea21 100644 --- a/qcodes/tests/common.py +++ b/qcodes/tests/common.py @@ -1,3 +1,8 @@ +from typing import Callable +from functools import wraps +from time import sleep + + def strip_qc(d, keys=('instrument', '__class__')): # depending on how you run the tests, __module__ can either # have qcodes on the front or not. Just strip it off. @@ -5,3 +10,54 @@ def strip_qc(d, keys=('instrument', '__class__')): if key in d: d[key] = d[key].replace('qcodes.tests.', 'tests.') return d + + +def retry_until_does_not_throw(exception_class_to_expect: Exception=AssertionError, + tries: int=5, + delay: float=0.1) -> Callable: + """ + Call the decorated function given number of times with given delay between the calls + until it does not throw an exception of a given class. + + If the function throws an exception of a different class, it gets propagated outside + (i.e. the function is not called anymore). + + Usage: + >> x = False # let's assume that another thread has access to "x", + # and it is going to change "x" to "True" very soon + >> @retry_until_does_not_throw() ... + def assert_x_is_true(): ... + assert x, "x is still False..." ... + >> assert_x_is_true() # depending on the settings of "retry_until_does_not_throw", + # it will keep calling the function (with breaks in between) until + # either it does not throw or the number of tries is exceeded. + + Args: + exception_class_to_expect + Only in case of this exception the function will be called again + tries + Number of times to retry calling the function before giving up + delay + Delay between retries of the function call, in seconds + + Returns: + A callable that runs the decorated function until it does not throw a given exception + """ + def retry_until_passes_decorator(func: Callable): + + @wraps(func) + def func_retry(*args, **kwargs): + tries_left = tries - 1 + while tries_left > 0: + try: + return func(*args, **kwargs) + except exception_class_to_expect: + tries_left -= 1 + sleep(delay) + # the very last attempt to call the function is outside the "try-except" clause, + # so that the exception can propagate up the call stack + return func(*args, **kwargs) + + return func_retry + + return retry_until_passes_decorator From 87c99df8ea7460bad0c96c58fbcde2e5dc3fb1a1 Mon Sep 17 00:00:00 2001 From: MIkhail Astafev Date: Mon, 25 Jun 2018 19:00:35 +0200 Subject: [PATCH 093/150] qcodes-1124 Robustly retry assertions to give Subscribers more time Use the new rety_until_does_not_throw decorator to wrap the assertions on the states. This will give more time to Subscribers to exhaust their queues and call their callbacks (which in turn will update the state). --- .../dataset/test_measurement_context_manager.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/qcodes/tests/dataset/test_measurement_context_manager.py b/qcodes/tests/dataset/test_measurement_context_manager.py index df7f2080f4a..d20cbf9fed5 100644 --- a/qcodes/tests/dataset/test_measurement_context_manager.py +++ b/qcodes/tests/dataset/test_measurement_context_manager.py @@ -8,6 +8,8 @@ import hypothesis.strategies as hst import numpy as np +from qcodes.tests.common import retry_until_does_not_throw + import qcodes as qc from qcodes.dataset.measurements import Measurement from qcodes.dataset.experiment_container import new_experiment @@ -416,8 +418,14 @@ def collect_values_larger_than_7(results, length, state): # queue which is populated via a trigger call from the SQL database, hence from this "main" thread it is # difficult to say whether the queue is empty because the subscriber callbacks have already been executed # or because the triggers of the SQL database has not been executed yet. - assert values_larger_than_7 == values_larger_than_7__expected - assert list(all_results_dict.keys()) == [result_number for result_number in range(1, num+1+1)] + # + # In order to overcome this problem, a special decorator is used to wrap the assertions. This is going to + # ensure that some time is given to the Subscriber threads to finish exhausting the queue. + @retry_until_does_not_throw(exception_class_to_expect=AssertionError, delay=0.5, tries=10) + def assert_states_updated_from_callbacks(): + assert values_larger_than_7 == values_larger_than_7__expected + assert list(all_results_dict.keys()) == [result_number for result_number in range(1, num + 1 + 1)] + assert_states_updated_from_callbacks() # Ensure that after exiting the "run()" context, all subscribers get unsubscribed from the dataset assert len(datasaver._dataset.subscribers) == 0 From 67099d7a6c36f23f0d8aa457409d38a56ec575be Mon Sep 17 00:00:00 2001 From: MIkhail Astafev Date: Mon, 25 Jun 2018 19:22:08 +0200 Subject: [PATCH 094/150] Correct type checking to allow subclasses of Exception --- qcodes/tests/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qcodes/tests/common.py b/qcodes/tests/common.py index c49b8c2ea21..830fdaab7cd 100644 --- a/qcodes/tests/common.py +++ b/qcodes/tests/common.py @@ -1,4 +1,4 @@ -from typing import Callable +from typing import Callable, Type from functools import wraps from time import sleep @@ -12,7 +12,7 @@ def strip_qc(d, keys=('instrument', '__class__')): return d -def retry_until_does_not_throw(exception_class_to_expect: Exception=AssertionError, +def retry_until_does_not_throw(exception_class_to_expect: Type[Exception]=AssertionError, tries: int=5, delay: float=0.1) -> Callable: """ From 5a94345f73efd96a545fbd79c6ca3ca1ee614f96 Mon Sep 17 00:00:00 2001 From: sohailc Date: Mon, 25 Jun 2018 20:28:36 +0200 Subject: [PATCH 095/150] Improve the code related to capturing data via buffer - capture only the requested number of samples in start_capture_at_trigger, not the full buffer - add min_capture_length_in_kb property - add max_capture_length_in_kb property - make _set_capture_len_parser object method from static method - make _set_capture_len_parser use new capture-length-related properties - make _calc_capture_size_in_kb reuse _set_capture_len_parser - add convenient set_capture_rate_to_maximum method - add convenient _get_list_of_capture_variable_names and reuse it - add convenient _get_number_of_capture_variables and reuse it - remove capture_samples method because it does not make much sense --- .../stanford_research/SR86x.py | 131 +++++++++--------- 1 file changed, 67 insertions(+), 64 deletions(-) diff --git a/qcodes/instrument_drivers/stanford_research/SR86x.py b/qcodes/instrument_drivers/stanford_research/SR86x.py index 960c35f627d..117b9e2eb91 100644 --- a/qcodes/instrument_drivers/stanford_research/SR86x.py +++ b/qcodes/instrument_drivers/stanford_research/SR86x.py @@ -1,13 +1,13 @@ import numpy as np -import time import logging -from typing import Optional, Sequence, Dict, Callable +from typing import Sequence, Dict, Callable from qcodes import VisaInstrument from qcodes.instrument.channel import InstrumentChannel from qcodes.utils.validators import Numbers, Ints, Enum from qcodes.instrument.parameter import ArrayParameter + log = logging.getLogger(__name__) @@ -86,6 +86,9 @@ def __init__(self, parent: 'SR86x', name: str) ->None: get_parser=int, unit="kB" ) + self.bytes_per_sample = 4 + self.min_capture_length_in_kb = 1 + self.max_capture_length_in_kb = 4096 self.add_parameter( # Configure which parameters we want to capture "capture_config", @@ -141,8 +144,6 @@ def __init__(self, parent: 'SR86x', name: str) ->None: parameter_class=SR86xBufferReadout ) - self.bytes_per_sample = 4 - def snapshot_base(self, update: bool = False, params_to_skip_update: Sequence[str] = None) -> Dict: if params_to_skip_update is None: @@ -157,17 +158,34 @@ def snapshot_base(self, update: bool = False, snapshot = super().snapshot_base(update, params_to_skip_update) return snapshot - @staticmethod - def _set_capture_len_parser(value: int) -> int: + def _set_capture_len_parser(self, capture_len_in_kb: int) -> int: + """ + According to the manual, the capture length is set in kB. This method checks for the range of the input value, + and throws an error if the range is not satisfied. Additionally, if the input value is odd, it is made even + (and a warning is issued) since the capture length can only be set in even numbers. - if value % 2: - log.warning("the capture length needs to be even. Setting to {}".format(value + 1)) - value += 1 + Args: + capture_len_in_kb (int): The desired capture length in kB. - if not 1 <= value <= 4096: - raise ValueError("the capture length should be between 1 and 4096") + Returns: + capture_len_in_kb (int) + """ + if capture_len_in_kb % 2: + log.warning("the capture length needs to be even. Setting to {}".format(capture_len_in_kb + 1)) + capture_len_in_kb += 1 + + if not self.min_capture_length_in_kb <= capture_len_in_kb <= self.max_capture_length_in_kb: + raise ValueError(f"the capture length should be between " + f"{self.min_capture_length} and {self.max_capture_length_in_kb}") - return value + return capture_len_in_kb + + def set_capture_rate_to_maximum(self) -> None: + """ + Sets the capture rate to maximum. The maximum capture rate is retrieved from the device, and depends on + the current value of the time constant. + """ + self.capture_rate(self.capture_rate_max()) def _set_capture_rate_parser(self, capture_rate_hz: float) -> int: """ @@ -217,27 +235,28 @@ def start_capture(self, acquisition_mode: str, trigger_mode: str) -> None: self.write(cmd_str) def stop_capture(self): - """ - Stop a capture - """ + """Stop a capture""" self.write("CAPTURESTOP") + def _get_list_of_capture_variable_names(self): + """Retrieve the list of names of variables (readouts) that are set to be captured""" + return self.capture_config().split(",") + + def _get_number_of_capture_variables(self): + """Retrieve the number of variables (readouts) that are set to be captured""" + capture_variables = self._get_list_of_capture_variable_names() + n_variables = len(capture_variables) + return n_variables + def _calc_capture_size_in_kb(self, sample_count: int) ->int: """ - Given the number of samples to capture and the capture configuration, calculate the number of kb + Given the number of samples to capture, calculate the capture length that the buffer needs to be set to + in order to fit the requested number of samples. Note that the number of activated readouts + is taken into account. """ - capture_variables = self.capture_config().split(",") - n_variables = len(capture_variables) - + n_variables = self._get_number_of_capture_variables() total_size_in_kb = int(np.ceil(n_variables * sample_count * self.bytes_per_sample / 1024)) - # The number of kb always needs to be even - if total_size_in_kb % 2 == 1: - total_size_in_kb += 1 - - if total_size_in_kb > 64: - raise ValueError("Number of samples specified is larger then the buffer size") - - return total_size_in_kb + return self._set_capture_len_parser(total_size_in_kb) def get_capture_data(self, sample_count: int) -> dict: """ @@ -257,8 +276,8 @@ def get_capture_data(self, sample_count: int) -> dict: values = np.array(values) values = values[values != 0] - capture_variables = self.capture_config().split(",") - n_variables = len(capture_variables) + capture_variables = self._get_list_of_capture_variable_names() + n_variables = self._get_number_of_capture_variables() values = values.reshape((-1, n_variables)).T values = values[:, :sample_count] @@ -271,69 +290,53 @@ def get_capture_data(self, sample_count: int) -> dict: return data - def capture_samples(self, sample_count: int) ->dict: + def capture_one_sample_per_trigger(self, trigger_count: int, start_triggers_pulsetrain: Callable) -> dict: """ - Capture a number of samples. This convenience function provides an example how we use the start and stop - methods. We acquire the samples by sleeping for a time and then reading the buffer. - - Args: - sample_count (int) - - Returns: - dict - """ - capture_rate = self.capture_rate() - capture_time = sample_count / capture_rate - - self.start_capture("CONT", "IMM") - time.sleep(capture_time) - self.stop_capture() - - return self.get_capture_data(sample_count) - - def capture_one_sample_per_trigger(self, trigger_count: int, start_triggers_pulsetrain: Callable) ->dict: - """ - Capture one sample for each trigger and return when the specified number of triggers has been received + Capture one sample per each trigger, and return when the specified number of triggers has been received. Args: trigger_count (int) start_triggers_pulsetrain (callable) - By calling this *non-blocking* function we start a pulse train + By calling this *non-blocking* function, the train of trigger pulses should start """ + # Set buffer size to fit the expected number of samples (one sample per one trigger) total_size_in_kb = self._calc_capture_size_in_kb(trigger_count) - self.capture_length_in_kb(total_size_in_kb) + self.start_capture("ONE", "SAMP") start_triggers_pulsetrain() - n_bytes_captured = 0 - while n_bytes_captured < trigger_count * self.bytes_per_sample: - n_bytes_captured = self.count_capture_bytes() + n_captured_bytes = 0 + n_bytes_to_capture = trigger_count * self.bytes_per_sample + while n_captured_bytes < n_bytes_to_capture: + n_captured_bytes = self.count_capture_bytes() self.stop_capture() return self.get_capture_data(trigger_count) - def start_capture_at_trigger(self, sample_count: int, start_triggers_pulsetrain: Callable) ->dict: + def start_capture_at_trigger(self, sample_count: int, send_trigger: Callable) -> dict: """ Capture a number of samples after a trigger has been received. Please refer to page 135 of the manual - for details + for details. Args: - trigger_count (int) - start_triggers_pulsetrain (callable) - By calling this *non-blocking* function we start a pulse train + sample_count (int) + send_trigger (callable) + By calling this *non-blocking* function, one trigger should be sent that will initiate the capture """ + # Set buffer size to fit the requested number of samples total_size_in_kb = self._calc_capture_size_in_kb(sample_count) - self.capture_length_in_kb(total_size_in_kb) + self.start_capture("ONE", "TRIG") - start_triggers_pulsetrain() + send_trigger() n_bytes_captured = 0 - while n_bytes_captured < total_size_in_kb * 1024: + n_bytes_to_capture = sample_count * self.bytes_per_sample + while n_bytes_captured < n_bytes_to_capture: n_bytes_captured = self.count_capture_bytes() self.stop_capture() From 2e5d393e070b3a10cacec377ea477dcab9726fbb Mon Sep 17 00:00:00 2001 From: sohailc Date: Mon, 25 Jun 2018 20:33:43 +0200 Subject: [PATCH 096/150] Remove unnecessary empty lines --- qcodes/instrument_drivers/stanford_research/SR86x.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/qcodes/instrument_drivers/stanford_research/SR86x.py b/qcodes/instrument_drivers/stanford_research/SR86x.py index 117b9e2eb91..de554320c5f 100644 --- a/qcodes/instrument_drivers/stanford_research/SR86x.py +++ b/qcodes/instrument_drivers/stanford_research/SR86x.py @@ -621,7 +621,6 @@ def __init__(self, name, address, max_frequency, reset=False, **kwargs): get_cmd='OAUX? {}'.format(i), get_parser=float, unit='V') - self.add_parameter('aux_out{}'.format(i), label='Aux output {}'.format(i), get_cmd='AUXV? {}'.format(i), @@ -641,9 +640,6 @@ def __init__(self, name, address, max_frequency, reset=False, **kwargs): self.input_config() self.connect_message() - - - def _set_units(self, unit): for param in [self.X, self.Y, self.R, self.sensitivity]: param.unit = unit From e5528c1c4b44ce79b8cec5f72c0e4023dfb71761 Mon Sep 17 00:00:00 2001 From: sohailc Date: Mon, 25 Jun 2018 20:45:18 +0200 Subject: [PATCH 097/150] Give better name for the reference signal source parameter --- qcodes/instrument_drivers/stanford_research/SR86x.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qcodes/instrument_drivers/stanford_research/SR86x.py b/qcodes/instrument_drivers/stanford_research/SR86x.py index de554320c5f..bfeba3e6d0e 100644 --- a/qcodes/instrument_drivers/stanford_research/SR86x.py +++ b/qcodes/instrument_drivers/stanford_research/SR86x.py @@ -524,14 +524,14 @@ def __init__(self, name, address, max_frequency, reset=False, **kwargs): "SIN": 0, "POS": 1, "POSTTL": 1, + "NEG": 2, "NEGTTL": 2, - "NEG": 2 } ) self.add_parameter( - name="source", - label="Source", + name="reference_source", + label="Reference source", get_cmd="RSRC?", set_cmd="RSRC {}", val_mapping={ From 236563929947eabf1930d918e87a565fae80e848 Mon Sep 17 00:00:00 2001 From: sohailc Date: Mon, 25 Jun 2018 20:55:23 +0200 Subject: [PATCH 098/150] Fix bug due to property name in format string --- qcodes/instrument_drivers/stanford_research/SR86x.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/instrument_drivers/stanford_research/SR86x.py b/qcodes/instrument_drivers/stanford_research/SR86x.py index bfeba3e6d0e..428206dd82c 100644 --- a/qcodes/instrument_drivers/stanford_research/SR86x.py +++ b/qcodes/instrument_drivers/stanford_research/SR86x.py @@ -176,7 +176,7 @@ def _set_capture_len_parser(self, capture_len_in_kb: int) -> int: if not self.min_capture_length_in_kb <= capture_len_in_kb <= self.max_capture_length_in_kb: raise ValueError(f"the capture length should be between " - f"{self.min_capture_length} and {self.max_capture_length_in_kb}") + f"{self.min_capture_length_in_kb} and {self.max_capture_length_in_kb}") return capture_len_in_kb From 06447a9d065a1f89fac5d9e582e5aea2f24edd07 Mon Sep 17 00:00:00 2001 From: sohailc Date: Mon, 25 Jun 2018 21:04:34 +0200 Subject: [PATCH 099/150] Fix bug --- .../stanford_research/SR86x.py | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/qcodes/instrument_drivers/stanford_research/SR86x.py b/qcodes/instrument_drivers/stanford_research/SR86x.py index 428206dd82c..f244848e8ca 100644 --- a/qcodes/instrument_drivers/stanford_research/SR86x.py +++ b/qcodes/instrument_drivers/stanford_research/SR86x.py @@ -170,15 +170,34 @@ def _set_capture_len_parser(self, capture_len_in_kb: int) -> int: Returns: capture_len_in_kb (int) """ - if capture_len_in_kb % 2: - log.warning("the capture length needs to be even. Setting to {}".format(capture_len_in_kb + 1)) - capture_len_in_kb += 1 + return self._parse_capture_length(capture_len_in_kb, issue_warning=True) - if not self.min_capture_length_in_kb <= capture_len_in_kb <= self.max_capture_length_in_kb: + def _parse_capture_length(self, capture_length_in_kb: int, issue_warning: bool=False) -> int: + """ + Parse the capture length in kB according to the way buffer treats it. This method checks for the range + of the input value, and throws an error if the range is not satisfied. Additionally, if the input value + is odd, it is made even (and a warning is issued, if desired) since the capture length can only be set + in even numbers. + + This method intended for use in other methods inside this class for calculation purposes. Setting issue_warning + argument to True allows this method to be used as well for set commands of the instrument parameters. + + Args: + capture_length_in_kb (int): The desired capture length in kB. + issue_warning (bool): If True, a warning is issued when setting capture length to an odd number + Returns: + capture_len_in_kb (int) + """ + if capture_length_in_kb % 2: + capture_length_in_kb += 1 + if issue_warning: + log.warning("the capture length needs to be even. Setting to {}".format(capture_length_in_kb)) + + if not self.min_capture_length_in_kb <= capture_length_in_kb <= self.max_capture_length_in_kb: raise ValueError(f"the capture length should be between " f"{self.min_capture_length_in_kb} and {self.max_capture_length_in_kb}") - return capture_len_in_kb + return capture_length_in_kb def set_capture_rate_to_maximum(self) -> None: """ @@ -256,7 +275,7 @@ def _calc_capture_size_in_kb(self, sample_count: int) ->int: """ n_variables = self._get_number_of_capture_variables() total_size_in_kb = int(np.ceil(n_variables * sample_count * self.bytes_per_sample / 1024)) - return self._set_capture_len_parser(total_size_in_kb) + return self._parse_capture_length(total_size_in_kb, issue_warning=False) def get_capture_data(self, sample_count: int) -> dict: """ From 23b87f0464f04cefb517d18d86c04e0e7f2a6db7 Mon Sep 17 00:00:00 2001 From: MIkhail Astafev Date: Tue, 26 Jun 2018 12:10:10 +0200 Subject: [PATCH 100/150] Reduce line length to 80 characters (PEP8) --- qcodes/tests/common.py | 32 ++++--- .../test_measurement_context_manager.py | 93 +++++++++++-------- 2 files changed, 75 insertions(+), 50 deletions(-) diff --git a/qcodes/tests/common.py b/qcodes/tests/common.py index 830fdaab7cd..2dded1bfb0a 100644 --- a/qcodes/tests/common.py +++ b/qcodes/tests/common.py @@ -12,15 +12,17 @@ def strip_qc(d, keys=('instrument', '__class__')): return d -def retry_until_does_not_throw(exception_class_to_expect: Type[Exception]=AssertionError, - tries: int=5, - delay: float=0.1) -> Callable: +def retry_until_does_not_throw( + exception_class_to_expect: Type[Exception]=AssertionError, + tries: int=5, + delay: float=0.1 +) -> Callable: """ - Call the decorated function given number of times with given delay between the calls - until it does not throw an exception of a given class. + Call the decorated function given number of times with given delay between + the calls until it does not throw an exception of a given class. - If the function throws an exception of a different class, it gets propagated outside - (i.e. the function is not called anymore). + If the function throws an exception of a different class, it gets propagated + outside (i.e. the function is not called anymore). Usage: >> x = False # let's assume that another thread has access to "x", @@ -28,9 +30,11 @@ def retry_until_does_not_throw(exception_class_to_expect: Type[Exception]=Assert >> @retry_until_does_not_throw() ... def assert_x_is_true(): ... assert x, "x is still False..." ... - >> assert_x_is_true() # depending on the settings of "retry_until_does_not_throw", - # it will keep calling the function (with breaks in between) until - # either it does not throw or the number of tries is exceeded. + >> assert_x_is_true() # depending on the settings of + # "retry_until_does_not_throw", it will keep + # calling the function (with breaks in between) + # until either it does not throw or + # the number of tries is exceeded. Args: exception_class_to_expect @@ -41,7 +45,8 @@ def assert_x_is_true(): ... Delay between retries of the function call, in seconds Returns: - A callable that runs the decorated function until it does not throw a given exception + A callable that runs the decorated function until it does not throw + a given exception """ def retry_until_passes_decorator(func: Callable): @@ -54,8 +59,9 @@ def func_retry(*args, **kwargs): except exception_class_to_expect: tries_left -= 1 sleep(delay) - # the very last attempt to call the function is outside the "try-except" clause, - # so that the exception can propagate up the call stack + # the very last attempt to call the function is outside + # the "try-except" clause, so that the exception can propagate + # up the call stack return func(*args, **kwargs) return func_retry diff --git a/qcodes/tests/dataset/test_measurement_context_manager.py b/qcodes/tests/dataset/test_measurement_context_manager.py index d20cbf9fed5..2f5d4bdc849 100644 --- a/qcodes/tests/dataset/test_measurement_context_manager.py +++ b/qcodes/tests/dataset/test_measurement_context_manager.py @@ -351,27 +351,27 @@ def test_subscriptions(experiment, DAC, DMM): """ Test that subscribers are called at the moment the data is flushed to database - Note that for the purpose of this test, flush_data_to_database method is called explicitly instead of waiting for - the data to be flushed automatically after the write_period passes after a add_result call. - - Args: - experiment (qcodes.dataset.experiment_container.Experiment) : qcodes experiment object - DAC (qcodes.instrument.base.Instrument) : dummy instrument object - DMM (qcodes.instrument.base.Instrument) : another dummy instrument object + Note that for the purpose of this test, flush_data_to_database method is + called explicitly instead of waiting for the data to be flushed + automatically after the write_period passes after a add_result call. """ def collect_all_results(results, length, state): """ - Updates the *state* to contain all the *results* acquired during the experiment run + Updates the *state* to contain all the *results* acquired + during the experiment run """ - # Due to the fact that by default subscribers only hold 1 data value in their internal queue, - # this assignment should work (i.e. not overwrite values in the "state" object) assuming that at - # the start of the experiment both the dataset and the *state* objects have the same length. + # Due to the fact that by default subscribers only hold 1 data value + # in their internal queue, this assignment should work (i.e. not + # overwrite values in the "state" object) assuming that at the start + # of the experiment both the dataset and the *state* objects have + # the same length. state[length] = results def collect_values_larger_than_7(results, length, state): """ - Appends to the *state* only the values from *results* that are larger than 7 + Appends to the *state* only the values from *results* + that are larger than 7 """ for result_tuple in results: state += [value for value in result_tuple if value > 7] @@ -380,7 +380,9 @@ def collect_values_larger_than_7(results, length, state): meas.register_parameter(DAC.ch1) meas.register_parameter(DMM.v1, setpoints=(DAC.ch1,)) - all_results_dict = {} # key is the number of the result tuple, value is the result tuple itself + # key is the number of the result tuple, + # value is the result tuple itself + all_results_dict = {} values_larger_than_7 = [] meas.add_subscriber(collect_all_results, state=all_results_dict) @@ -392,7 +394,8 @@ def collect_values_larger_than_7(results, length, state): with meas.run() as datasaver: - # Assert that the measurement, runner, and datasaver have added subscribers to the dataset + # Assert that the measurement, runner, and datasaver + # have added subscribers to the dataset assert len(datasaver._dataset.subscribers) == 2 assert all_results_dict == {} @@ -403,44 +406,57 @@ def collect_values_larger_than_7(results, length, state): for num in range(5): (dac_val, dmm_val) = dac_vals_and_dmm_vals[num] - values_larger_than_7__expected += [val for val in (dac_val, dmm_val) if val > 7] + values_larger_than_7__expected += \ + [val for val in (dac_val, dmm_val) if val > 7] datasaver.add_result((DAC.ch1, dac_val), (DMM.v1, dmm_val)) - # Ensure that data is flushed to the database despite the write period, so that the database triggers - # are executed, which in turn add data to the queues within the subscribers + # Ensure that data is flushed to the database despite the write + # period, so that the database triggers are executed, which in turn + # add data to the queues within the subscribers datasaver.flush_data_to_database() - # In order to make this test deterministic, we need to ensure that just enough time has passed between - # the moment the data is flushed to database and the "state" object (that is passed to subscriber - # constructor) has been updated by the corresponding subscriber's callback function. - # At the moment, there is no robust way to ensure this. The reason is that the subscribers have internal - # queue which is populated via a trigger call from the SQL database, hence from this "main" thread it is - # difficult to say whether the queue is empty because the subscriber callbacks have already been executed - # or because the triggers of the SQL database has not been executed yet. + # In order to make this test deterministic, we need to ensure that + # just enough time has passed between the moment the data is flushed + # to database and the "state" object (that is passed to subscriber + # constructor) has been updated by the corresponding subscriber's + # callback function. At the moment, there is no robust way to ensure + # this. The reason is that the subscribers have internal queue which + # is populated via a trigger call from the SQL database, hence from + # this "main" thread it is difficult to say whether the queue is + # empty because the subscriber callbacks have already been executed + # or because the triggers of the SQL database has not been executed + # yet. # - # In order to overcome this problem, a special decorator is used to wrap the assertions. This is going to - # ensure that some time is given to the Subscriber threads to finish exhausting the queue. - @retry_until_does_not_throw(exception_class_to_expect=AssertionError, delay=0.5, tries=10) + # In order to overcome this problem, a special decorator is used + # to wrap the assertions. This is going to ensure that some time + # is given to the Subscriber threads to finish exhausting the queue. + @retry_until_does_not_throw( + exception_class_to_expect=AssertionError, delay=0.5, tries=10) def assert_states_updated_from_callbacks(): assert values_larger_than_7 == values_larger_than_7__expected - assert list(all_results_dict.keys()) == [result_number for result_number in range(1, num + 1 + 1)] + assert list(all_results_dict.keys()) == \ + [result_index for result_index in range(1, num + 1 + 1)] assert_states_updated_from_callbacks() - # Ensure that after exiting the "run()" context, all subscribers get unsubscribed from the dataset + # Ensure that after exiting the "run()" context, + # all subscribers get unsubscribed from the dataset assert len(datasaver._dataset.subscribers) == 0 - # Ensure that the triggers for each subscriber have been removed from the database + # Ensure that the triggers for each subscriber + # have been removed from the database get_triggers_sql = "SELECT * FROM sqlite_master WHERE TYPE = 'trigger';" - triggers = atomic_transaction(datasaver._dataset.conn, get_triggers_sql).fetchall() + triggers = atomic_transaction( + datasaver._dataset.conn, get_triggers_sql).fetchall() assert len(triggers) == 0 def test_subscribers_called_at_exiting_context_if_queue_is_not_empty(experiment, DAC): """ - Upon quitting the "run()" context, verify that in case the queue is not empty, the subscriber's callback is - still called on that data. This situation is created by setting the minimum length of the queue to a number that is - larger than the number of value written to the dataset. + Upon quitting the "run()" context, verify that in case the queue is + not empty, the subscriber's callback is still called on that data. + This situation is created by setting the minimum length of the queue + to a number that is larger than the number of value written to the dataset. """ def collect_x_vals(results, length, state): """ @@ -459,16 +475,19 @@ def collect_x_vals(results, length, state): given_x_vals = [0, 1, 2, 3] with meas.run() as datasaver: - # Set the minimum queue size of the subscriber to more that the total number of - # values being added to the dataset; this way the subscriber callback is not called before + # Set the minimum queue size of the subscriber to more that + # the total number of values being added to the dataset; + # this way the subscriber callback is not called before # we exit the "run()" context. subscriber = list(datasaver.dataset.subscribers.values())[0] subscriber.min_queue_length = int(len(given_x_vals) + 1) for x in given_x_vals: datasaver.add_result((DAC.ch1, x)) - assert collected_x_vals == [] # Ensure that the subscriber callback is not called yet + # Verify that the subscriber callback is not called yet + assert collected_x_vals == [] + # Verify that the subscriber callback is finally called assert collected_x_vals == given_x_vals From 805933f44de991e414e10d415176ea07e18afc10 Mon Sep 17 00:00:00 2001 From: sohail chatoor Date: Tue, 26 Jun 2018 17:43:14 +0200 Subject: [PATCH 101/150] 1) the routine setting set points will first adjust individual instruments and then update _set_point. 2) get rid of "_set_x", "_set_y", etc. Instead have a single "_set_setpoints" method 3) Make the "test_ramp_down_first" more robust by not checking time stamps any more 4) In the test which tests field limits assert that the set point is not set when an error is raised. --- .../american_magnetics/AMI430.py | 103 +++++++----------- qcodes/tests/drivers/test_ami430.py | 38 ++++--- 2 files changed, 58 insertions(+), 83 deletions(-) diff --git a/qcodes/instrument_drivers/american_magnetics/AMI430.py b/qcodes/instrument_drivers/american_magnetics/AMI430.py index bb151efd19e..d72c8ae08c7 100644 --- a/qcodes/instrument_drivers/american_magnetics/AMI430.py +++ b/qcodes/instrument_drivers/american_magnetics/AMI430.py @@ -570,32 +570,32 @@ def __init__(self, name, instrument_x, instrument_y, # Get and set parameters for the set points of the coordinates self.add_parameter( 'cartesian', - get_cmd=partial(self._get_setpoints, 'x', 'y', 'z'), - set_cmd=self._set_cartesian, + get_cmd=partial(self._get_setpoints, ['x', 'y', 'z']), + set_cmd=partial(self._set_setpoints, ['x', 'y', 'z']), unit='T', vals=Anything() ) self.add_parameter( 'x', - get_cmd=partial(self._get_setpoints, 'x'), - set_cmd=self._set_x, + get_cmd=partial(self._get_setpoints, ['x']), + set_cmd=partial(self._set_setpoints, ['x']), unit='T', vals=Numbers() ) self.add_parameter( 'y', - get_cmd=partial(self._get_setpoints, 'y'), - set_cmd=self._set_y, + get_cmd=partial(self._get_setpoints, ['y']), + set_cmd=partial(self._set_setpoints, ['y']), unit='T', vals=Numbers() ) self.add_parameter( 'z', - get_cmd=partial(self._get_setpoints, 'z'), - set_cmd=self._set_z, + get_cmd=partial(self._get_setpoints, ['z']), + set_cmd=partial(self._set_setpoints, ['z']), unit='T', vals=Numbers() ) @@ -603,36 +603,35 @@ def __init__(self, name, instrument_x, instrument_y, self.add_parameter( 'spherical', get_cmd=partial( - self._get_setpoints, - 'r', - 'theta', - 'phi' + self._get_setpoints, ['r', 'theta', 'phi'] + ), + set_cmd=partial( + self._set_setpoints, ['r', 'theta', 'phi'] ), - set_cmd=self._set_spherical, unit='tuple?', vals=Anything() ) self.add_parameter( 'phi', - get_cmd=partial(self._get_setpoints, 'phi'), - set_cmd=self._set_phi, + get_cmd=partial(self._get_setpoints, ['phi']), + set_cmd=partial(self._set_setpoints, ['phi']), unit='deg', vals=Numbers() ) self.add_parameter( 'theta', - get_cmd=partial(self._get_setpoints, 'theta'), - set_cmd=self._set_theta, + get_cmd=partial(self._get_setpoints, ['theta']), + set_cmd=partial(self._set_setpoints, ['theta']), unit='deg', vals=Numbers() ) self.add_parameter( 'field', - get_cmd=partial(self._get_setpoints, 'r'), - set_cmd=self._set_r, + get_cmd=partial(self._get_setpoints, ['r']), + set_cmd=partial(self._set_setpoints, ['r']), unit='T', vals=Numbers() ) @@ -640,20 +639,19 @@ def __init__(self, name, instrument_x, instrument_y, self.add_parameter( 'cylindrical', get_cmd=partial( - self._get_setpoints, - 'rho', - 'phi', - 'z' + self._get_setpoints, ['rho', 'phi', 'z'] + ), + set_cmd=partial( + self._set_setpoints, ['rho', 'phi', 'z'] ), - set_cmd=self._set_cylindrical, unit='tuple?', vals=Anything() ) self.add_parameter( 'rho', - get_cmd=partial(self._get_setpoints, 'rho'), - set_cmd=self._set_rho, + get_cmd=partial(self._get_setpoints, ['rho']), + set_cmd=partial(self._set_setpoints, ['rho']), unit='T', vals=Numbers() ) @@ -668,7 +666,7 @@ def _verify_safe_setpoint(self, setpoint_values): return answer - def _set_fields(self, values): + def _adjust_child_instruments(self, values): """ Set the fields of the x/y/z magnets. This function is called whenever the field is changed and performs several safety checks @@ -750,7 +748,7 @@ def _get_measured(self, *names): return return_value - def _get_setpoints(self, *names): + def _get_setpoints(self, names): measured_values = self._set_point.get_components(*names) @@ -766,45 +764,20 @@ def _get_setpoints(self, *names): return return_value - def _set_cartesian(self, values): - x, y, z = values - self._set_point.set_vector(x=x, y=y, z=z) - self._set_fields(self._set_point.get_components("x", "y", "z")) - - def _set_x(self, x): - self._set_point.set_component(x=x) - self._set_fields(self._set_point.get_components("x", "y", "z")) + def _set_setpoints(self, names, values): - def _set_y(self, y): - self._set_point.set_component(y=y) - self._set_fields(self._set_point.get_components("x", "y", "z")) + kwargs = dict(zip(names, np.atleast_1d(values))) - def _set_z(self, z): - self._set_point.set_component(z=z) - self._set_fields(self._set_point.get_components("x", "y", "z")) - - def _set_spherical(self, values): - r, theta, phi = values - self._set_point.set_vector(r=r, theta=theta, phi=phi) - self._set_fields(self._set_point.get_components("x", "y", "z")) - - def _set_r(self, r): - self._set_point.set_component(r=r) - self._set_fields(self._set_point.get_components("x", "y", "z")) - - def _set_theta(self, theta): - self._set_point.set_component(theta=theta) - self._set_fields(self._set_point.get_components("x", "y", "z")) + set_point = FieldVector() + set_point.copy(self._set_point) + if len(kwargs) == 3: + set_point.set_vector(**kwargs) + else: + set_point.set_component(**kwargs) - def _set_phi(self, phi): - self._set_point.set_component(phi=phi) - self._set_fields(self._set_point.get_components("x", "y", "z")) + self._adjust_child_instruments( + set_point.get_components("x", "y", "z") + ) - def _set_cylindrical(self, values): - rho, phi, z = values - self._set_point.set_vector(rho=rho, phi=phi, z=z) - self._set_fields(self._set_point.get_components("x", "y", "z")) + self._set_point = set_point - def _set_rho(self, rho): - self._set_point.set_component(rho=rho) - self._set_fields(self._set_point.get_components("x", "y", "z")) diff --git a/qcodes/tests/drivers/test_ami430.py b/qcodes/tests/drivers/test_ami430.py index db63a212717..90ee0facac2 100644 --- a/qcodes/tests/drivers/test_ami430.py +++ b/qcodes/tests/drivers/test_ami430.py @@ -1,5 +1,6 @@ import io import numpy as np +import re import pytest from hypothesis import given, settings from hypothesis.strategies import floats @@ -263,19 +264,18 @@ def test_measured(current_driver, set_target): cartesian_z]) -def get_time_dict(messages: List[str]) -> Dict[str, float]: - """ - Helper function used in test_ramp_down_first - """ - relevant_messages = [line for line in messages - if 'CONF:FIELD:TARG' in line] - time_dict = {} - for rm in relevant_messages: - time = rm.split(' - ')[0] - name = rm[rm.find(':')-1: rm.find(':')] - time_dict.update({name: float(time)}) +def get_ramp_down_order(messages: List[str]) -> List[str]: + order = [] + + for msg in messages: + if "CONF:FIELD:TARG" not in msg: + continue - return time_dict + g = re.search("(.): CONF:FIELD:TARG", msg) + name = g.groups()[0] + order.append(name) + + return order def test_ramp_down_first(current_driver): @@ -307,12 +307,10 @@ def test_ramp_down_first(current_driver): current_driver.cartesian(set_point) # get the logging outputs from the instruments. messages = get_messages(iostream) - # extract the time stamps - times = get_time_dict(messages) - - for ramp_up_name in np.delete(names, count): - # ramp down occurs before ramp up. - assert times[ramp_down_name] < times[ramp_up_name] + # get the order in which the ramps down occur + order = get_ramp_down_order(messages) + # the first one should be the one for which delta < 0 + assert order[0] == names[count] def test_field_limit_exception(current_driver): @@ -341,6 +339,9 @@ def test_field_limit_exception(current_driver): current_driver.cartesian(set_point) assert "field would exceed limit" in excinfo.value.args[0] + assert not all([a == b for a, b in zip( + current_driver.cartesian(), set_point + )]) def test_cylindrical_poles(current_driver): @@ -412,3 +413,4 @@ def test_ramp_rate_exception(current_driver): errmsg = "must be between 0 and {} inclusive".format(max_ramp_rate) assert errmsg in excinfo.value.args[0] + From 76359c56edc9c93f44f55e59eea0225c26ce8f5d Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 27 Jun 2018 10:30:53 +0200 Subject: [PATCH 102/150] pin hypothesis to less than 3.62 which adds problematic typehints --- test_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_requirements.txt b/test_requirements.txt index 52f47da737c..a875dfae708 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -2,7 +2,7 @@ coverage!=4.5.0 pytest-cov pytest codacy-coverage -hypothesis +hypothesis==3.61.0 # type annotation failures with mypy for 3.62.0 and later mypy!=0.570 # due to https://github.com/python/mypy/issues/4674 git+https://github.com/QCoDeS/pyvisa-sim.git git+https://github.com/QCoDeS/broadbean From fd4fb4ac94faf08d94c838b051d19ee2832f1083 Mon Sep 17 00:00:00 2001 From: sohailc Date: Thu, 14 Jun 2018 16:52:37 +0200 Subject: [PATCH 103/150] Fix maximum frequency for SRS860 - it is 500 kHz --- qcodes/instrument_drivers/stanford_research/SR860.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/instrument_drivers/stanford_research/SR860.py b/qcodes/instrument_drivers/stanford_research/SR860.py index 45449dab524..8ee9c6142e4 100644 --- a/qcodes/instrument_drivers/stanford_research/SR860.py +++ b/qcodes/instrument_drivers/stanford_research/SR860.py @@ -6,4 +6,4 @@ class SR860(SR86x): The SR860 instrument is almost equal to the SR865, except for the max frequency """ def __init__(self, name: str, address: str, reset: bool=False, **kwargs: str) ->None: - super().__init__(name, address, max_frequency=5E3, reset=reset, **kwargs) + super().__init__(name, address, max_frequency=500e3, reset=reset, **kwargs) From 72b45eb8dd7a00cb0102000b7398b51c7d251d21 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 27 Jun 2018 13:27:30 +0200 Subject: [PATCH 104/150] increase timeout for test_nested_loop_over_channels Appveyor can be really slow for these tests --- qcodes/tests/test_channels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/tests/test_channels.py b/qcodes/tests/test_channels.py index 241f00549b2..829a8c9e53f 100644 --- a/qcodes/tests/test_channels.py +++ b/qcodes/tests/test_channels.py @@ -288,7 +288,7 @@ def test_loop_measure_channels_by_name(self, values): @given(loop_channels=hst.lists(hst.integers(0, 3), min_size=2, max_size=2, unique=True), measure_channel=hst.integers(0, 3)) - @settings(max_examples=10, deadline=400) + @settings(max_examples=10, deadline=800) def test_nested_loop_over_channels(self, loop_channels, measure_channel): channel_to_label = {0: 'A', 1: 'B', 2: 'C', 3: "D"} loop = Loop(self.instrument.channels[loop_channels[0]].temperature.sweep(0, 10, 0.5)) From 44fdc8cb5a1197e0ee2aab355ead351ac39eae74 Mon Sep 17 00:00:00 2001 From: sohailc Date: Wed, 27 Jun 2018 15:03:26 +0200 Subject: [PATCH 105/150] Substitute warning with excpetion in set capture length parser --- .../stanford_research/SR86x.py | 56 ++++++++----------- 1 file changed, 22 insertions(+), 34 deletions(-) diff --git a/qcodes/instrument_drivers/stanford_research/SR86x.py b/qcodes/instrument_drivers/stanford_research/SR86x.py index f244848e8ca..873e3663dc3 100644 --- a/qcodes/instrument_drivers/stanford_research/SR86x.py +++ b/qcodes/instrument_drivers/stanford_research/SR86x.py @@ -158,44 +158,27 @@ def snapshot_base(self, update: bool = False, snapshot = super().snapshot_base(update, params_to_skip_update) return snapshot - def _set_capture_len_parser(self, capture_len_in_kb: int) -> int: + def _set_capture_len_parser(self, capture_length_in_kb: int) -> int: """ - According to the manual, the capture length is set in kB. This method checks for the range of the input value, - and throws an error if the range is not satisfied. Additionally, if the input value is odd, it is made even - (and a warning is issued) since the capture length can only be set in even numbers. + Parse the capture length in kB according to the way buffer treats it + (refer to the manual for details). The given value has to fit in the + range and has to be even, otherwise this function raises exceptions. Args: - capture_len_in_kb (int): The desired capture length in kB. + capture_length_in_kb: The desired capture length in kB. Returns: - capture_len_in_kb (int) - """ - return self._parse_capture_length(capture_len_in_kb, issue_warning=True) - - def _parse_capture_length(self, capture_length_in_kb: int, issue_warning: bool=False) -> int: - """ - Parse the capture length in kB according to the way buffer treats it. This method checks for the range - of the input value, and throws an error if the range is not satisfied. Additionally, if the input value - is odd, it is made even (and a warning is issued, if desired) since the capture length can only be set - in even numbers. - - This method intended for use in other methods inside this class for calculation purposes. Setting issue_warning - argument to True allows this method to be used as well for set commands of the instrument parameters. - - Args: - capture_length_in_kb (int): The desired capture length in kB. - issue_warning (bool): If True, a warning is issued when setting capture length to an odd number - Returns: - capture_len_in_kb (int) + capture_length_in_kb """ if capture_length_in_kb % 2: - capture_length_in_kb += 1 - if issue_warning: - log.warning("the capture length needs to be even. Setting to {}".format(capture_length_in_kb)) + raise ValueError("The capture length should be an even number") - if not self.min_capture_length_in_kb <= capture_length_in_kb <= self.max_capture_length_in_kb: - raise ValueError(f"the capture length should be between " - f"{self.min_capture_length_in_kb} and {self.max_capture_length_in_kb}") + if not self.min_capture_length_in_kb \ + <= capture_length_in_kb \ + <= self.max_capture_length_in_kb: + raise ValueError(f"The capture length should be between " + f"{self.min_capture_length_in_kb} and " + f"{self.max_capture_length_in_kb}") return capture_length_in_kb @@ -269,13 +252,18 @@ def _get_number_of_capture_variables(self): def _calc_capture_size_in_kb(self, sample_count: int) ->int: """ - Given the number of samples to capture, calculate the capture length that the buffer needs to be set to - in order to fit the requested number of samples. Note that the number of activated readouts - is taken into account. + Given the number of samples to capture, calculate the capture length + that the buffer needs to be set to in order to fit the requested + number of samples. Note that the number of activated readouts is + taken into account. """ n_variables = self._get_number_of_capture_variables() total_size_in_kb = int(np.ceil(n_variables * sample_count * self.bytes_per_sample / 1024)) - return self._parse_capture_length(total_size_in_kb, issue_warning=False) + # Make sure that the total size in kb is an even number, as expected by + # the instrument + if total_size_in_kb % 2: + total_size_in_kb += 1 + return total_size_in_kb def get_capture_data(self, sample_count: int) -> dict: """ From 780073eb8e27c551dd10119a49adfee09e2a5a32 Mon Sep 17 00:00:00 2001 From: sohailc Date: Wed, 27 Jun 2018 15:15:21 +0200 Subject: [PATCH 106/150] Reimplement getting capture data correctly - _get_raw_capture_data_block implements the direct low-level call to CAPTUREGET command of the instrument - _get_raw_capture_data allows to get data from the beginning of the buffer avoiding the instrument's 64kB-reading-per-time limit - get_capture_data uses the above functions; it does not have the 64kB limit anymore, instead it's limit is the current capture length (via _get_raw_capture_data function) --- .../stanford_research/SR86x.py | 153 ++++++++++++++++-- 1 file changed, 137 insertions(+), 16 deletions(-) diff --git a/qcodes/instrument_drivers/stanford_research/SR86x.py b/qcodes/instrument_drivers/stanford_research/SR86x.py index 873e3663dc3..e9678378d76 100644 --- a/qcodes/instrument_drivers/stanford_research/SR86x.py +++ b/qcodes/instrument_drivers/stanford_research/SR86x.py @@ -87,8 +87,10 @@ def __init__(self, parent: 'SR86x', name: str) ->None: unit="kB" ) self.bytes_per_sample = 4 - self.min_capture_length_in_kb = 1 - self.max_capture_length_in_kb = 4096 + self.min_capture_length_in_kb = 1 # i.e. minimum buffer size + self.max_capture_length_in_kb = 4096 # i.e. maximum buffer size + # Maximum amount of kB that can be read per single CAPTUREGET command + self.max_size_per_reading_in_kb = 64 self.add_parameter( # Configure which parameters we want to capture "capture_config", @@ -125,17 +127,25 @@ def __init__(self, parent: 'SR86x', name: str) ->None: self.add_parameter( "count_capture_bytes", - label="capture bytes", + label="captured bytes", get_cmd="CAPTUREBYTES?", unit="B", - get_parser=int + get_parser=int, + docstring="Number of bytes captured so far in the buffer. Can be " + "used to track live progress." ) self.add_parameter( "count_capture_kilobytes", - label="capture kilobytes", + label="captured kilobytes", get_cmd="CAPTUREPROG?", - unit="kB" + unit="kB", + docstring="Number of kilobytes captured so far in the buffer, " + "rounded-up to 2 kilobyte chunks. Capture must be " + "stopped before requesting the value of this " + "parameter. If the acquisition wrapped during operating " + "in Continuous mode, then the returned value is " + "simply equal to the current capture length." ) for parameter_name in ["X", "Y", "R", "T"]: @@ -267,25 +277,30 @@ def _calc_capture_size_in_kb(self, sample_count: int) ->int: def get_capture_data(self, sample_count: int) -> dict: """ - Read capture data from the buffer. + Read the given number of samples of the capture data from the buffer. Args: - sample_count (int): number of samples to read from the buffer + sample_count + number of samples to read from the buffer Returns: - data (dict): The keys in the dictionary is the variables we have captures. For instance, if before the - capture we specify 'capture_config("X,Y")', then the keys will be "X" and "Y". + data + The keys in the dictionary correspond to the captured + variables. For instance, if before the capture, the capture + config was set as 'capture_config("X,Y")', then the keys will + be "X" and "Y". The values in the dictionary are numpy arrays + of numbers. """ total_size_in_kb = self._calc_capture_size_in_kb(sample_count) - - values = self._parent.visa_handle.query_binary_values("CAPTUREGET? 0,{}".format(total_size_in_kb), - datatype='f', is_big_endian=False) - values = np.array(values) - values = values[values != 0] - capture_variables = self._get_list_of_capture_variable_names() n_variables = self._get_number_of_capture_variables() + values = self._get_raw_capture_data(total_size_in_kb) + + # Remove zeros which mark the end part of the buffer that is not + # filled with captured data + values = values[values != 0] + values = values.reshape((-1, n_variables)).T values = values[:, :sample_count] @@ -297,6 +312,112 @@ def get_capture_data(self, sample_count: int) -> dict: return data + def _get_raw_capture_data(self, size_in_kb: int) -> np.ndarray: + """ + Read data from the buffer from its beginning avoiding the instrument + limit of 64 kilobytes per reading. + + Args: + size_in_kb + Size of the data that needs to be read; if it exceeds the + capture length, an exception is raised. + + Returns: + A one-dimensional numpy array of the requested data. Note that the + returned array contains data for all the variables that are + mentioned in the capture config. + """ + current_capture_length = self.capture_length_in_kb() + if size_in_kb > current_capture_length: + raise ValueError(f"The size of the requested data ({size_in_kb}kB) " + f"is larger than current capture length of the " + f"buffer ({current_capture_length}kB).") + + values = np.array([]) + data_size_to_read_in_kb = size_in_kb + n_readings = 0 + + while data_size_to_read_in_kb > 0: + offset = n_readings * self.max_size_per_reading_in_kb + + if data_size_to_read_in_kb > self.max_size_per_reading_in_kb: + size_of_this_reading = self.max_size_per_reading_in_kb + else: + size_of_this_reading = data_size_to_read_in_kb + + data_from_this_reading = self._get_raw_capture_data_block( + size_of_this_reading, + offset_in_kb=offset) + values = np.append(values, data_from_this_reading) + + data_size_to_read_in_kb -= size_of_this_reading + n_readings += 1 + + return values + + def _get_raw_capture_data_block(self, + size_in_kb: int, + offset_in_kb: int=0 + ) -> np.ndarray: + """ + Read data from the buffer. The maximum amount of data that can be + read with this function (size_in_kb) is 64kB (this limitation comes + from the instrument). The offset argument can be used to navigate + along the buffer. + + An exception will be raised if either size_in_kb or offset_in_kb are + longer that the *current* capture length (number of kB of data that is + captured so far rounded up to 2kB chunks). If (offset_in_kb + + size_in_kb) is longer than the *current* capture length, + the instrument returns the wrapped data. + + For more information, refer to the description of the "CAPTUREGET" + command in the manual. + + Args: + size_in_kb + Amount of data in kB that is to be read from the buffer + offset_in_kb + Offset within the buffer of where to read the data; for + example, when 0 is specified, the data is read from the start + of the buffer + + Returns: + A one-dimensional numpy array of the requested data. Note that the + returned array contains data for all the variables that are + mentioned in the capture config. + """ + if size_in_kb > self.max_size_per_reading_in_kb: + raise ValueError(f"The size of the requested data ({size_in_kb}kB) " + f"is larger than maximum size that can be read " + f"at once ({self.max_size_per_reading_in_kb}kB).") + + # Calculate the size of the data captured so far, in kB, rounded up + # to 2kB chunks + size_of_currently_captured_data = int( + np.ceil(np.ceil(self.count_capture_bytes() / 1024) / 2) * 2 + ) + + if size_in_kb > size_of_currently_captured_data: + raise ValueError(f"The size of the requested data ({size_in_kb}kB) " + f"cannot be larger than the size of currently " + f"captured data rounded up to 2kB chunks " + f"({size_of_currently_captured_data}kB)") + + if offset_in_kb > size_of_currently_captured_data: + raise ValueError(f"The offset for reading the requested data " + f"({offset_in_kb}kB) cannot be larger than the " + f"size of currently captured data rounded up to " + f"2kB chunks " + f"({size_of_currently_captured_data}kB)") + + values = self._parent.visa_handle.query_binary_values( + f"CAPTUREGET? {offset_in_kb}, {size_in_kb}", + datatype='f', + is_big_endian=False) + + return np.array(values) + def capture_one_sample_per_trigger(self, trigger_count: int, start_triggers_pulsetrain: Callable) -> dict: """ Capture one sample per each trigger, and return when the specified number of triggers has been received. From 442d24185ab45be994eec12da08136f13b84bb02 Mon Sep 17 00:00:00 2001 From: sohailc Date: Wed, 27 Jun 2018 15:27:16 +0200 Subject: [PATCH 107/150] Improve code style of the driver (PEP8) --- .../stanford_research/SR86x.py | 162 ++++++++++++------ 1 file changed, 109 insertions(+), 53 deletions(-) diff --git a/qcodes/instrument_drivers/stanford_research/SR86x.py b/qcodes/instrument_drivers/stanford_research/SR86x.py index e9678378d76..e636f93f1a5 100644 --- a/qcodes/instrument_drivers/stanford_research/SR86x.py +++ b/qcodes/instrument_drivers/stanford_research/SR86x.py @@ -13,13 +13,14 @@ class SR86xBufferReadout(ArrayParameter): """ - The parameter array that holds read out data. We need this to be compatible with qcodes.Measure - - Args - ---- - name (str) - instrument (SR86x): This argument is unused, but needed because the add_parameter method of the Instrument - base class adds this as a kwarg. + The parameter array that holds read out data. We need this to be compatible + with qcodes.Measure + + Args: + name + instrument + This argument is unused, but needed because the add_parameter + method of the Instrument base class adds this as a kwarg. """ def __init__(self, name: str, instrument: 'SR86x') ->None: @@ -33,18 +34,18 @@ def __init__(self, name: str, instrument: 'SR86x') ->None: setpoint_names=('Time',), setpoint_labels=('Time',), setpoint_units=('s',), - docstring='Holds an acquired (part of the) data buffer of one channel.') + docstring='Holds an acquired (part of the) data ' + 'buffer of one channel.') self.name = name self._capture_data = None - def prepare_readout(self, capture_data: np.array) ->None: + def prepare_readout(self, capture_data: np.array) -> None: """ Prepare this parameter for readout. - Args - ---- - capture_data (np.array) + Args: + capture_data """ self._capture_data = capture_data @@ -55,22 +56,24 @@ def prepare_readout(self, capture_data: np.array) ->None: self.setpoint_labels = ('Sample number',) self.setpoints = (tuple(np.arange(0, data_len)),) - def get_raw(self) ->np.ndarray: + def get_raw(self) -> np.ndarray: """ Public method to access the capture data """ if self._capture_data is None: - err_str = "Cannot return data for parameter {}. Please prepare for ".format(self.name) - err_str = err_str + "readout by calling 'get_capture_data' with appropriate configuration settings" - raise ValueError(err_str) + raise ValueError(f"Cannot return data for parameter {self.name}. " + f"Please prepare for readout by calling " + f"'get_capture_data' with appropriate " + f"configuration settings") return self._capture_data class SR86xBuffer(InstrumentChannel): """ - The buffer module for the SR86x driver. This driver has been verified to work with the SR860 and SR865. - For reference, please consult the SR860 manual: http://thinksrs.com/downloads/PDFs/Manuals/SR860m.pdf + The buffer module for the SR86x driver. This driver has been verified to + work with the SR860 and SR865. For reference, please consult the SR860 + manual: http://thinksrs.com/downloads/PDFs/Manuals/SR860m.pdf """ def __init__(self, parent: 'SR86x', name: str) ->None: @@ -194,56 +197,70 @@ def _set_capture_len_parser(self, capture_length_in_kb: int) -> int: def set_capture_rate_to_maximum(self) -> None: """ - Sets the capture rate to maximum. The maximum capture rate is retrieved from the device, and depends on - the current value of the time constant. + Sets the capture rate to maximum. The maximum capture rate is + retrieved from the device, and depends on the current value of the + time constant. """ self.capture_rate(self.capture_rate_max()) def _set_capture_rate_parser(self, capture_rate_hz: float) -> int: """ - According to the manual, the capture rate query returns a value in Hz, but then setting this value it is - expected to give a value n, where the capture rate in Hz is given by capture_rate_hz = max_rate / 2 ** n. - Please see page 136 of the manual. Here n is an integer in the range [0, 20] + According to the manual, the capture rate query returns a value in + Hz, but then setting this value it is expected to give a value n, + where the capture rate in Hz is given by + capture_rate_hz = max_rate / 2 ** n. Please see page 136 of the + manual. Here n is an integer in the range [0, 20]. Args: - capture_rate_hz (float): The desired capture rate in Hz. If the desired rate is more then 1 Hz from the - nearest valid rate, a warning is issued and the nearest valid rate it used. + capture_rate_hz + The desired capture rate in Hz. If the desired rate is more + than 1 Hz from the nearest valid rate, a warning is issued + and the nearest valid rate it used. Returns: - n_round (int) + n_round """ max_rate = self.capture_rate_max() n = np.log2(max_rate / capture_rate_hz) n_round = int(round(n)) if not 0 <= n_round <= 20: - raise ValueError("The chosen frequency is invalid. Please consult the SR860 manual at page 136. The maximum" - " capture rate is {}".format(max_rate)) + raise ValueError(f"The chosen frequency is invalid. Please " + f"consult the SR860 manual at page 136. " + f"The maximum capture rate is {max_rate}") nearest_valid_rate = max_rate / 2 ** n_round if abs(capture_rate_hz - nearest_valid_rate) > 1: - log.warning("Warning: Setting capture rate to {:.5} Hz".format(nearest_valid_rate)) - available_frequencies = ", ".join([str(f) for f in self.available_frequencies]) - log.warning("The available frequencies are: {}".format(available_frequencies)) + available_frequencies = ", ".join( + [str(f) for f in self.available_frequencies]) + log.warning("Warning: Setting capture rate to {:.5} Hz" + .format(nearest_valid_rate)) + log.warning("The available frequencies are: {}" + .format(available_frequencies)) return n_round def start_capture(self, acquisition_mode: str, trigger_mode: str) -> None: """ - Start an acquisition. Please see page 137 of the manual for a detailed explanation. + Start an acquisition. Please see page 137 of the manual for a detailed + explanation. Args: - acquisition_mode (str): "ONE" | "CONT". - trigger_mode (str): IMM | TRIG | SAMP + acquisition_mode + "ONE" | "CONT" + trigger_mode + "IMM" | "TRIG" | "SAMP" """ if acquisition_mode not in ["ONE", "CONT"]: - raise ValueError("The acquisition mode needs to be either 'ONE' or 'CONT'") + raise ValueError( + "The acquisition mode needs to be either 'ONE' or 'CONT'") if trigger_mode not in ["IMM", "TRIG", "SAMP"]: - raise ValueError("The trigger mode needs to be either 'IMM', 'TRIG' or 'SAMP'") + raise ValueError( + "The trigger mode needs to be either 'IMM', 'TRIG' or 'SAMP'") - cmd_str = "CAPTURESTART {},{}".format(acquisition_mode, trigger_mode) + cmd_str = f"CAPTURESTART {acquisition_mode}, {trigger_mode}" self.write(cmd_str) def stop_capture(self): @@ -251,11 +268,17 @@ def stop_capture(self): self.write("CAPTURESTOP") def _get_list_of_capture_variable_names(self): - """Retrieve the list of names of variables (readouts) that are set to be captured""" + """ + Retrieve the list of names of variables (readouts) that are + set to be captured + """ return self.capture_config().split(",") def _get_number_of_capture_variables(self): - """Retrieve the number of variables (readouts) that are set to be captured""" + """ + Retrieve the number of variables (readouts) that are + set to be captured + """ capture_variables = self._get_list_of_capture_variable_names() n_variables = len(capture_variables) return n_variables @@ -268,7 +291,9 @@ def _calc_capture_size_in_kb(self, sample_count: int) ->int: taken into account. """ n_variables = self._get_number_of_capture_variables() - total_size_in_kb = int(np.ceil(n_variables * sample_count * self.bytes_per_sample / 1024)) + total_size_in_kb = int( + np.ceil(n_variables * sample_count * self.bytes_per_sample / 1024) + ) # Make sure that the total size in kb is an even number, as expected by # the instrument if total_size_in_kb % 2: @@ -418,16 +443,31 @@ def _get_raw_capture_data_block(self, return np.array(values) - def capture_one_sample_per_trigger(self, trigger_count: int, start_triggers_pulsetrain: Callable) -> dict: + def capture_one_sample_per_trigger(self, + trigger_count: int, + start_triggers_pulsetrain: Callable + ) -> dict: """ - Capture one sample per each trigger, and return when the specified number of triggers has been received. + Capture one sample per each trigger, and return when the specified + number of triggers has been received. Args: - trigger_count (int) - start_triggers_pulsetrain (callable) - By calling this *non-blocking* function, the train of trigger pulses should start + trigger_count + Number of triggers to capture samples for + start_triggers_pulsetrain + By calling this *non-blocking* function, the train of trigger + pulses should start + + Returns: + data + The keys in the dictionary correspond to the captured + variables. For instance, if before the capture, the capture + config was set as 'capture_config("X,Y")', then the keys will + be "X" and "Y". The values in the dictionary are numpy arrays + of numbers. """ - # Set buffer size to fit the expected number of samples (one sample per one trigger) + # Set buffer size to fit the expected number of samples + # (one sample per one trigger) total_size_in_kb = self._calc_capture_size_in_kb(trigger_count) self.capture_length_in_kb(total_size_in_kb) @@ -444,15 +484,28 @@ def capture_one_sample_per_trigger(self, trigger_count: int, start_triggers_puls return self.get_capture_data(trigger_count) - def start_capture_at_trigger(self, sample_count: int, send_trigger: Callable) -> dict: + def start_capture_at_trigger(self, + sample_count: int, + send_trigger: Callable + ) -> dict: """ - Capture a number of samples after a trigger has been received. Please refer to page 135 of the manual - for details. + Capture a number of samples after a trigger has been received. + Please refer to page 135 of the manual for details. Args: - sample_count (int) - send_trigger (callable) - By calling this *non-blocking* function, one trigger should be sent that will initiate the capture + sample_count + Number of samples to capture + send_trigger + By calling this *non-blocking* function, one trigger should + be sent that will initiate the capture + + Returns: + data + The keys in the dictionary correspond to the captured + variables. For instance, if before the capture, the capture + config was set as 'capture_config("X,Y")', then the keys will + be "X" and "Y". The values in the dictionary are numpy arrays + of numbers. """ # Set buffer size to fit the requested number of samples total_size_in_kb = self._calc_capture_size_in_kb(sample_count) @@ -519,7 +572,10 @@ def __init__(self, name, address, max_frequency, reset=False, **kwargs): get_cmd='FREQ?', set_cmd='FREQ {}', get_parser=float, - vals=Numbers(min_value=1e-3, max_value=self._max_frequency)) + vals=Numbers( + min_value=1e-3, + max_value=self._max_frequency) + ) self.add_parameter(name='sine_outdc', label='Sine out dc level', unit='V', From 11dbe5d9ce20da57ecd37daffcae378695f9dc60 Mon Sep 17 00:00:00 2001 From: sohailc Date: Thu, 28 Jun 2018 13:59:58 +0200 Subject: [PATCH 108/150] Extract duplicate code into convenient functions - set_capture_length_to_fit_samples - wait_until_samples_captured --- .../stanford_research/SR86x.py | 56 ++++++++++--------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/qcodes/instrument_drivers/stanford_research/SR86x.py b/qcodes/instrument_drivers/stanford_research/SR86x.py index e636f93f1a5..7760367ae58 100644 --- a/qcodes/instrument_drivers/stanford_research/SR86x.py +++ b/qcodes/instrument_drivers/stanford_research/SR86x.py @@ -300,6 +300,33 @@ def _calc_capture_size_in_kb(self, sample_count: int) ->int: total_size_in_kb += 1 return total_size_in_kb + def set_capture_length_to_fit_samples(self, sample_count: int) -> None: + """ + Set the capture length of the buffer to fit the given number of + samples. + + Args: + sample_count + Number of samples that the buffer has to fit + """ + total_size_in_kb = self._calc_capture_size_in_kb(sample_count) + self.capture_length_in_kb(total_size_in_kb) + + def wait_until_samples_captured(self, sample_count: int) -> None: + """ + Wait until the given number of samples is captured. This function + is blocking and has to be used with caution because it does not have + a timeout. + + Args: + sample_count + Number of samples that needs to be captured + """ + n_captured_bytes = 0 + n_bytes_to_capture = sample_count * self.bytes_per_sample + while n_captured_bytes < n_bytes_to_capture: + n_captured_bytes = self.count_capture_bytes() + def get_capture_data(self, sample_count: int) -> dict: """ Read the given number of samples of the capture data from the buffer. @@ -466,22 +493,11 @@ def capture_one_sample_per_trigger(self, be "X" and "Y". The values in the dictionary are numpy arrays of numbers. """ - # Set buffer size to fit the expected number of samples - # (one sample per one trigger) - total_size_in_kb = self._calc_capture_size_in_kb(trigger_count) - self.capture_length_in_kb(total_size_in_kb) - + self.set_capture_length_to_fit_samples(trigger_count) self.start_capture("ONE", "SAMP") - start_triggers_pulsetrain() - - n_captured_bytes = 0 - n_bytes_to_capture = trigger_count * self.bytes_per_sample - while n_captured_bytes < n_bytes_to_capture: - n_captured_bytes = self.count_capture_bytes() - + self.wait_until_samples_captured(trigger_count) self.stop_capture() - return self.get_capture_data(trigger_count) def start_capture_at_trigger(self, @@ -507,21 +523,11 @@ def start_capture_at_trigger(self, be "X" and "Y". The values in the dictionary are numpy arrays of numbers. """ - # Set buffer size to fit the requested number of samples - total_size_in_kb = self._calc_capture_size_in_kb(sample_count) - self.capture_length_in_kb(total_size_in_kb) - + self.set_capture_length_to_fit_samples(sample_count) self.start_capture("ONE", "TRIG") - send_trigger() - - n_bytes_captured = 0 - n_bytes_to_capture = sample_count * self.bytes_per_sample - while n_bytes_captured < n_bytes_to_capture: - n_bytes_captured = self.count_capture_bytes() - + self.wait_until_samples_captured(sample_count) self.stop_capture() - return self.get_capture_data(sample_count) From bc0c36e5d1f3818c1bd8d4f8ed48ed88f6b48271 Mon Sep 17 00:00:00 2001 From: sohailc Date: Mon, 2 Jul 2018 15:54:53 +0200 Subject: [PATCH 109/150] Correct parameter name, add docstring for clarity --- qcodes/instrument_drivers/stanford_research/SR86x.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/qcodes/instrument_drivers/stanford_research/SR86x.py b/qcodes/instrument_drivers/stanford_research/SR86x.py index 7760367ae58..251c4268d3f 100644 --- a/qcodes/instrument_drivers/stanford_research/SR86x.py +++ b/qcodes/instrument_drivers/stanford_research/SR86x.py @@ -706,8 +706,8 @@ def __init__(self, name, address, max_frequency, reset=False, **kwargs): 10e3: 20, 30e3: 21}) self.add_parameter( - name="trigger_reference", - label="Trigger Reference", + name="external_reference_trigger", + label="External reference trigger mode", get_cmd="RTRG?", set_cmd="RTRG {}", val_mapping={ @@ -716,7 +716,10 @@ def __init__(self, name, address, max_frequency, reset=False, **kwargs): "POSTTL": 1, "NEG": 2, "NEGTTL": 2, - } + }, + docstring="The triggering mode for synchronization of the " + "internal reference signal with the externally provided " + "one" ) self.add_parameter( From 4f60e3d278fe78c6612294ead8491ca040a9e4c7 Mon Sep 17 00:00:00 2001 From: sohailc Date: Mon, 2 Jul 2018 15:55:37 +0200 Subject: [PATCH 110/150] Add parameter for input resistance of external reference --- .../stanford_research/SR86x.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/qcodes/instrument_drivers/stanford_research/SR86x.py b/qcodes/instrument_drivers/stanford_research/SR86x.py index 251c4268d3f..98285eeb0fa 100644 --- a/qcodes/instrument_drivers/stanford_research/SR86x.py +++ b/qcodes/instrument_drivers/stanford_research/SR86x.py @@ -735,6 +735,23 @@ def __init__(self, name, address, max_frequency, reset=False, **kwargs): } ) + self.add_parameter( + name="external_reference_trigger_input_resistance", + label="External reference trigger input resistance", + get_cmd="REFZ?", + set_cmd="REFZ {}", + val_mapping={ + "50": 0, + "50OHMS": 0, + 0: 0, + "1M": 1, + "1MEG": 1, + 1: 1, + }, + docstring="Input resistance of the input for the external " + "reference signal" + ) + # Auto functions self.add_function('auto_range', call_cmd='ARNG') self.add_function('auto_scale', call_cmd='ASCL') From 05803457e02588fe15b16ea9f59cb5510729724a Mon Sep 17 00:00:00 2001 From: sohailc Date: Mon, 2 Jul 2018 15:56:01 +0200 Subject: [PATCH 111/150] Add docstring for reference source parameter --- qcodes/instrument_drivers/stanford_research/SR86x.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qcodes/instrument_drivers/stanford_research/SR86x.py b/qcodes/instrument_drivers/stanford_research/SR86x.py index 98285eeb0fa..22c72f3ecec 100644 --- a/qcodes/instrument_drivers/stanford_research/SR86x.py +++ b/qcodes/instrument_drivers/stanford_research/SR86x.py @@ -732,7 +732,8 @@ def __init__(self, name, address, max_frequency, reset=False, **kwargs): "EXT": 1, "DUAL": 2, "CHOP": 3 - } + }, + docstring="The source of the reference signal" ) self.add_parameter( From 9467e1b193b695add734b29e3ef8ab6a8ad4387e Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 4 Jul 2018 10:22:32 +0200 Subject: [PATCH 112/150] Keithley 2400 protect read commands In the Keithley 2400 doing a read with output disabled is an error that leaves the insturment in a state where no commands can be sent to it without restart --- .../tektronix/Keithley_2400.py | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/qcodes/instrument_drivers/tektronix/Keithley_2400.py b/qcodes/instrument_drivers/tektronix/Keithley_2400.py index 03a9f6e4df7..fd13de15cbf 100644 --- a/qcodes/instrument_drivers/tektronix/Keithley_2400.py +++ b/qcodes/instrument_drivers/tektronix/Keithley_2400.py @@ -34,14 +34,14 @@ def __init__(self, name, address, **kwargs): label='Current Compliance') self.add_parameter('volt', - get_cmd=':READ?', + get_cmd=self._get_volt_and_curr, get_parser=self._volt_parser, set_cmd=':SOUR:VOLT:LEV {:.8f}', label='Voltage', unit='V') self.add_parameter('curr', - get_cmd=':READ?', + get_cmd=self._get_volt_and_curr, get_parser=self._curr_parser, set_cmd=':SOUR:CURR:LEV {:.8f}', label='Current', @@ -77,11 +77,30 @@ def __init__(self, name, address, **kwargs): label='Current integration time') self.add_parameter('resistance', - get_cmd=':READ?', + get_cmd=self._get_volt_and_curr, get_parser=self._resistance_parser, label='Resistance', unit='Ohm') + def _get_volt_and_curr(self) -> str: + """ + This wrapper function around ":READ?" exists because calling + ":READ?" on an instrument with output disabled is an error. + So first we check that output is on and if not we return + nan for volt, curr etc. + """ + output = self.output.get_latest() + if output is None: + # if get_latest returns None we have + # to ask the instrument for the status of output + output = self.output.get() + + if output == 1: + msg = self.ask(':READ?') + else: + msg = "nan,nan,nan,nan" + return msg + def _set_mode_and_sense(self, msg): # This helps set the correct read out curr/volt if msg == 'VOLT': @@ -115,4 +134,8 @@ def _curr_parser(self, msg): def _resistance_parser(self, msg): fields = [float(x) for x in msg.split(',')] - return fields[0]/fields[1] + try: + res = fields[0] / fields[1] + except ZeroDivisionError: + res = float('NaN') + return res From c066f8dbc541394d5851dd71652d73ae7b39d167 Mon Sep 17 00:00:00 2001 From: sohailc Date: Wed, 4 Jul 2018 11:55:16 +0200 Subject: [PATCH 113/150] Fix bugs in warning message (cherry picked from commit 9960308) --- qcodes/dataset/plotting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qcodes/dataset/plotting.py b/qcodes/dataset/plotting.py index 2f153ad0515..90893ef7b62 100644 --- a/qcodes/dataset/plotting.py +++ b/qcodes/dataset/plotting.py @@ -150,8 +150,8 @@ def set_axis_labels(ax, data, cax=None): else: log.warning('Multi-dimensional data encountered. ' - f'parameter {data[-1].name} depends on ' - f'{len(data-1)} parameters, cannot plot ' + f'parameter {data[-1]["name"]} depends on ' + f'{len(data)-1} parameters, cannot plot ' f'that.') new_colorbars.append(None) From 81d8372a09063c48e06b3c0152d89eef21203527 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 4 Jul 2018 13:05:09 +0200 Subject: [PATCH 114/150] The command returns 5 values so make the placeholder 5 nans --- qcodes/instrument_drivers/tektronix/Keithley_2400.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes/instrument_drivers/tektronix/Keithley_2400.py b/qcodes/instrument_drivers/tektronix/Keithley_2400.py index fd13de15cbf..ff3c8fd1aa1 100644 --- a/qcodes/instrument_drivers/tektronix/Keithley_2400.py +++ b/qcodes/instrument_drivers/tektronix/Keithley_2400.py @@ -98,7 +98,7 @@ def _get_volt_and_curr(self) -> str: if output == 1: msg = self.ask(':READ?') else: - msg = "nan,nan,nan,nan" + msg = "nan,nan,nan,nan,nan" return msg def _set_mode_and_sense(self, msg): From 61c6723dcff3978bb369e653c04b6e1f07bee20b Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 4 Jul 2018 13:05:52 +0200 Subject: [PATCH 115/150] give function a more general name This does not only give out volt and curr --- qcodes/instrument_drivers/tektronix/Keithley_2400.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qcodes/instrument_drivers/tektronix/Keithley_2400.py b/qcodes/instrument_drivers/tektronix/Keithley_2400.py index ff3c8fd1aa1..5d1b3de631a 100644 --- a/qcodes/instrument_drivers/tektronix/Keithley_2400.py +++ b/qcodes/instrument_drivers/tektronix/Keithley_2400.py @@ -34,14 +34,14 @@ def __init__(self, name, address, **kwargs): label='Current Compliance') self.add_parameter('volt', - get_cmd=self._get_volt_and_curr, + get_cmd=self._get_read_output_protected, get_parser=self._volt_parser, set_cmd=':SOUR:VOLT:LEV {:.8f}', label='Voltage', unit='V') self.add_parameter('curr', - get_cmd=self._get_volt_and_curr, + get_cmd=self._get_read_output_protected, get_parser=self._curr_parser, set_cmd=':SOUR:CURR:LEV {:.8f}', label='Current', @@ -77,12 +77,12 @@ def __init__(self, name, address, **kwargs): label='Current integration time') self.add_parameter('resistance', - get_cmd=self._get_volt_and_curr, + get_cmd=self._get_read_output_protected, get_parser=self._resistance_parser, label='Resistance', unit='Ohm') - def _get_volt_and_curr(self) -> str: + def _get_read_output_protected(self) -> str: """ This wrapper function around ":READ?" exists because calling ":READ?" on an instrument with output disabled is an error. From ad62cc82c5d210f8ca44fc4c5029a79363cd2162 Mon Sep 17 00:00:00 2001 From: astafan8 Date: Wed, 4 Jul 2018 17:46:49 +0200 Subject: [PATCH 116/150] Bring back capture_samples (captures immediately and continuously) It has been accidentally removed, but now it is back and implemented in a better way rather than "sleeping" for "capture_rate*sample_count" amount of time. --- .../stanford_research/SR86x.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/qcodes/instrument_drivers/stanford_research/SR86x.py b/qcodes/instrument_drivers/stanford_research/SR86x.py index 22c72f3ecec..affb84dd502 100644 --- a/qcodes/instrument_drivers/stanford_research/SR86x.py +++ b/qcodes/instrument_drivers/stanford_research/SR86x.py @@ -530,6 +530,30 @@ def start_capture_at_trigger(self, self.stop_capture() return self.get_capture_data(sample_count) + def capture_samples(self, sample_count: int) -> dict: + """ + Capture a number of samples continuously and starting immediately. + The function blocks until the required number of samples is acquired, + and returns them. + + Args: + sample_count + Number of samples to capture + + Returns: + data + The keys in the dictionary correspond to the captured + variables. For instance, if before the capture, the capture + config was set as 'capture_config("X,Y")', then the keys will + be "X" and "Y". The values in the dictionary are numpy arrays + of numbers. + """ + self.set_capture_length_to_fit_samples(sample_count) + self.start_capture("CONT", "IMM") + self.wait_until_samples_captured(sample_count) + self.stop_capture() + return self.get_capture_data(sample_count) + class SR86x(VisaInstrument): """ From 4eb291559ffd70ed7e2f6664fd2012feb22735d8 Mon Sep 17 00:00:00 2001 From: astafan8 Date: Wed, 4 Jul 2018 17:49:02 +0200 Subject: [PATCH 117/150] Rename start_capture_at_trigger to capture_samples_after_trigger This is more consistent with the names of other methods. It is also a more correct name since the capture is not only started but also stopped within this method. --- qcodes/instrument_drivers/stanford_research/SR86x.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qcodes/instrument_drivers/stanford_research/SR86x.py b/qcodes/instrument_drivers/stanford_research/SR86x.py index affb84dd502..7e407b9ed77 100644 --- a/qcodes/instrument_drivers/stanford_research/SR86x.py +++ b/qcodes/instrument_drivers/stanford_research/SR86x.py @@ -500,10 +500,10 @@ def capture_one_sample_per_trigger(self, self.stop_capture() return self.get_capture_data(trigger_count) - def start_capture_at_trigger(self, - sample_count: int, - send_trigger: Callable - ) -> dict: + def capture_samples_after_trigger(self, + sample_count: int, + send_trigger: Callable + ) -> dict: """ Capture a number of samples after a trigger has been received. Please refer to page 135 of the manual for details. From c7a5e16d07b254188d1d808cde95f84d8bf9f66d Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 5 Jul 2018 10:26:39 +0200 Subject: [PATCH 118/150] Log the instrument that a failed parameter update comes from. this makes it much easier to identify the relevant parameter when adding multiple instruments in a startup script --- qcodes/instrument/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qcodes/instrument/base.py b/qcodes/instrument/base.py index e298a37aa6f..809a2bcca21 100644 --- a/qcodes/instrument/base.py +++ b/qcodes/instrument/base.py @@ -179,9 +179,9 @@ def snapshot_base(self, update: bool=False, except: # really log this twice. Once verbose for the UI and once # at lower level with more info for file based loggers - log.warning("Snapshot: Could not update parameter:" - "{}".format(name)) - log.info("Details for Snapshot of {}:".format(name), + log.warning(f"Snapshot: Could not update parameter: " + f"{name} on {self.full_name}") + log.info(f"Details for Snapshot of {name}:", exec_info=True) snap['parameters'][name] = param.snapshot(update=False) From 99b63acd00ebbdf54bf0d93185c287850da438c4 Mon Sep 17 00:00:00 2001 From: astafan8 Date: Thu, 5 Jul 2018 11:40:58 +0200 Subject: [PATCH 119/150] wait_until_samples_captured now is aware of number of variables Before it was not, which resulted in a bug for cases where the capture config had more than one variable, that the function would return before the requested number of samples is acquired. --- qcodes/instrument_drivers/stanford_research/SR86x.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qcodes/instrument_drivers/stanford_research/SR86x.py b/qcodes/instrument_drivers/stanford_research/SR86x.py index 7e407b9ed77..abd57a8adbd 100644 --- a/qcodes/instrument_drivers/stanford_research/SR86x.py +++ b/qcodes/instrument_drivers/stanford_research/SR86x.py @@ -323,7 +323,8 @@ def wait_until_samples_captured(self, sample_count: int) -> None: Number of samples that needs to be captured """ n_captured_bytes = 0 - n_bytes_to_capture = sample_count * self.bytes_per_sample + n_variables = self._get_number_of_capture_variables() + n_bytes_to_capture = sample_count * n_variables * self.bytes_per_sample while n_captured_bytes < n_bytes_to_capture: n_captured_bytes = self.count_capture_bytes() From 0b79c239cce4848d0df655994f85c783fa0e92a3 Mon Sep 17 00:00:00 2001 From: astafan8 Date: Thu, 5 Jul 2018 11:45:46 +0200 Subject: [PATCH 120/150] capture_samples: change capture mode to one-shot from continuous In "continuous" capture mode, this function does not work at high capture rates because the buffer starts getting overwritten *before* wait_until_samples_captured realizes that enough samples have been captured. This occurs due to the fact that VISA call to capture_bytes in the while in wait_until_samples_captured cannot keep up with the lock-in acquisition speed at high capture rates, hence the data gets "wrapped" (in other words, the buffer gets overwritten) which results in the value returned by capture_bytes to be small again, thus the while loop is not escaped. --- qcodes/instrument_drivers/stanford_research/SR86x.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/qcodes/instrument_drivers/stanford_research/SR86x.py b/qcodes/instrument_drivers/stanford_research/SR86x.py index abd57a8adbd..ce541407b24 100644 --- a/qcodes/instrument_drivers/stanford_research/SR86x.py +++ b/qcodes/instrument_drivers/stanford_research/SR86x.py @@ -533,7 +533,10 @@ def capture_samples_after_trigger(self, def capture_samples(self, sample_count: int) -> dict: """ - Capture a number of samples continuously and starting immediately. + Capture a number of samples at a capture rate, starting immediately. + Unlike the "continuous" capture mode, here the buffer does not get + overwritten with the new data once the buffer is full. + The function blocks until the required number of samples is acquired, and returns them. @@ -550,7 +553,7 @@ def capture_samples(self, sample_count: int) -> dict: of numbers. """ self.set_capture_length_to_fit_samples(sample_count) - self.start_capture("CONT", "IMM") + self.start_capture("ONE", "IMM") self.wait_until_samples_captured(sample_count) self.stop_capture() return self.get_capture_data(sample_count) From 7423496ef7382018e218a2925707d137593fa459 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 5 Jul 2018 13:05:54 +0200 Subject: [PATCH 121/150] make sure that steps are also validated --- qcodes/instrument/parameter.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qcodes/instrument/parameter.py b/qcodes/instrument/parameter.py index 12ddf0e3345..03407880ad1 100644 --- a/qcodes/instrument/parameter.py +++ b/qcodes/instrument/parameter.py @@ -415,6 +415,9 @@ def set_wrapper(value, **kwargs): steps = self.get_ramp_values(value, step=self.step) for step_index, val_step in enumerate(steps): + # even if the final value is valid we may be generating + # steps that are not so validate them too + self.validate(val_step) if self.val_mapping is not None: # Convert set values using val_mapping dictionary raw_value = self.val_mapping[val_step] From b52ea07bc921e4589aa981e4302d7479ea11a127 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 5 Jul 2018 13:06:51 +0200 Subject: [PATCH 122/150] Add test to ensure that intermediate steps are validated --- qcodes/tests/test_parameter.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/qcodes/tests/test_parameter.py b/qcodes/tests/test_parameter.py index 4092c5a779c..9666985571c 100644 --- a/qcodes/tests/test_parameter.py +++ b/qcodes/tests/test_parameter.py @@ -4,6 +4,7 @@ from collections import namedtuple, Iterable from unittest import TestCase from typing import Tuple +import pytest import numpy as np from hypothesis import given, event, settings @@ -14,7 +15,7 @@ InstrumentRefParameter) import qcodes.utils.validators as vals from qcodes.tests.instrument_mocks import DummyInstrument - +from qcodes.utils.validators import Numbers class GettableParam(Parameter): @@ -436,6 +437,31 @@ def test_ramp_parsed_scaled(self, scale, value): np.testing.assert_allclose(p.get(), value) assert p.raw_value == -scale * value + def test_steppeing_from_invalid_starting_point(self): + + the_value = -10 + + def set_function(value): + nonlocal the_value + the_value = value + + def get_function(): + return the_value + + a = Parameter('test', set_cmd=set_function, get_cmd=get_function, + vals=Numbers(0, 100), step=5) + # We start out by setting the parameter to an + # invalid value. This is not possible using initial_value + # as the validator will catch that but perhaps this may happen + # if the instrument can return out of range values. + assert a.get() == -10 + with pytest.raises(ValueError): + # trying to set to 10 should raise even with 10 valid + # as the steps demand that we first step to -5 which is not + a.set(10) + # afterwards the value should still be the same + assert a.get() == -10 + class TestValsandParseParameter(TestCase): From 4cb299e7e59999507247138184d7af3c0205eb8c Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 5 Jul 2018 13:41:49 +0200 Subject: [PATCH 123/150] The number of validations have now changed --- qcodes/tests/test_parameter.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/qcodes/tests/test_parameter.py b/qcodes/tests/test_parameter.py index 9666985571c..34da0013ccd 100644 --- a/qcodes/tests/test_parameter.py +++ b/qcodes/tests/test_parameter.py @@ -157,14 +157,21 @@ def test_number_of_validations(self): p = Parameter('p', set_cmd=None, initial_value=0, vals=BookkeepingValidator()) - - self.assertEqual(p.vals.values_validated, [0]) + # in the set wrapper the final value is validated + # and then subsequently each step is validated. + # in this case there is one step so the final value + # is validated twice. + self.assertEqual(p.vals.values_validated, [0, 0]) p.step = 1 p.set(10) - + # Intermediate steps are both validated before setting and + # at save_val stage. The final step is not validated at save_val + # stage but before stepping and as the final step. self.assertEqual(p.vals.values_validated, - [0, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + [0, 0, 10, 1, 1, 2, 2, 3, 3, + 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, + 9, 9, 10]) def test_snapshot_value(self): p_snapshot = Parameter('no_snapshot', set_cmd=None, get_cmd=None, From 5d9ff61e3ad0ecf667f159347d458d3594b11d0d Mon Sep 17 00:00:00 2001 From: astafan8 Date: Thu, 5 Jul 2018 14:47:51 +0200 Subject: [PATCH 124/150] Update the example notebook for SR86x driver It is now up to date with the driver implementation, and covers three popular capture modes. --- ...ple_with_SR86x_with_buffered_readout.ipynb | 3189 ++++++++++++++++- 1 file changed, 3035 insertions(+), 154 deletions(-) diff --git a/docs/examples/driver_examples/QCodes_example_with_SR86x_with_buffered_readout.ipynb b/docs/examples/driver_examples/QCodes_example_with_SR86x_with_buffered_readout.ipynb index 85cfa18f480..a07453cefde 100644 --- a/docs/examples/driver_examples/QCodes_example_with_SR86x_with_buffered_readout.ipynb +++ b/docs/examples/driver_examples/QCodes_example_with_SR86x_with_buffered_readout.ipynb @@ -1,19 +1,58 @@ { "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Standford Research SR86x Lock-in Amplifier example (with buffered readout)\n", + "\n", + "This notebook provides a code example of usage of the driver for Standford Research SR86x lock-in amplifier. Special attention is given to reading the captured data from the lock-in's internal buffer.\n", + "\n", + "This notebook covers several capturing modes including starting capture at trigger, and capturing one sample per trigger. For the purpose of this example, a Tektronix AWG5208 will be used to send trigger signals. One can also use `qcodes.instrument_drivers.QuTech.IVVI` with its `trigger` method, or else." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setting up infrastructure for the examples" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Imports" + ] + }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "%matplotlib notebook\n", - "import time \n", "\n", + "# Useful utilities\n", + "import time\n", "import matplotlib.pyplot as plt \n", + "import numpy\n", "\n", + "# QCoDeS\n", "import qcodes\n", - "from qcodes.instrument_drivers.stanford_research.SR860 import SR860\n", - "from qcodes.instrument_drivers.QuTech.IVVI import IVVI" + "\n", + "# Drivers\n", + "from qcodes.instrument_drivers.stanford_research.SR860 \\\n", + " import SR860 # the lock-in amplifier\n", + "from qcodes.instrument_drivers.tektronix.AWG5208 \\\n", + " import AWG5208 # used for sending trigger signals" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Initialize the driver" ] }, { @@ -25,37 +64,40 @@ "name": "stdout", "output_type": "stream", "text": [ - "Connected to: Stanford_Research_Systems SR860 (serial:003101, firmware:V1.47) in 0.12s\n" + "Connected to: Stanford_Research_Systems SR860 (serial:003230, firmware:V1.47) in 0.23s\n" ] } ], "source": [ - "sr = SR860(\"sr\", \"GPIB0::4::INSTR\")" + "lockin = SR860(\"lockin\", \"GPIB0::4::INSTR\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Set capture configuration\n", + "\n", + "The lock-in needs to know what values shall be captured. Let's capture \"X\" and \"Y\"." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Initialized IVVI-rack in 0.02s\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "C:\\Users\\Administrator\\Documents\\development\\qcodes_dev\\qcodes\\instrument\\parameter.py:182: UserWarning: Delay kwarg is deprecated. Replace with inter_delay or post_delay as needed\n", - " warnings.warn(\"Delay kwarg is deprecated. Replace with \"\n" - ] - } - ], + "outputs": [], + "source": [ + "lockin.input_config('a')\n", + "lockin.buffer.capture_config('X,Y')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, "source": [ - "ivvi = IVVI(\"ivvi\", \"COM4\")" + "### Set lock-in parameters\n", + "\n", + "Let's tune the lock-in so that it measures something reasonable. Let's not attach any cables to the lock-in, and try to perform a measurement of the noise that the instrument is capturing. The most obvious noise source is the grid, 50 Hz, hence let's set the frequency of the lock-in amplifier somewhere below that frequency to see modulations instead of a flat value that we could have measured at 50 Hz." ] }, { @@ -64,174 +106,181 @@ "metadata": {}, "outputs": [], "source": [ - "sr.buffer.capture_config(\"X,Y\")\n", - "data = sr.buffer.capture_samples(100)" + "lockin.input_range(10e-3) # V\n", + "\n", + "lockin.sensitivity(500e-3) # V\n", + "lockin.frequency(27.3645) # Hz\n", + "lockin.time_constant(10e-3) # s" ] }, { - "cell_type": "code", - "execution_count": 5, + "cell_type": "markdown", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "99" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "len(data[\"X\"])" + "### Initialize AWG" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "metadata": {}, "outputs": [ { - "data": { - "image/png": "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\nwgfvcgAAAABJRU5ErkJggg==\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" + "name": "stdout", + "output_type": "stream", + "text": [ + "Connected to: TEKTRONIX AWG5208 (serial:B020205, firmware:FV:6.0.0242.0) in 0.12s\n" + ] } ], "source": [ - "plt.plot(data[\"X\"])\n", - "plt.show()" + "awg = AWG5208(\"awg\", address='TCPIP0::169.254.254.84::inst0::INSTR')" ] }, { - "cell_type": "code", - "execution_count": 7, + "cell_type": "markdown", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAaMAAAD8CAYAAADaOstiAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzt3Xl8VPW9//HXhyzs+yI7YQlIkE0Ddala0SpUKtyKFuqC\nXlttr/5sq7ZqW7tY2ytutFZt6261Ftcqeq87oFDLEmQRkCWGsAuBBAhL1vn8/pijN00zyQBJTjJ5\nPx+PeWTmO9/lc+aEfPie851zzN0REREJU7OwAxAREVEyEhGR0CkZiYhI6JSMREQkdEpGIiISOiUj\nEREJnZKRiIiETslIRERCp2QkIiKhSw47gMaiS5cunpaWFnYYIiKNytKlS3e7e9ea6ikZxSktLY2s\nrKywwxARaVTMbFM89XSYTkREQqdkJCIioVMyEhGR0CkZiYhI6JSMREQkdEpGIiISOiUjEREJnZKR\niIhUKRJx7nh9Ddm7DtT5WEpGIiJSpb8v28ajCzaybHNBnY+lZCQiIv+msKiU/35jLaP6dODCE3vX\n+Xi6HJCIiPyb+9/bwJ6DxTw2PZNmzazOx9PMSERE/kX2rgM88Y9cLj6pDyP7dKiXMZWMRETkC+7O\nr15bTcvUJH40fki9jatkJCIiX3hnzU7mb9jND88ZTJc2zettXCUjEREBoKi0nF//zxoGH9eGy07p\nV69jawGDiIgA8Oj8HLbkH+av3/4SKUn1O1fRzEhERNi+9zAPzv2UCSd057RBXep9fCUjERHhv99Y\nS8Sdn3xtaCjjKxmJiDRxi3L28NqK7Vxz5kD6dGoVSgxKRiIiTVhZeYRfzF5Nrw4t+d6ZA0OLQ8lI\nRKQJ+9vizaz9rJCfnj+UlqlJocWhZCQi0kTlHyzhnrfXc8qAzkw4oXuosSgZiYg0Ufe+vY4DxWX8\n8oJhmNX99eeqo2QkItIErdq2j2cXb+ayk/sxpHvbsMNRMhIRaWrcnV/OXk2nVqn88KuDww4HUDIS\nEWlyXlm+jaxNBfx4/BDat0wJOxwgzmRkZuPNbJ2ZZZvZLVW839zMngveX2RmaRXeuzUoX2dm59XU\np5n1D/rYEPSZWt0YZtbZzOaa2QEze6BSXPOCMZYHj25B+RVmlleh/NtH8qGJiDRWB4rL+O//XcvI\n3u256KQ+YYfzhRqTkZklAQ8CE4AMYJqZZVSqdhVQ4O6DgJnAjKBtBjAVGAaMBx4ys6Qa+pwBzHT3\ndKAg6DvmGEARcBtwU4xNuMTdRwWPXRXKn6tQ/mhNn4OISCL4w3sb2FVYzC8vGFYvN82LVzwzo7FA\ntrvnuHsJMAuYVKnOJOCp4PmLwNkWXZoxCZjl7sXuvhHIDvqrss+gzbigD4I+J1c3hrsfdPcFRJOS\niIjE8GneAR7/x0YuzuzN6L4dww7nX8STjHoBWyq83hqUVVnH3cuAfUDnatrGKu8M7A36qDxWrDFq\n8kRwKO42+9e1ixea2Uoze9HMGs5cVUSkDkRvmreGFilJ/Hj88WGH82/iSUZVzeM8zjq1VR5vHJVd\n4u7DgdODx2VB+WtAmruPAN7l/2Zc/8LMrjazLDPLysvLq2EoEZGG6501O/lgfV693zQvXvEko61A\nxZlDb2B7rDpmlgy0B/KraRurfDfQIeij8lixxojJ3bcFPwuBZ4keHsTd97h7cVDtEeCkGO0fdvdM\nd8/s2rVrdUOJiDRYRaXl3P56ODfNi1c8yWgJkB6scksluiBhdqU6s4HpwfMpwBx396B8arASrj+Q\nDiyO1WfQZm7QB0Gfr9YwRpXMLNnMugTPU4CJwKrgdY8KVS8APonjcxARaZT+/H4OWwsO88sLhtX7\nTfPiVeOdXt29zMyuA94CkoDH3X21md0OZLn7bOAx4GkzyyY6W5katF1tZs8Da4Ay4Fp3Lweoqs9g\nyJuBWWZ2B7As6JtYYwR95QLtgFQzmwycC2wC3goSURLRw3GPBE2uN7MLgpjygSvi/8hERBqPLfmH\neGheNueP6MGpA+v/pnnxsmomF1JBZmamZ2VlhR2GiMgR+e7TS3l/fR7v3XgmPTu0rPfxzWypu2fW\nVK9hztdEROSYLdiwmzdXf8a1Zw0MJREdCSUjEZEEVFoe4ZevraZvp1Z8+/QBYYdTIyUjEZEE9NSH\nuWTvOsDPJ2bQIiW8m+bFS8lIRCTB5BUW8/t3N/CVIV05e2i3sMOJi5KRiEiCmfHmWorKyvn5xIzQ\nb5oXLyUjEZEE8tHmAl5cupX//HJ/BnRtE3Y4cVMyEhFJEOUR5xevrua4ds25flx62OEcESUjEZEE\n8XzWFj7eto+ffG0orZvXeE2DBkXJSEQkAew9VMJdb65lbP9OXDCyZ9jhHDElIxGRBHDfO+vZd7iU\nX10wrNEsWqhIyUhEpJFbtW0fzyzcxGUn92Noj3Zhh3NUlIxERBqxSMS57dVVdGqdyg3nDgk7nKOm\nZCQi0oi9sHQLyzbv5dYJQ2nfMiXscI6akpGISCNVcLCEO99Yy5i0jnzjxF5hh3NMlIxERBqpu99e\nx/6iMm6fdEKjXLRQkZKRiEgjtHRTPn9bvJnpp6Q12kULFSkZiYg0MiVlEW59+WN6tGvBDecODjuc\nWtG4vqIrIiI8/MGnrN95gEcvz6RNI7vSQiyaGYmINCI5eQe4f0425w/vwTkZx4UdTq1RMhIRaSQi\nEecnf/+Y5snN+MXXM8IOp1YpGYmINBJ/XbSJhTn53DphKN3atQg7nFqlZCQi0gjk7j7Ib/93LWcM\n7sq0sX3CDqfWKRmJiDRw5RHnxhdWkJJk3HXhiEb/naKqJMYyDBGRBPbI/ByWbipg5jdH0r19Yh2e\n+5xmRiIiDdjq7fu47+31jB/WncmjGvclf6oTVzIys/Fmts7Mss3slireb25mzwXvLzKztArv3RqU\nrzOz82rq08z6B31sCPpMrW4MM+tsZnPN7ICZPVAprnnBGMuDR7dK708xMzezzHg+BxGR+lRYVMq1\nf/2Ijq1T+M1/NP5L/lSnxmRkZknAg8AEIAOYZmaV1xReBRS4+yBgJjAjaJsBTAWGAeOBh8wsqYY+\nZwAz3T0dKAj6jjkGUATcBtwUYxMucfdRwWNXhe1qC1wPLKrpMxARqW/uzq0vf8yWgsP8YdqJdG7T\nPOyQ6lQ8M6OxQLa757h7CTALmFSpziTgqeD5i8DZFk3hk4BZ7l7s7huB7KC/KvsM2owL+iDoc3J1\nY7j7QXdfQDQpHYlfA3cdRTsRkTr310WbeX3lDm48dzBj+3cKO5w6F08y6gVsqfB6a1BWZR13LwP2\nAZ2raRurvDOwN+ij8lixxqjJE8EhutuCZIeZjQb6uPvr1TU0s6vNLMvMsvLy8uIYSkTk2K3cupfb\nX1/DmYO78t0zBoYdTr2IJxlVdZDS46xTW+XxxlHZJe4+HDg9eFxmZs2IHua7sYa2uPvD7p7p7pld\nu3atqbqIyDHbub+I7/wli65tmnPfxSNp1ixxzxNVFE8y2gpU/IZVb2B7rDpmlgy0B/KraRurfDfQ\nIeij8lixxojJ3bcFPwuBZ4keHmwLnADMM7Nc4GRgthYxiEjYikrLufovWRQWlfHo9MyEP09UUTzJ\naAmQHqxySyW6IGF2pTqzgenB8ynAHHf3oHxqsBKuP5AOLI7VZ9BmbtAHQZ+v1jBGlcws2cy6BM9T\ngInAKnff5+5d3D3N3dOAhcAF7p4Vx2chIlIn3J2bX1rJiq37mPnNUQlxj6IjUeOXXt29zMyuA94C\nkoDH3X21md0OZLn7bOAx4GkzyyY6W5katF1tZs8Da4Ay4Fp3Lweoqs9gyJuBWWZ2B7As6JtYYwR9\n5QLtgFQzmwycC2wC3goSURLwLvDIUXxGIiJ17v73snl1+XZuOncw5w3rHnY49c6qmVxIBZmZmZ6V\npcmTiNS+55ds4ccvreQbJ/bi3otGJtT3icxsqbvXeBpEV2AQEQnRvHW7uPXvH3N6ehfu/EZiXncu\nHkpGIiIh+XjrPv7rrx8x5Li2/PHSk0hNbrp/kpvulouIhCh390GueGIxHVul8uSVYxLm9uFHS8lI\nRKSe7Sos4vLHFxNx5y9XjU24G+UdjaadikVE6llhUSlXPrGEvMJinv3OlxjYtU3YITUImhmJiNST\nkrII33vmI9Z9VsgfLz2R0X07hh1Sg6GZkYhIPfj8KtwLsndz95QRfGVIt5obNSGaGYmI1IOZ727g\npY+28sNzBnNRZp+aGzQxSkYiInXs+awt3P/eBi7O7M31Zw8KO5wGSclIRKQOLcnN56fBl1p/8x/D\nm+yXWmuiZCQiUke27T3Md59eSp+OrXjgWyeSkqQ/ubHokxERqQOHSsr49lNZlJRHeGR6Ju1bpoQd\nUoOmZCQiUsvcnR+9sJJ1n+3nD9NG67tEcVAyEhGpZU/8I5f/+XgHPx5/vJZwx0nJSESkFi3dlM9v\n//cTzs04jmvOGBB2OI2GkpGISC3Zc6CYa/+6jJ4dWnJ3gt2XqK7pCgwiIrUgEnF+8Nxy8g+V8PL3\nTtWChSOkmZGISC348wc5zN+wm19+fRgn9GofdjiNjpKRiMgxWra5gHvfXsf5w3swbawu9XM0lIxE\nRI7B/qJSrp+1jOPateC339AVFo6WzhmJiBwld+e2V1axfW8Rz19zss4THQPNjEREjtIry7fx6vLt\nfP/sdE7q1ynscBo1JSMRkaOwJf8QP39lNWPSOnLtWboS97FSMhIROULlEeeG55cDcN/Fo0hqpvNE\nx0rnjEREjtAf52WzJLeAmd8cSZ9OrcIOJyHENTMys/Fmts7Mss3slireb25mzwXvLzKztArv3RqU\nrzOz82rq08z6B31sCPpMrW4MM+tsZnPN7ICZPVAprnnBGMuDR7eg/Ltm9nFQtsDMMo7kQxORpmvF\nlr387t0NfH1kTyaP6hV2OAmjxmRkZknAg8AEIAOYVsUf76uAAncfBMwEZgRtM4CpwDBgPPCQmSXV\n0OcMYKa7pwMFQd8xxwCKgNuAm2JswiXuPip47ArKnnX34e4+CrgLuK+mz0FE5FBJGT98bjnd2jbn\njkknaBl3LYpnZjQWyHb3HHcvAWYBkyrVmQQ8FTx/ETjbontpEjDL3YvdfSOQHfRXZZ9Bm3FBHwR9\nTq5uDHc/6O4LiCaluLj7/govWwMeb1sRabru+J9P2LjnIPdePIr2rbSMuzbFk4x6AVsqvN4alFVZ\nx93LgH1A52raxirvDOwN+qg8VqwxavJEcDjuNqvw3xgzu9bMPiU6M7q+qoZmdrWZZZlZVl5eXhxD\niUiiemfNTp5dtJmrzxjAKQPj+dMjRyKeZFTVPLTyTCJWndoqjzeOyi5x9+HA6cHjsi8auj/o7gOB\nm4GfVdXY3R9290x3z+zatWsNQ4lIotpVWMQtL60ko0c7bvjq4LDDSUjxJKOtQMWLLfUGtseqY2bJ\nQHsgv5q2scp3Ax2CPiqPFWuMmNx9W/CzEHiW6OHBymbxf4cCRUT+xed3bT1QXMbvp46ieXJS2CEl\npHiS0RIgPVjllkp0QcLsSnVmA9OD51OAOe7uQfnUYCVcfyAdWByrz6DN3KAPgj5frWGMKplZspl1\nCZ6nABOBVcHr9ApVzwc2xPE5iEgT9NSHuby/Po+fnT+U9OPahh1Owqrxe0buXmZm1wFvAUnA4+6+\n2sxuB7LcfTbwGPC0mWUTna1MDdquNrPngTVAGXCtu5cDVNVnMOTNwCwzuwNYFvRNrDGCvnKBdkCq\nmU0GzgU2AW8FiSgJeBd4JGhynZmdA5QSXbH3eZITEfnCus8K+e0baxl3fDcuPblf2OEkNKtmciEV\nZGZmelZWVthhiEg9KSotZ/KD/2D3gRLe/MHpdGnTPOyQGiUzW+rumTXV0xUYRESqcOcba1n7WSFP\nXDlGiage6Np0IiKVzFm7kyc/zOXK09I4a0i3sMNpEpSMREQq2LW/iJteWMnQHu24ZcLxYYfTZCgZ\niYgEIhHnxhdWcKikjD9M0zLu+qRkJCISeHRBDvM37ObnE4cxqJuWcdcnJSMREaJX477rzXWMH9ad\naWP71NxAapWSkYg0eQeKy7h+1jK6tW3OnRcO19W4Q6Cl3SLS5P381VVsyT/ErKtPoUOr1LDDaZI0\nMxKRJu3vy7by8kfb+H/j0hnbv1PY4TRZSkYi0mTl7j7Iz/6+ijFpHfl/4waFHU6TpmQkIk1SSVmE\n62ctIzmpGb+bOprkJP05DJPOGYlIk3TP2+tYuXUff7r0JHp1aBl2OE2e/isgIk3O++vzePiDHC49\nuS/jT+gedjiCkpGINDG7Cou48fnlDD6uDT87PyPscCSgw3Qi0mREIs4Nz63gQHEZz37nZFqk6HI/\nDYWSkYg0GX/64FMWZO/mzm8MZ7Du2tqg6DCdiDQJSzcVcO/b65k4ogffHKPL/TQ0SkYikvD2Hirh\n+r8to2eHFvz2G7rcT0Okw3QiktAiEefG51ewq7CIF797Ku1apIQdklRBMyMRSWiPzM/hvbW7+OnX\nhjKyT4eww5EYlIxEJGFl5eZz11vr+Nrw7kw/NS3scKQaSkYikpB2FRZx3bPL6N2xJXdeOELniRo4\nnTMSkYRTUhbhv575iL2HS3j5itN0nqgRUDISkYRz++urydpUwP3TRpPRs13Y4Ugc4jpMZ2bjzWyd\nmWWb2S1VvN/czJ4L3l9kZmkV3rs1KF9nZufV1KeZ9Q/62BD0mVrdGGbW2czmmtkBM3ugUlzzgjGW\nB49uQfkNZrbGzFaa2Xtm1u9IPjQRabhmLd7MMws3c82ZA7hgZM+ww5E41ZiMzCwJeBCYAGQA08ys\n8gWdrgIK3H0QMBOYEbTNAKYCw4DxwENmllRDnzOAme6eDhQEfcccAygCbgNuirEJl7j7qOCxKyhb\nBmS6+wjgReCumj4HEWn4PszezW2vruL09C78+Lzjww5HjkA8M6OxQLa757h7CTALmFSpziTgqeD5\ni8DZFj1bOAmY5e7F7r4RyA76q7LPoM24oA+CPidXN4a7H3T3BUSTUlzcfa67HwpeLgR6x9tWRBqm\nT3bs55qnl9K/S2se+NaJJDXTgoXGJJ5k1AvYUuH11qCsyjruXgbsAzpX0zZWeWdgb9BH5bFijVGT\nJ4JDdLdZ1ctprgLeiKMfEWmgtu89zJVPLKFV8ySevHIs7VtqwUJjE08yquoPuMdZp7bK442jskvc\nfThwevC4rOKbZnYpkAncXVVjM7vazLLMLCsvL6+GoUQkDPkHS7jiicUcKC7jySvH0lM3ymuU4klG\nW4GKVxXsDWyPVcfMkoH2QH41bWOV7wY6BH1UHivWGDG5+7bgZyHwLNHDgwR9nAP8FLjA3YtjtH/Y\n3TPdPbNr167VDSUiIdhzoJhvPbKQTXsO8fBlJzG0h1bONVbxJKMlQHqwyi2V6IKE2ZXqzAamB8+n\nAHPc3YPyqcFKuP5AOrA4Vp9Bm7lBHwR9vlrDGFUys2Qz6xI8TwEmAquC16OBPxNNRLti9SEiDdfu\nA8V865FFbNx9kMemj+HUQV3CDkmOQY3fM3L3MjO7DngLSAIed/fVZnY7kOXus4HHgKfNLJvobGVq\n0Ha1mT0PrAHKgGvdvRygqj6DIW8GZpnZHURXvT0WlFc5RtBXLtAOSDWzycC5wCbgrSARJQHvAo8E\nTe4G2gAvBKeRNrv7BfF/bCISps/2FXH544vYnH+Ix68Yw2lKRI2eVTO5kAoyMzM9Kysr7DBEmrw1\n2/fzn08uobColEemZ3LqQCWihszMlrp7Zk31dAUGEWk05q7dxXXPfkS7lim8+L1TdY4ogSgZiUiD\nF4k4D8/P4a431zK0Rzsev2IMx7VrEXZYUouUjESkQdt3qJQbX1jBu5/s5PzhPbhryghaN9efrkSj\nPSoiDdbKrXu59tmP2LG3iF98PYMrTk3TrSASlJKRiDQ4kYjz2IKN3PXWWrq0ac5z15zCSf06hh2W\n1CElIxFpUPYcKObGF1Ywb10e52Ycx11TRtChVWrYYUkdUzISkQZj8cZ8/t/fPqLgUCm/njSMS0/u\np8NyTYSSkYiELhJx/vxBDve8vY6+nVrxxBVjdVO8JkbJSERCdaC4jBueW87ba6Kr5e68cDhtdZvw\nJkfJSERCsyX/EN9+KovsvAP8fGIGV56m1XJNlZKRiIRiUc4evvvMUsojzpNXjuH0dF0ZvylTMhKR\nevfGxzv4/qzl9O7Ukkcvz2RA1zZhhyQhUzISkXr1zMJN3PbqKkb36cDjV4zRsm0BlIxEpJ64O3+Y\nk81976xn3PHdePBbJ9IyNSnssKSBUDISkTrn7tzz9joenPsp3zixFzMuHEFKUjz39pSmQslIROqU\nu/Pb//2ER+ZvZNrYvvxm8gk0a6YVc/KvlIxEpM64O796bQ1PfpjL9FP68csLhmnptlRJyUhE6oS7\nc/vr0UR01Zf787PzhyoRSUw6aCsite7zQ3NP/COXK09LUyKSGikZiUitcndmvLmOR+ZvZPop/fj5\nxAwlIqmRkpGI1Bp359631/On9z/l0pP76hyRxE3JSERqze/f28ADc7OZNrYPt19wghKRxE3JSERq\nxQNzNvC7dzcw5aTe/GbycC3fliOiZCQix+zBudnc8/Z6/mN09AutSkRypJSMROSYPDg3m7vfWsek\nUT25e8oIkpSI5CjElYzMbLyZrTOzbDO7pYr3m5vZc8H7i8wsrcJ7twbl68zsvJr6NLP+QR8bgj5T\nqxvDzDqb2VwzO2BmD1SKa14wxvLg0S0oP8PMPjKzMjObciQfmIj8n4qJ6N6LRpKsS/zIUarxN8fM\nkoAHgQlABjDNzDIqVbsKKHD3QcBMYEbQNgOYCgwDxgMPmVlSDX3OAGa6ezpQEPQdcwygCLgNuCnG\nJlzi7qOCx66gbDNwBfBsTdsvIlV7YM4GJSKpNfH89owFst09x91LgFnApEp1JgFPBc9fBM626DKa\nScAsdy92941AdtBflX0GbcYFfRD0Obm6Mdz9oLsvIJqU4uLuue6+EojE20ZEotyd+95Z/8U5IiUi\nqQ3x/Ab1ArZUeL01KKuyjruXAfuAztW0jVXeGdgb9FF5rFhj1OSJ4BDdbaZ1piLHxN25+6113P9e\ndNXcPUpEUkvi+S2q6g+4x1mntsrjjaOyS9x9OHB68Lishvr/wsyuNrMsM8vKy8s7kqYiCcfd+c3/\nfMJD8z5l2tg+3HWhFitI7YknGW0F+lR43RvYHquOmSUD7YH8atrGKt8NdAj6qDxWrDFicvdtwc9C\noueHxla7pf/e/mF3z3T3zK5dux5JU5GEEok4t726ikcXbOTyU/rpe0RS6+JJRkuA9GCVWyrRBQmz\nK9WZDUwPnk8B5ri7B+VTg5Vw/YF0YHGsPoM2c4M+CPp8tYYxqmRmyWbWJXieAkwEVsWxvSJSQXnE\nufmllTyzcDPXnDGAX10wTIlIal2Nt5Bw9zIzuw54C0gCHnf31WZ2O5Dl7rOBx4CnzSyb6GxlatB2\ntZk9D6wByoBr3b0coKo+gyFvBmaZ2R3AsqBvYo0R9JULtANSzWwycC6wCXgrSERJwLvAI0H9McDf\ngY7A183sV+4+7Mg+OpHEV1oe4YbnV/Daiu18/+x0fnBOui7xI3XCqplcSAWZmZmelZUVdhgi9aao\ntJzrnl3Gu5/s5JYJx/PdMweGHZI0Qma21N0za6qnm+uJyL85XFLO1U9nMX/Dbm6fNIzLT0kLOyRJ\ncEpGIvIv9heVctWTS1i6qYC7p4zgosw+NTcSOUZKRiLyhT0Hirn88cWs31nI/dNGM3FEz7BDkiZC\nyUhEANix7zCXPrqIbXsP8/DlmZw1pFvYIUkTomRUxyKR6AIRLYWVhix7VyHTH1/CvsOl/OU/v8TY\n/p3CDkmaGF3Ho469/vEOJvx+Pm+u2oFWLkpDtHRTAVP+9E+KyyLMuvpkJSIJhZJRHWvbIpnSSITv\nPvMRE/+wgPc+2amkJA3Gu2t2csmjC+nQMoWXv3cqJ/RqH3ZI0kTpMF0dO2tIN04f1IVXlm/n9++t\n56qnshjeqz3XjRvEV4cep8N3Egp3588f5DDjzbUM79Wex68YQ5c2zcMOS5owfek1TrXxpdfS8ggv\nf7SVB+d+yub8QxzfvS3/ddYgzh/eQxeclHpTVFrOT17+mJeXbeP8ET24Z8pIWqYmhR2WJKh4v/Sq\nZBSn2rwCQ1l5hNdWbueBOdl8mneQ/l1a870zBzJ5dC9Sk3XkVOpOTt4BfvDcclZu3ceNXx3MdeMG\n6fI+UqeUjGpZXVwOKBJx3lr9GQ/MzWb19v30aN+C75w+gKlj+9AqVUdQpfa4O39bvIVfv76G1ORm\n3DVlBOcN6x52WNIEKBnVsrq8Np27M299Hn+c+ymLc/Pp2CqF6aemcfkpaXRqnVonY0rTkbv7ILe/\nvoY5a3fx5UFduOeikXRv3yLssKSJUDKqZfV1odSs3Hwemvcpc9buokVKMy46qQ9Xfbk/aV1a1/nY\nklj2HSrl/jkb+Ms/c0lJasaN5w7hylPTtGhG6pWSUS2r76t2r99ZyKPzc3hl2XZKIxHOPr4bV57W\nn1MHdtYxfqnW5j2HeHphLs8t2UJhcRnfzOzDDV8dTLd2mg1J/VMyqmVh3UJi1/4inlm4iWcWbSb/\nYAlDjmvL9FPTmDy6p84ryRf2HSrlvbU7eX3lDuau20UzM8YP6861Zw0io2e7sMOTJkzJqJaFfT+j\notJyZi/fzpMf5rJmx37atUhm2ti+TD81jZ4dWoYWl4TD3Vm/8wDzN+Qxb10eC3P2UBZxurdrwcWZ\nvfnWl/rpvJA0CEpGtSzsZPQ5dydrUwFP/iOXN1d/BsDXhvfgO6f3Z0TvDiFHJ3Vp36FS5mdHk88H\n6/PYVVgMwMCurflqRnfGn9CdEb3a65yQNCi6uV6CMjPGpHViTFonthYc4qkPc5m1eAuvrdjOmYO7\n8v1z0jmxb8eww5Rasu9QKW+u3sFrK3bwz5w9lEec9i1T+HJ6F85M78qX07toZiwJQTOjODWUmVFV\nCotKeXrhJh75IIeCQ6WcObgrP/naUIZ0bxt2aHIU3J0luQU89WEub6/5jNJyJ61zK84f0YNxx3dj\nZO8OJCcgx1teAAAMEklEQVTpy9HSOOgwXS1ryMnocweLy3h64Sb+OO9TCotK+daX+nLDV4fou0qN\nRGl5hFeXb+exBRv5ZMd+2rdM4cITezN5dE+G92qvVZTSKCkZ1bLGkIw+V3CwhN+9u55nFm2mdWoS\nP5uYwUUn9dYfswaqqmsWXnFqGpNG9dI146TRUzKqZY0pGX1uw85CfvrKKhZvzOf09C789j+G06dT\nq7DDkkAk4sxesZ1731nHlvzDjOjdnuvHpXP20G76j4MkDCWjWtYYkxFE/+D9ddEm7nxjLQ784usZ\nXJzZR3/sQuTuzFuXx4w317L2s0IyerTjpvMGc9YQJSFJPFpNJ0D0dueXnZLGWcd340cvrOTmlz5m\nztpd3PmNEXTUuaR699HmAu58Yy2LN+bTt1Mrfj91FF8f0VPLsaXJ08woTo11ZlRRJOI8Mj+He95e\nR8dWqdx78UhOT+8adlhNwvqdhdz79jreWr2TLm1S+f7Z6XxzTF/dMkQSXrwzo7j+JZjZeDNbZ2bZ\nZnZLFe83N7PngvcXmVlahfduDcrXmdl5NfVpZv2DPjYEfaZWN4aZdTazuWZ2wMweqBTXvGCM5cGj\nW03xJrJmzYxrzhzIK9eeRruWKVz22GLueH0NxWXlYYeWsDbtOcgPn1vOeb/7gH9k7+GH5wzm/R+d\nxWWnpCkRiVRQ478GM0sCHgQmABnANDPLqFTtKqDA3QcBM4EZQdsMYCowDBgPPGRmSTX0OQOY6e7p\nQEHQd8wxgCLgNuCmGJtwibuPCh67auirSRjWsz2vXfdlLju5H48u2MjkBz9k7Wf7ww4roeTuPsiP\nXljBuHvf541VO7j6jAHM//FZfP+cdFo319Fxkcri+a/ZWCDb3XPcvQSYBUyqVGcS8FTw/EXgbIue\niZ0EzHL3YnffCGQH/VXZZ9BmXNAHQZ+TqxvD3Q+6+wKiSSleseJtMlqmJvHrySfw2PRMdu0vYuL9\nC7jvnfWUlEXCDq1Ry951gBueX87Z973P7BXbuezkfnzwo7O4dcJQnaMTqUY8/0XrBWyp8Hor8KVY\nddy9zMz2AZ2D8oWV2vYKnlfVZ2dgr7uXVVE/1hi7a4j/CTMrB14C7vDoSbK4+jKzq4GrAfr27VvD\nMI3T2UOP450bzuT211Zz/3sbeHPVDn496QS+NKBz2KE1Kqu27eOhedm8seozmic344pT07jmjAG6\nbYNInOJJRlXNGCqveohVJ1Z5VTOy6urHG0dll7j7NjNrSzQZXQb8Jd6+3P1h4GGILmCoYaxGq1Pr\nVH43dTQXjOrJT/++im8+vJBxx3fjx+OHcHx33X4gFndnYU4+f3r/U95fn0fb5sn811cGcuVp/enS\npnnY4Yk0KvEko61AnwqvewPbY9TZambJQHsgv4a2VZXvBjqYWXIwO6pYP9YYMbn7tuBnoZk9S/Tw\n4F+Opq+mYNzxxzHnxi48+WEuf5yXzYTfz+fcjOOYNrYvp6d3JUnLjwEojzjvrPmMP72fw/Ite+nS\nJpUfnTeES0/uR/uWKWGHJ9IoxZOMlgDpZtYf2EZ0QcK3KtWZDUwH/glMAea4u5vZbOBZM7sP6Amk\nA4uJzkz+rc+gzdygj1lBn69WN0asoIMk08Hdd5tZCjARePdo+mpKWqYm8b2vDGTa2D48/EEOs5Zs\n4a3VO+nVoSUTR/Tg5IGdGZPWiTZN8CT8oZIyXlq6lUcXbGTTnkP07dSKOyafwJSTetMiRZftETkW\ncX3PyMy+BvwOSAIed/ffmNntQJa7zzazFsDTwGiiM4yp7p4TtP0p8J9AGfADd38jVp9B+QCiiagT\nsAy41N2LaxgjF2gHpAJ7gXOBTcAHQEowxrvADe5eXl1fsSTC94yORklZhHfW7GTWks0szNlDabmT\n1MwYfFxb+ndpRb/OrenVoSUdW6XSvmUK7Vom0zIliRYpSTRPaUZqUjNSkpqRnGQkmdHMDDOIOETc\nKY84xWURikrLKSot52BxOQeKyzhYXMaB4jIOl5RzsKSM0vIIn/+qJic1o0PLFDq0SqFzm+b079ya\n9q3qbkaycfdBnv7nJl5YuoXCojJG9enANWcM4Nxh3TVbFKmBLgdUy5pqMqrocEk5SzcV8M+c3azZ\nvp9New6xOf8QZZHwf4c6tU5lYNfWDOvZnpF92jO8VwcGdGl91Fc2KDhYwhurPmP2im0szMknuZkx\nYXgPpp/Sj5P6ddRle0TipGRUy5SMqlZWHmH3gRL2HS5l76ES9heVfTHLKSqLUFYeobQ8Qmm5E4n4\nFzOiZmY0s+gXcZsnN6NFMJtq0zyJNs1TaN08iTbNk2mZmkTr1GRSkpthgFl0thYdr5RdhcXk7j5I\nzu6DbNhZyOrt+zlcGv0Sb9sWyYzs3YERvdszpHtbBnZtw8Cubf7tStjlESf/YAlrduwnKzefJbn5\nZOUWUBZxBnRpzeTRvZg6po9WxokcBV2bTupFclIzurdvQff29feHulUqdGiVSr8qVp+XlUf4NO8g\nK7bsZcXWvSzfspc/f5BDeYXZW6vUJFo3T6Z1ahIHisvJP1jM5283M8jo2Y6rTu/PBSN7ktGjnWZB\nIvVAyUgSSnJSM4Z0b8uQ7m25eEx0wWZRaTmb9hwie9cBcvIOsO9wKQdLyjhYXE6r1CS6tGlOlzap\nDOrWllF9OzTJxRkiYdO/Okl4LVKSvkhQItIw6UqNIiISOiUjEREJnZKRiIiETslIRERCp2QkIiKh\nUzISEZHQKRmJiEjolIxERCR0ujZdnMwsj+iVwI9GF2q+I20iaqrbDU1327XdTUs8293P3bvW1JGS\nUT0ws6x4LhSYaJrqdkPT3XZtd9NSm9utw3QiIhI6JSMREQmdklH9eDjsAELSVLcbmu62a7ubllrb\nbp0zEhGR0GlmJCIioVMyqmNmNt7M1plZtpndEnY8dcXM+pjZXDP7xMxWm9n3g/JOZvaOmW0IfnYM\nO9a6YGZJZrbMzF4PXvc3s0XBdj9nZqlhx1jbzKyDmb1oZmuD/X5KU9jfZvbD4Hd8lZn9zcxaJOr+\nNrPHzWyXma2qUFblPrao+4O/dSvN7MQjGUvJqA6ZWRLwIDAByACmmVlGuFHVmTLgRncfCpwMXBts\n6y3Ae+6eDrwXvE5E3wc+qfB6BjAz2O4C4KpQoqpbvwfedPfjgZFEtz+h97eZ9QKuBzLd/QQgCZhK\n4u7vJ4Hxlcpi7eMJQHrwuBr445EMpGRUt8YC2e6e4+4lwCxgUsgx1Ql33+HuHwXPC4n+YepFdHuf\nCqo9BUwOJ8K6Y2a9gfOBR4PXBowDXgyqJNx2m1k74AzgMQB3L3H3vTSB/U30DtktzSwZaAXsIEH3\nt7t/AORXKo61jycBf/GohUAHM+sR71hKRnWrF7ClwuutQVlCM7M0YDSwCDjO3XdANGEB3cKLrM78\nDvgxEAledwb2untZ8DoR9/sAIA94Ijg8+aiZtSbB97e7bwPuATYTTUL7gKUk/v6uKNY+Pqa/d0pG\ndcuqKEvo5Ytm1gZ4CfiBu+8PO566ZmYTgV3uvrRicRVVE22/JwMnAn9099HAQRLskFxVgvMjk4D+\nQE+gNdHDU5Ul2v6OxzH93isZ1a2tQJ8Kr3sD20OKpc6ZWQrRRPRXd385KN75+VQ9+LkrrPjqyGnA\nBWaWS/Qw7DiiM6UOwWEcSMz9vhXY6u6LgtcvEk1Oib6/zwE2unueu5cCLwOnkvj7u6JY+/iY/t4p\nGdWtJUB6sNImleiJztkhx1QngvMkjwGfuPt9Fd6aDUwPnk8HXq3v2OqSu9/q7r3dPY3o/p3j7pcA\nc4EpQbVE3O7PgC1mNiQoOhtYQ4Lvb6KH5042s1bB7/zn253Q+7uSWPt4NnB5sKruZGDf54fz4qEv\nvdYxM/sa0f8pJwGPu/tvQg6pTpjZl4H5wMf837mTnxA9b/Q80JfoP+SL3L3yCdGEYGZfAW5y94lm\nNoDoTKkTsAy41N2Lw4yvtpnZKKKLNlKBHOBKov/BTej9bWa/Ar5JdAXpMuDbRM+NJNz+NrO/AV8h\nenXuncAvgFeoYh8HyfkBoqvvDgFXuntW3GMpGYmISNh0mE5EREKnZCQiIqFTMhIRkdApGYmISOiU\njEREJHRKRiIiEjolIxERCZ2SkYiIhO7/A3Qm5CO0GUcgAAAAAElFTkSuQmCC\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], "source": [ - "plt.plot(data[\"Y\"])\n", - "plt.show()" + "## One-shot immediate capture\n", + "\n", + "We are going to capture a number of data points (also referred to as samples) at a capture rate, retrieve data from the buffer, and plot it." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# Lets capture by sending triggers. We use the IVVI to send triggers." + "### Set capture rate\n", + "\n", + "The lock-in will be capturing data at some rate. Let's set this rate:" ] }, { "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], + "execution_count": 6, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Capture rate is set to 1220.703125\n" + ] + } + ], "source": [ - "def send_trigger(): \n", - " ivvi.trigger()\n", - " time.sleep(0.1)" + "lockin.buffer.capture_rate(1220.7)\n", + "print(f\"Capture rate is set to {lockin.buffer.capture_rate()}\")" ] }, { - "cell_type": "code", - "execution_count": 9, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "n_samples = 100\n", - "sr.buffer.start_capture(\"ONE\", \"SAMP\")\n", - "\n", - "time.sleep(0.1)\n", - "for _ in range(n_samples): \n", - " send_trigger()\n", - "\n", - "sr.buffer.stop_capture()\n", - "\n", - "data = sr.buffer.get_capture_data(n_samples)" + "In case you want to capture data at a maximum possible rate, but you do not remember the value of the maximum possible capture rate, the convenient `set_capture_rate_to_maximum` method can be used: " ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 7, "metadata": {}, "outputs": [ { - "data": { - "image/png": "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\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" + "name": "stdout", + "output_type": "stream", + "text": [ + "Capture rate is set to its maximum: 39062.5\n" + ] } ], "source": [ - "plt.plot(data[\"X\"])\n", - "plt.show()" + "lockin.buffer.set_capture_rate_to_maximum()\n", + "print(f\"Capture rate is set to its maximum: {lockin.buffer.capture_rate()}\")" ] }, { - "cell_type": "code", - "execution_count": 13, + "cell_type": "markdown", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "100" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "len(data[\"X\"])" + "### Capture data\n", + "\n", + "Now let's acquire some data:" ] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "DataSet:\n", - " location = 'data/2017-11-28/#002_{name}_15-57-34'\n", - " | | | \n", - " Measured | X | X | (100,)\n", - "acquired at 2017-11-28 15:57:34\n" + "The number of acquired X data points is 678\n", + "The number of acquired Y data points is 678\n" ] } ], "source": [ - "meas = qcodes.Measure(sr.buffer.X)\n", - "data = meas.run()" + "sample_count = 678\n", + "\n", + "# Set the capture length (which is a portion of the buffer size) \n", + "# to fit the number of samples that we want to capture.\n", + "# For more information about the maximum buffer size, \n", + "# and other buffer-related properties, refer to the instrument manual.\n", + "lockin.buffer.set_capture_length_to_fit_samples(sample_count)\n", + "\n", + "# Start capturing data immediately, \n", + "# and without overwriting the buffer in case it gets full.\n", + "# For more information about the capture modes, refer to the instrument manual.\n", + "lockin.buffer.start_capture(\"ONE\", \"IMM\")\n", + "\n", + "# We call this blocking method to wait until \n", + "# the requested number of samples is captured.\n", + "lockin.buffer.wait_until_samples_captured(sample_count)\n", + "\n", + "# Stop capturing.\n", + "lockin.buffer.stop_capture()\n", + "\n", + "# Retrieve the data from the buffer.\n", + "# The returned data is a dictionary where keys are names of the variables\n", + "# from the capture config, and values are the captured values.\n", + "data = lockin.buffer.get_capture_data(sample_count)\n", + "\n", + "print(f\"The number of acquired X data points is {len(data['X'])}\")\n", + "print(f\"The number of acquired Y data points is {len(data['Y'])}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For convenience, the measurement code from above is encapsulated into a convenient `capture_samples` method of the lock-in buffer:" ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 9, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "data = lockin.buffer.capture_samples(sample_count)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's plot the captured data:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -316,7 +365,7 @@ " };\n", "\n", " this.imageObj.onunload = function() {\n", - " this.ws.close();\n", + " fig.ws.close();\n", " }\n", "\n", " this.ws.onmessage = this._make_on_message_function(this);\n", @@ -793,7 +842,7 @@ "};\n", "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", "\n", - "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n", + "mpl.extensions = [\"eps\", \"jpeg\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\"];\n", "\n", "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n", " // Create a \"websocket\"-like object which calls the given IPython comm\n", @@ -811,7 +860,7 @@ " // Register the callback with on_msg.\n", " comm.on_msg(function(msg) {\n", " //console.log('receiving', msg['content']['data'], msg);\n", - " // Pass the mpl event to the overriden (by mpl) onmessage function.\n", + " // Pass the mpl event to the overridden (by mpl) onmessage function.\n", " ws.onmessage(msg['content']['data'])\n", " });\n", " return ws;\n", @@ -963,9 +1012,12 @@ " // Check for shift+enter\n", " if (event.shiftKey && event.which == 13) {\n", " this.canvas_div.blur();\n", - " // select the cell after this one\n", - " var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n", - " IPython.notebook.select(index + 1);\n", + " event.shiftKey = false;\n", + " // Send a \"J\" for go to next cell\n", + " event.which = 74;\n", + " event.keyCode = 74;\n", + " manager.command_mode();\n", + " manager.handle_keydown(event);\n", " }\n", "}\n", "\n", @@ -1014,7 +1066,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -1025,35 +1077,2864 @@ } ], "source": [ - "plot = qcodes.MatPlot()\n", - "plot.add(data.X)" + "# Set the figure size to be more convenient then the defaults\n", + "fig = plt.figure(figsize=(6, 3))\n", + "\n", + "# plot values of all the captured variables\n", + "for var_name in data:\n", + " plt.plot(data[var_name], label=var_name)\n", + "plt.xlabel(\"Sample, #\")\n", + "plt.ylabel(\"Measured value, V\")\n", + "\n", + "plt.legend() # show legend\n", + "plt.tight_layout() # adjust the figure so that all the labels fit" ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" + "source": [ + "### Access captured data via Parameter interface\n", + "\n", + "The SR86x driver has the capture variables (X, Y, R, theta) as QCoDeS Parameters in the buffer. This allows to use `lockin.buffer.X.get()` syntax to retrieve the captured data. Beware that it is necessary that the `lockin.buffer.get_capture_data` method has been called before retrieving data via Parameters (in other words, `lockin.buffer.X.get()` does not initiate a new capture, it just returns the latest captured data).\n", + "\n", + "For the sake of demonstration, let's plot the captured data again, but now using the Parameters (notice that this plot contains the same data as the one above):" + ] }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.3" + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "application/javascript": [ + "/* Put everything inside the global mpl namespace */\n", + "window.mpl = {};\n", + "\n", + "\n", + "mpl.get_websocket_type = function() {\n", + " if (typeof(WebSocket) !== 'undefined') {\n", + " return WebSocket;\n", + " } else if (typeof(MozWebSocket) !== 'undefined') {\n", + " return MozWebSocket;\n", + " } else {\n", + " alert('Your browser does not have WebSocket support.' +\n", + " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", + " 'Firefox 4 and 5 are also supported but you ' +\n", + " 'have to enable WebSockets in about:config.');\n", + " };\n", + "}\n", + "\n", + "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", + " this.id = figure_id;\n", + "\n", + " this.ws = websocket;\n", + "\n", + " this.supports_binary = (this.ws.binaryType != undefined);\n", + "\n", + " if (!this.supports_binary) {\n", + " var warnings = document.getElementById(\"mpl-warnings\");\n", + " if (warnings) {\n", + " warnings.style.display = 'block';\n", + " warnings.textContent = (\n", + " \"This browser does not support binary websocket messages. \" +\n", + " \"Performance may be slow.\");\n", + " }\n", + " }\n", + "\n", + " this.imageObj = new Image();\n", + "\n", + " this.context = undefined;\n", + " this.message = undefined;\n", + " this.canvas = undefined;\n", + " this.rubberband_canvas = undefined;\n", + " this.rubberband_context = undefined;\n", + " this.format_dropdown = undefined;\n", + "\n", + " this.image_mode = 'full';\n", + "\n", + " this.root = $('
');\n", + " this._root_extra_style(this.root)\n", + " this.root.attr('style', 'display: inline-block');\n", + "\n", + " $(parent_element).append(this.root);\n", + "\n", + " this._init_header(this);\n", + " this._init_canvas(this);\n", + " this._init_toolbar(this);\n", + "\n", + " var fig = this;\n", + "\n", + " this.waiting = false;\n", + "\n", + " this.ws.onopen = function () {\n", + " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", + " fig.send_message(\"send_image_mode\", {});\n", + " if (mpl.ratio != 1) {\n", + " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", + " }\n", + " fig.send_message(\"refresh\", {});\n", + " }\n", + "\n", + " this.imageObj.onload = function() {\n", + " if (fig.image_mode == 'full') {\n", + " // Full images could contain transparency (where diff images\n", + " // almost always do), so we need to clear the canvas so that\n", + " // there is no ghosting.\n", + " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", + " }\n", + " fig.context.drawImage(fig.imageObj, 0, 0);\n", + " };\n", + "\n", + " this.imageObj.onunload = function() {\n", + " fig.ws.close();\n", + " }\n", + "\n", + " this.ws.onmessage = this._make_on_message_function(this);\n", + "\n", + " this.ondownload = ondownload;\n", + "}\n", + "\n", + "mpl.figure.prototype._init_header = function() {\n", + " var titlebar = $(\n", + " '
');\n", + " var titletext = $(\n", + " '
');\n", + " titlebar.append(titletext)\n", + " this.root.append(titlebar);\n", + " this.header = titletext[0];\n", + "}\n", + "\n", + "\n", + "\n", + "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", + "\n", + "}\n", + "\n", + "\n", + "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", + "\n", + "}\n", + "\n", + "mpl.figure.prototype._init_canvas = function() {\n", + " var fig = this;\n", + "\n", + " var canvas_div = $('
');\n", + "\n", + " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", + "\n", + " function canvas_keyboard_event(event) {\n", + " return fig.key_event(event, event['data']);\n", + " }\n", + "\n", + " canvas_div.keydown('key_press', canvas_keyboard_event);\n", + " canvas_div.keyup('key_release', canvas_keyboard_event);\n", + " this.canvas_div = canvas_div\n", + " this._canvas_extra_style(canvas_div)\n", + " this.root.append(canvas_div);\n", + "\n", + " var canvas = $('');\n", + " canvas.addClass('mpl-canvas');\n", + " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", + "\n", + " this.canvas = canvas[0];\n", + " this.context = canvas[0].getContext(\"2d\");\n", + "\n", + " var backingStore = this.context.backingStorePixelRatio ||\n", + "\tthis.context.webkitBackingStorePixelRatio ||\n", + "\tthis.context.mozBackingStorePixelRatio ||\n", + "\tthis.context.msBackingStorePixelRatio ||\n", + "\tthis.context.oBackingStorePixelRatio ||\n", + "\tthis.context.backingStorePixelRatio || 1;\n", + "\n", + " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", + "\n", + " var rubberband = $('');\n", + " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", + "\n", + " var pass_mouse_events = true;\n", + "\n", + " canvas_div.resizable({\n", + " start: function(event, ui) {\n", + " pass_mouse_events = false;\n", + " },\n", + " resize: function(event, ui) {\n", + " fig.request_resize(ui.size.width, ui.size.height);\n", + " },\n", + " stop: function(event, ui) {\n", + " pass_mouse_events = true;\n", + " fig.request_resize(ui.size.width, ui.size.height);\n", + " },\n", + " });\n", + "\n", + " function mouse_event_fn(event) {\n", + " if (pass_mouse_events)\n", + " return fig.mouse_event(event, event['data']);\n", + " }\n", + "\n", + " rubberband.mousedown('button_press', mouse_event_fn);\n", + " rubberband.mouseup('button_release', mouse_event_fn);\n", + " // Throttle sequential mouse events to 1 every 20ms.\n", + " rubberband.mousemove('motion_notify', mouse_event_fn);\n", + "\n", + " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", + " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", + "\n", + " canvas_div.on(\"wheel\", function (event) {\n", + " event = event.originalEvent;\n", + " event['data'] = 'scroll'\n", + " if (event.deltaY < 0) {\n", + " event.step = 1;\n", + " } else {\n", + " event.step = -1;\n", + " }\n", + " mouse_event_fn(event);\n", + " });\n", + "\n", + " canvas_div.append(canvas);\n", + " canvas_div.append(rubberband);\n", + "\n", + " this.rubberband = rubberband;\n", + " this.rubberband_canvas = rubberband[0];\n", + " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", + " this.rubberband_context.strokeStyle = \"#000000\";\n", + "\n", + " this._resize_canvas = function(width, height) {\n", + " // Keep the size of the canvas, canvas container, and rubber band\n", + " // canvas in synch.\n", + " canvas_div.css('width', width)\n", + " canvas_div.css('height', height)\n", + "\n", + " canvas.attr('width', width * mpl.ratio);\n", + " canvas.attr('height', height * mpl.ratio);\n", + " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", + "\n", + " rubberband.attr('width', width);\n", + " rubberband.attr('height', height);\n", + " }\n", + "\n", + " // Set the figure to an initial 600x600px, this will subsequently be updated\n", + " // upon first draw.\n", + " this._resize_canvas(600, 600);\n", + "\n", + " // Disable right mouse context menu.\n", + " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", + " return false;\n", + " });\n", + "\n", + " function set_focus () {\n", + " canvas.focus();\n", + " canvas_div.focus();\n", + " }\n", + "\n", + " window.setTimeout(set_focus, 100);\n", + "}\n", + "\n", + "mpl.figure.prototype._init_toolbar = function() {\n", + " var fig = this;\n", + "\n", + " var nav_element = $('
')\n", + " nav_element.attr('style', 'width: 100%');\n", + " this.root.append(nav_element);\n", + "\n", + " // Define a callback function for later on.\n", + " function toolbar_event(event) {\n", + " return fig.toolbar_button_onclick(event['data']);\n", + " }\n", + " function toolbar_mouse_event(event) {\n", + " return fig.toolbar_button_onmouseover(event['data']);\n", + " }\n", + "\n", + " for(var toolbar_ind in mpl.toolbar_items) {\n", + " var name = mpl.toolbar_items[toolbar_ind][0];\n", + " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", + " var image = mpl.toolbar_items[toolbar_ind][2];\n", + " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", + "\n", + " if (!name) {\n", + " // put a spacer in here.\n", + " continue;\n", + " }\n", + " var button = $('');\n", + " button.click(method_name, toolbar_event);\n", + " button.mouseover(tooltip, toolbar_mouse_event);\n", + " nav_element.append(button);\n", + " }\n", + "\n", + " // Add the status bar.\n", + " var status_bar = $('');\n", + " nav_element.append(status_bar);\n", + " this.message = status_bar[0];\n", + "\n", + " // Add the close button to the window.\n", + " var buttongrp = $('
');\n", + " var button = $('');\n", + " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", + " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", + " buttongrp.append(button);\n", + " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", + " titlebar.prepend(buttongrp);\n", + "}\n", + "\n", + "mpl.figure.prototype._root_extra_style = function(el){\n", + " var fig = this\n", + " el.on(\"remove\", function(){\n", + "\tfig.close_ws(fig, {});\n", + " });\n", + "}\n", + "\n", + "mpl.figure.prototype._canvas_extra_style = function(el){\n", + " // this is important to make the div 'focusable\n", + " el.attr('tabindex', 0)\n", + " // reach out to IPython and tell the keyboard manager to turn it's self\n", + " // off when our div gets focus\n", + "\n", + " // location in version 3\n", + " if (IPython.notebook.keyboard_manager) {\n", + " IPython.notebook.keyboard_manager.register_events(el);\n", + " }\n", + " else {\n", + " // location in version 2\n", + " IPython.keyboard_manager.register_events(el);\n", + " }\n", + "\n", + "}\n", + "\n", + "mpl.figure.prototype._key_event_extra = function(event, name) {\n", + " var manager = IPython.notebook.keyboard_manager;\n", + " if (!manager)\n", + " manager = IPython.keyboard_manager;\n", + "\n", + " // Check for shift+enter\n", + " if (event.shiftKey && event.which == 13) {\n", + " this.canvas_div.blur();\n", + " event.shiftKey = false;\n", + " // Send a \"J\" for go to next cell\n", + " event.which = 74;\n", + " event.keyCode = 74;\n", + " manager.command_mode();\n", + " manager.handle_keydown(event);\n", + " }\n", + "}\n", + "\n", + "mpl.figure.prototype.handle_save = function(fig, msg) {\n", + " fig.ondownload(fig, null);\n", + "}\n", + "\n", + "\n", + "mpl.find_output_cell = function(html_output) {\n", + " // Return the cell and output element which can be found *uniquely* in the notebook.\n", + " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", + " // IPython event is triggered only after the cells have been serialised, which for\n", + " // our purposes (turning an active figure into a static one), is too late.\n", + " var cells = IPython.notebook.get_cells();\n", + " var ncells = cells.length;\n", + " for (var i=0; i= 3 moved mimebundle to data attribute of output\n", + " data = data.data;\n", + " }\n", + " if (data['text/html'] == html_output) {\n", + " return [cell, data, j];\n", + " }\n", + " }\n", + " }\n", + " }\n", + "}\n", + "\n", + "// Register the function which deals with the matplotlib target/channel.\n", + "// The kernel may be null if the page has been refreshed.\n", + "if (IPython.notebook.kernel != null) {\n", + " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n", + "}\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Set the figure size to be more convenient then the defaults\n", + "fig = plt.figure(figsize=(6, 3))\n", + "\n", + "# plot values of all the captured variables\n", + "for var_name in data:\n", + " plt.plot(data[var_name], label=var_name) \n", + "plt.xlabel(\"Sample, #\")\n", + "plt.ylabel(\"Measured value, V\")\n", + "\n", + "plt.legend() # show legend\n", + "plt.tight_layout() # adjust the figure so that all the labels fit" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Capturing one sample per trigger\n", + "\n", + "In this example, we are going to initiate data capture that will capture one sample per received trigger signal. Lock-in amplifier SR860 reacts on the falling edge of the trigger signal. We are going to use a marker of an AWG channel to send the triggers." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Setup AWG to emit a trigger pulse train\n", + "\n", + "The following code is specific to the AWG5208 that is used in this example. One is welcome to use waveform generation frameworks like `broadbean` rather than the low-level kind of interaction with the AWG driver below." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "awg.sample_rate(3000) # S/s\n", + "\n", + "# Create a waveform where the analog channel plays 0s,\n", + "# while the marker channel plays \n", + "# a number of falling edges from 1 to 0.\n", + "\n", + "n_samples_in_waveform = 3000\n", + "# the waveform will have a length of 1s\n", + "waveform_ch1 = numpy.zeros((2, n_samples_in_waveform))\n", + "\n", + "n_trigger_pulses = 100\n", + "waveform_ch1[1, :] = numpy.tile(\n", + " numpy.concatenate(( # this defines a single trigger pulse\n", + " # that represents a falling edge\n", + " # first half a single pulse is high (1s)\n", + " numpy.ones(n_samples_in_waveform//n_trigger_pulses//2),\n", + " # second half a single pulse is low (0s)\n", + " numpy.zeros(n_samples_in_waveform//n_trigger_pulses//2)\n", + " )),\n", + " n_trigger_pulses\n", + ") # falling from 1 to 0 (a.u.) every 0.01s after the start of the waveform\n", + "\n", + "elements = numpy.array([waveform_ch1]) # we only have one element in the sequence\n", + "waveforms = numpy.array([elements]) # we will use only 1 channel\n", + "\n", + "# Create a sequence file from the \"waveform\" array\n", + "seq_name = 'single_trigger_marker_1'\n", + "seqx = awg.makeSEQXFile(\n", + " trig_waits=[0], nreps=[1], event_jumps=[0], event_jump_to=[0], go_to=[1],\n", + " wfms=waveforms, amplitudes=[1.0], seqname=seq_name)\n", + "\n", + "# Send the sequence file to AWG internal disk\n", + "seqx_file_name = seq_name + '.seqx'\n", + "awg.sendSEQXFile(seqx, seqx_file_name)\n", + "\n", + "awg.clearSequenceList()\n", + "awg.clearWaveformList()\n", + "\n", + "# Load the sequence file\n", + "awg.loadSEQXFile(seqx_file_name)\n", + "\n", + "# Full resolution of the AWG channel is 16 bits for AWG5208,\n", + "# but we take 1 bit for the marker channel\n", + "awg.ch1.resolution = 16 - 1\n", + "\n", + "awg.ch1.awg_amplitude(1) # V\n", + "# Load a particular sequence to the channel\n", + "awg.ch1.setSequenceTrack(seq_name, 0)\n", + "# Assign a waveform to the channel\n", + "awg.ch1.setWaveform(awg.waveformList[0])\n", + "\n", + "# Set the marker's high as high as possible \n", + "# so that the lock-in detects the falling edge\n", + "awg.ch1.marker1_high(1.75) # V\n", + "awg.ch1.marker1_low(0) # V\n", + "\n", + "awg.ch1.state(1) # turns the channel \"on\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Capture data" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The number of acquired X data points is 100\n", + "The number of acquired Y data points is 100\n" + ] + } + ], + "source": [ + "sample_count = n_trigger_pulses\n", + "\n", + "# Set the capture length (which is a portion of the buffer size) \n", + "# to fit the number of samples that we want to acquire.\n", + "# For more information about the maximum buffer size, \n", + "# and other buffer-related properties, refer to the instrument manual.\n", + "lockin.buffer.set_capture_length_to_fit_samples(sample_count)\n", + "\n", + "# Start capturing data, and capture one sample per each received trigger signal.\n", + "# This basically arms the lock-in amplifier.\n", + "# For more information about the capture modes, refer to the instrument manual.\n", + "lockin.buffer.start_capture(\"ONE\", \"TRIG\")\n", + "\n", + "# Play the AWG sequence that contains the trigger pulses train.\n", + "awg.play()\n", + "\n", + "# We call this blocking function to wait\n", + "# until the requested number of samples is captured.\n", + "lockin.buffer.wait_until_samples_captured(sample_count)\n", + "\n", + "# Stop capturing.\n", + "lockin.buffer.stop_capture()\n", + "\n", + "# Stop the AWG sequence playback\n", + "awg.stop()\n", + "\n", + "# Retrieve the data from the buffer.\n", + "# The returned data is a dictionary where keys are names of the variables\n", + "# from the capture config, and values are the captured values.\n", + "data = lockin.buffer.get_capture_data(sample_count)\n", + "\n", + "print(f\"The number of acquired X data points is {len(data['X'])}\")\n", + "print(f\"The number of acquired Y data points is {len(data['Y'])}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For convenience, the measurement code from above is encapsulated into a method of the lock-in buffer `capture_one_sample_per_trigger`:" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "try:\n", + " \n", + " # Note the second argument - it is a callable \n", + " # that is responsible for starting the trigger pulse train\n", + " data = lockin.buffer.capture_one_sample_per_trigger(sample_count, awg.play)\n", + " \n", + "finally: # the try-finally block is here purely \n", + " # to ensure that the AWG is stopped anyway\n", + " awg.stop()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's plot the captured data:" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "application/javascript": [ + "/* Put everything inside the global mpl namespace */\n", + "window.mpl = {};\n", + "\n", + "\n", + "mpl.get_websocket_type = function() {\n", + " if (typeof(WebSocket) !== 'undefined') {\n", + " return WebSocket;\n", + " } else if (typeof(MozWebSocket) !== 'undefined') {\n", + " return MozWebSocket;\n", + " } else {\n", + " alert('Your browser does not have WebSocket support.' +\n", + " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", + " 'Firefox 4 and 5 are also supported but you ' +\n", + " 'have to enable WebSockets in about:config.');\n", + " };\n", + "}\n", + "\n", + "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", + " this.id = figure_id;\n", + "\n", + " this.ws = websocket;\n", + "\n", + " this.supports_binary = (this.ws.binaryType != undefined);\n", + "\n", + " if (!this.supports_binary) {\n", + " var warnings = document.getElementById(\"mpl-warnings\");\n", + " if (warnings) {\n", + " warnings.style.display = 'block';\n", + " warnings.textContent = (\n", + " \"This browser does not support binary websocket messages. \" +\n", + " \"Performance may be slow.\");\n", + " }\n", + " }\n", + "\n", + " this.imageObj = new Image();\n", + "\n", + " this.context = undefined;\n", + " this.message = undefined;\n", + " this.canvas = undefined;\n", + " this.rubberband_canvas = undefined;\n", + " this.rubberband_context = undefined;\n", + " this.format_dropdown = undefined;\n", + "\n", + " this.image_mode = 'full';\n", + "\n", + " this.root = $('
');\n", + " this._root_extra_style(this.root)\n", + " this.root.attr('style', 'display: inline-block');\n", + "\n", + " $(parent_element).append(this.root);\n", + "\n", + " this._init_header(this);\n", + " this._init_canvas(this);\n", + " this._init_toolbar(this);\n", + "\n", + " var fig = this;\n", + "\n", + " this.waiting = false;\n", + "\n", + " this.ws.onopen = function () {\n", + " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", + " fig.send_message(\"send_image_mode\", {});\n", + " if (mpl.ratio != 1) {\n", + " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", + " }\n", + " fig.send_message(\"refresh\", {});\n", + " }\n", + "\n", + " this.imageObj.onload = function() {\n", + " if (fig.image_mode == 'full') {\n", + " // Full images could contain transparency (where diff images\n", + " // almost always do), so we need to clear the canvas so that\n", + " // there is no ghosting.\n", + " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", + " }\n", + " fig.context.drawImage(fig.imageObj, 0, 0);\n", + " };\n", + "\n", + " this.imageObj.onunload = function() {\n", + " fig.ws.close();\n", + " }\n", + "\n", + " this.ws.onmessage = this._make_on_message_function(this);\n", + "\n", + " this.ondownload = ondownload;\n", + "}\n", + "\n", + "mpl.figure.prototype._init_header = function() {\n", + " var titlebar = $(\n", + " '
');\n", + " var titletext = $(\n", + " '
');\n", + " titlebar.append(titletext)\n", + " this.root.append(titlebar);\n", + " this.header = titletext[0];\n", + "}\n", + "\n", + "\n", + "\n", + "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", + "\n", + "}\n", + "\n", + "\n", + "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", + "\n", + "}\n", + "\n", + "mpl.figure.prototype._init_canvas = function() {\n", + " var fig = this;\n", + "\n", + " var canvas_div = $('
');\n", + "\n", + " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", + "\n", + " function canvas_keyboard_event(event) {\n", + " return fig.key_event(event, event['data']);\n", + " }\n", + "\n", + " canvas_div.keydown('key_press', canvas_keyboard_event);\n", + " canvas_div.keyup('key_release', canvas_keyboard_event);\n", + " this.canvas_div = canvas_div\n", + " this._canvas_extra_style(canvas_div)\n", + " this.root.append(canvas_div);\n", + "\n", + " var canvas = $('');\n", + " canvas.addClass('mpl-canvas');\n", + " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", + "\n", + " this.canvas = canvas[0];\n", + " this.context = canvas[0].getContext(\"2d\");\n", + "\n", + " var backingStore = this.context.backingStorePixelRatio ||\n", + "\tthis.context.webkitBackingStorePixelRatio ||\n", + "\tthis.context.mozBackingStorePixelRatio ||\n", + "\tthis.context.msBackingStorePixelRatio ||\n", + "\tthis.context.oBackingStorePixelRatio ||\n", + "\tthis.context.backingStorePixelRatio || 1;\n", + "\n", + " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", + "\n", + " var rubberband = $('');\n", + " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", + "\n", + " var pass_mouse_events = true;\n", + "\n", + " canvas_div.resizable({\n", + " start: function(event, ui) {\n", + " pass_mouse_events = false;\n", + " },\n", + " resize: function(event, ui) {\n", + " fig.request_resize(ui.size.width, ui.size.height);\n", + " },\n", + " stop: function(event, ui) {\n", + " pass_mouse_events = true;\n", + " fig.request_resize(ui.size.width, ui.size.height);\n", + " },\n", + " });\n", + "\n", + " function mouse_event_fn(event) {\n", + " if (pass_mouse_events)\n", + " return fig.mouse_event(event, event['data']);\n", + " }\n", + "\n", + " rubberband.mousedown('button_press', mouse_event_fn);\n", + " rubberband.mouseup('button_release', mouse_event_fn);\n", + " // Throttle sequential mouse events to 1 every 20ms.\n", + " rubberband.mousemove('motion_notify', mouse_event_fn);\n", + "\n", + " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", + " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", + "\n", + " canvas_div.on(\"wheel\", function (event) {\n", + " event = event.originalEvent;\n", + " event['data'] = 'scroll'\n", + " if (event.deltaY < 0) {\n", + " event.step = 1;\n", + " } else {\n", + " event.step = -1;\n", + " }\n", + " mouse_event_fn(event);\n", + " });\n", + "\n", + " canvas_div.append(canvas);\n", + " canvas_div.append(rubberband);\n", + "\n", + " this.rubberband = rubberband;\n", + " this.rubberband_canvas = rubberband[0];\n", + " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", + " this.rubberband_context.strokeStyle = \"#000000\";\n", + "\n", + " this._resize_canvas = function(width, height) {\n", + " // Keep the size of the canvas, canvas container, and rubber band\n", + " // canvas in synch.\n", + " canvas_div.css('width', width)\n", + " canvas_div.css('height', height)\n", + "\n", + " canvas.attr('width', width * mpl.ratio);\n", + " canvas.attr('height', height * mpl.ratio);\n", + " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", + "\n", + " rubberband.attr('width', width);\n", + " rubberband.attr('height', height);\n", + " }\n", + "\n", + " // Set the figure to an initial 600x600px, this will subsequently be updated\n", + " // upon first draw.\n", + " this._resize_canvas(600, 600);\n", + "\n", + " // Disable right mouse context menu.\n", + " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", + " return false;\n", + " });\n", + "\n", + " function set_focus () {\n", + " canvas.focus();\n", + " canvas_div.focus();\n", + " }\n", + "\n", + " window.setTimeout(set_focus, 100);\n", + "}\n", + "\n", + "mpl.figure.prototype._init_toolbar = function() {\n", + " var fig = this;\n", + "\n", + " var nav_element = $('
')\n", + " nav_element.attr('style', 'width: 100%');\n", + " this.root.append(nav_element);\n", + "\n", + " // Define a callback function for later on.\n", + " function toolbar_event(event) {\n", + " return fig.toolbar_button_onclick(event['data']);\n", + " }\n", + " function toolbar_mouse_event(event) {\n", + " return fig.toolbar_button_onmouseover(event['data']);\n", + " }\n", + "\n", + " for(var toolbar_ind in mpl.toolbar_items) {\n", + " var name = mpl.toolbar_items[toolbar_ind][0];\n", + " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", + " var image = mpl.toolbar_items[toolbar_ind][2];\n", + " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", + "\n", + " if (!name) {\n", + " // put a spacer in here.\n", + " continue;\n", + " }\n", + " var button = $('